summaryrefslogtreecommitdiffstats
path: root/ansible_collections/community/windows/plugins/modules
diff options
context:
space:
mode:
Diffstat (limited to 'ansible_collections/community/windows/plugins/modules')
-rw-r--r--ansible_collections/community/windows/plugins/modules/__init__.py0
-rw-r--r--ansible_collections/community/windows/plugins/modules/psexec.py511
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_audit_policy_system.ps1132
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_audit_policy_system.py69
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_audit_rule.ps1174
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_audit_rule.py140
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_auto_logon.ps1404
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_auto_logon.py72
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_certificate_info.ps1115
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_certificate_info.py228
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_computer_description.ps154
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_computer_description.py62
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_credential.ps1724
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_credential.py206
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_data_deduplication.ps1132
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_data_deduplication.py82
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_defrag.ps1101
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_defrag.py98
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_dhcp_lease.ps1446
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_dhcp_lease.py135
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_disk_facts.ps1280
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_disk_facts.py902
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_disk_image.ps179
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_disk_image.py57
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_dns_record.ps1202
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_dns_record.py192
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_dns_zone.ps1267
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_dns_zone.py182
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_domain_computer.ps1316
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_domain_computer.py210
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_domain_group.ps1371
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_domain_group.py236
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_domain_group_membership.ps1136
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_domain_group_membership.py126
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_domain_object_info.ps1284
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_domain_object_info.py157
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_domain_ou.ps1253
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_domain_ou.py174
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_domain_user.ps1564
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_domain_user.py477
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_dotnet_ngen.ps165
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_dotnet_ngen.py80
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_eventlog.ps1287
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_eventlog.py159
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_eventlog_entry.ps1106
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_eventlog_entry.py78
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_feature_info.ps145
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_feature_info.py166
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_file_compression.ps1120
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_file_compression.py93
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_file_version.ps163
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_file_version.py73
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_firewall.ps190
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_firewall.py89
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_firewall_rule.ps1315
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_firewall_rule.py196
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_format.ps1232
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_format.py98
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_hosts.ps1268
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_hosts.py119
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_hotfix.ps1270
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_hotfix.py150
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_http_proxy.ps1268
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_http_proxy.py100
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_iis_virtualdirectory.ps1139
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_iis_virtualdirectory.py90
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_iis_webapplication.ps1140
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_iis_webapplication.py91
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_iis_webapppool.ps1341
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_iis_webapppool.py206
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_iis_webbinding.ps1348
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_iis_webbinding.py145
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_iis_website.ps1175
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_iis_website.py130
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_inet_proxy.ps1496
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_inet_proxy.py171
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_initialize_disk.ps1163
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_initialize_disk.py75
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_lineinfile.ps1483
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_lineinfile.py171
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_listen_ports_facts.ps190
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_listen_ports_facts.py95
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_mapped_drive.ps1449
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_mapped_drive.py147
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_msg.ps152
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_msg.py88
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_net_adapter_feature.ps167
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_net_adapter_feature.py69
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_netbios.ps170
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_netbios.py69
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_nssm.ps1604
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_nssm.py216
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_pagefile.ps1243
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_pagefile.py131
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_partition.ps1371
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_partition.py110
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_pester.ps1125
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_pester.py104
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_power_plan.ps1231
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_power_plan.py68
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_product_facts.ps1105
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_product_facts.py61
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_psexec.ps1161
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_psexec.py167
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_psmodule.ps1549
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_psmodule.py174
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_psmodule_info.ps1305
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_psmodule_info.py435
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_psrepository.ps1179
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_psrepository.py153
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_psrepository_copy.ps1233
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_psrepository_copy.py143
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_psrepository_info.ps168
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_psrepository_info.py107
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_psscript.ps1198
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_psscript.py128
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_psscript_info.ps1129
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_psscript_info.py206
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_pssession_configuration.ps1555
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_pssession_configuration.py390
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_rabbitmq_plugin.ps1156
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_rabbitmq_plugin.py58
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_rds_cap.ps1407
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_rds_cap.py128
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_rds_rap.ps1318
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_rds_rap.py86
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_rds_settings.ps195
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_rds_settings.py57
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_region.ps1444
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_region.py95
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_regmerge.ps199
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_regmerge.py74
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_robocopy.ps1148
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_robocopy.py139
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_route.ps1112
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_route.py62
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_say.ps1108
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_say.py108
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_scheduled_task.ps11234
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_scheduled_task.py539
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_scheduled_task_stat.ps1398
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_scheduled_task_stat.py371
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_scoop.ps1285
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_scoop.py79
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_scoop_bucket.ps1125
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_scoop_bucket.py73
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_security_policy.ps1224
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_security_policy.py118
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_shortcut.ps1381
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_shortcut.py116
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_snmp.ps1132
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_snmp.py70
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_timezone.ps173
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_timezone.py63
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_toast.ps193
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_toast.py94
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_unzip.ps1197
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_unzip.py112
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_user_profile.ps1169
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_user_profile.py109
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_wait_for_process.ps1176
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_wait_for_process.py128
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_wakeonlan.ps152
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_wakeonlan.py57
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_webpicmd.ps1103
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_webpicmd.py35
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_xml.ps1285
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_xml.py139
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_zip.ps173
-rw-r--r--ansible_collections/community/windows/plugins/modules/win_zip.py53
170 files changed, 32939 insertions, 0 deletions
diff --git a/ansible_collections/community/windows/plugins/modules/__init__.py b/ansible_collections/community/windows/plugins/modules/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/__init__.py
diff --git a/ansible_collections/community/windows/plugins/modules/psexec.py b/ansible_collections/community/windows/plugins/modules/psexec.py
new file mode 100644
index 00000000..2a78691d
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/psexec.py
@@ -0,0 +1,511 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Jordan Borean <jborean93@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = r'''
+---
+module: psexec
+short_description: Runs commands on a remote Windows host based on the PsExec
+ model
+description:
+- Runs a remote command from a Linux host to a Windows host without WinRM being
+ set up.
+- Can be run on the Ansible controller to bootstrap Windows hosts to get them
+ ready for WinRM.
+options:
+ hostname:
+ description:
+ - The remote Windows host to connect to, can be either an IP address or a
+ hostname.
+ type: str
+ required: yes
+ connection_username:
+ description:
+ - The username to use when connecting to the remote Windows host.
+ - This user must be a member of the C(Administrators) group of the Windows
+ host.
+ - Required if the Kerberos requirements are not installed or the username
+ is a local account to the Windows host.
+ - Can be omitted to use the default Kerberos principal ticket in the
+ local credential cache if the Kerberos library is installed.
+ - If I(process_username) is not specified, then the remote process will run
+ under a Network Logon under this account.
+ type: str
+ connection_password:
+ description:
+ - The password for I(connection_user).
+ - Required if the Kerberos requirements are not installed or the username
+ is a local account to the Windows host.
+ - Can be omitted to use a Kerberos principal ticket for the principal set
+ by I(connection_user) if the Kerberos library is installed and the
+ ticket has already been retrieved with the C(kinit) command before.
+ type: str
+ port:
+ description:
+ - The port that the remote SMB service is listening on.
+ type: int
+ default: 445
+ encrypt:
+ description:
+ - Will use SMB encryption to encrypt the SMB messages sent to and from the
+ host.
+ - This requires the SMB 3 protocol which is only supported from Windows
+ Server 2012 or Windows 8, older versions like Windows 7 or Windows Server
+ 2008 (R2) must set this to C(no) and use no encryption.
+ - When setting to C(no), the packets are in plaintext and can be seen by
+ anyone sniffing the network, any process options are included in this.
+ type: bool
+ default: yes
+ connection_timeout:
+ description:
+ - The timeout in seconds to wait when receiving the initial SMB negotiate
+ response from the server.
+ type: int
+ default: 60
+ executable:
+ description:
+ - The executable to run on the Windows host.
+ type: str
+ required: yes
+ arguments:
+ description:
+ - Any arguments as a single string to use when running the executable.
+ type: str
+ working_directory:
+ description:
+ - Changes the working directory set when starting the process.
+ type: str
+ default: C:\Windows\System32
+ asynchronous:
+ description:
+ - Will run the command as a detached process and the module returns
+ immediately after starting the process while the process continues to
+ run in the background.
+ - The I(stdout) and I(stderr) return values will be null when this is set
+ to C(yes).
+ - The I(stdin) option does not work with this type of process.
+ - The I(rc) return value is not set when this is C(yes)
+ type: bool
+ default: no
+ load_profile:
+ description:
+ - Runs the remote command with the user's profile loaded.
+ type: bool
+ default: yes
+ process_username:
+ description:
+ - The user to run the process as.
+ - This can be set to run the process under an Interactive logon of the
+ specified account which bypasses limitations of a Network logon used when
+ this isn't specified.
+ - If omitted then the process is run under the same account as
+ I(connection_username) with a Network logon.
+ - Set to C(System) to run as the builtin SYSTEM account, no password is
+ required with this account.
+ - If I(encrypt) is C(no), the username and password are sent as a simple
+ XOR scrambled byte string that is not encrypted. No special tools are
+ required to get the username and password just knowledge of the protocol.
+ type: str
+ process_password:
+ description:
+ - The password for I(process_username).
+ - Required if I(process_username) is defined and not C(System).
+ type: str
+ integrity_level:
+ description:
+ - The integrity level of the process when I(process_username) is defined
+ and is not equal to C(System).
+ - When C(default), the default integrity level based on the system setup.
+ - When C(elevated), the command will be run with Administrative rights.
+ - When C(limited), the command will be forced to run with
+ non-Administrative rights.
+ type: str
+ choices:
+ - limited
+ - default
+ - elevated
+ default: default
+ interactive:
+ description:
+ - Will run the process as an interactive process that shows a process
+ Window of the Windows session specified by I(interactive_session).
+ - The I(stdout) and I(stderr) return values will be null when this is set
+ to C(yes).
+ - The I(stdin) option does not work with this type of process.
+ type: bool
+ default: no
+ interactive_session:
+ description:
+ - The Windows session ID to use when displaying the interactive process on
+ the remote Windows host.
+ - This is only valid when I(interactive) is C(yes).
+ - The default is C(0) which is the console session of the Windows host.
+ type: int
+ default: 0
+ priority:
+ description:
+ - Set the command's priority on the Windows host.
+ - See U(https://msdn.microsoft.com/en-us/library/windows/desktop/ms683211.aspx)
+ for more details.
+ type: str
+ choices:
+ - above_normal
+ - below_normal
+ - high
+ - idle
+ - normal
+ - realtime
+ default: normal
+ show_ui_on_logon_screen:
+ description:
+ - Shows the process UI on the Winlogon secure desktop when
+ I(process_username) is C(System).
+ type: bool
+ default: no
+ process_timeout:
+ description:
+ - The timeout in seconds that is placed upon the running process.
+ - A value of C(0) means no timeout.
+ type: int
+ default: 0
+ stdin:
+ description:
+ - Data to send on the stdin pipe once the process has started.
+ - This option has no effect when I(interactive) or I(asynchronous) is
+ C(yes).
+ type: str
+requirements:
+- pypsexec
+- smbprotocol[kerberos] for optional Kerberos authentication
+notes:
+- This module requires the Windows host to have SMB configured and enabled,
+ and port 445 opened on the firewall.
+- This module will wait until the process is finished unless I(asynchronous)
+ is C(yes), ensure the process is run as a non-interactive command to avoid
+ infinite hangs waiting for input.
+- The I(connection_username) must be a member of the local Administrator group
+ of the Windows host. For non-domain joined hosts, the
+ C(LocalAccountTokenFilterPolicy) should be set to C(1) to ensure this works,
+ see U(https://support.microsoft.com/en-us/help/951016/description-of-user-account-control-and-remote-restrictions-in-windows).
+- For more information on this module and the various host requirements, see
+ U(https://github.com/jborean93/pypsexec).
+seealso:
+- module: ansible.builtin.raw
+- module: ansible.windows.win_command
+- module: community.windows.win_psexec
+- module: ansible.windows.win_shell
+author:
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = r'''
+- name: Run a cmd.exe command
+ community.windows.psexec:
+ hostname: server
+ connection_username: username
+ connection_password: password
+ executable: cmd.exe
+ arguments: /c echo Hello World
+
+- name: Run a PowerShell command
+ community.windows.psexec:
+ hostname: server.domain.local
+ connection_username: username@DOMAIN.LOCAL
+ connection_password: password
+ executable: powershell.exe
+ arguments: Write-Host Hello World
+
+- name: Send data through stdin
+ community.windows.psexec:
+ hostname: 192.168.1.2
+ connection_username: username
+ connection_password: password
+ executable: powershell.exe
+ arguments: '-'
+ stdin: |
+ Write-Host Hello World
+ Write-Error Error Message
+ exit 0
+
+- name: Run the process as a different user
+ community.windows.psexec:
+ hostname: server
+ connection_user: username
+ connection_password: password
+ executable: whoami.exe
+ arguments: /all
+ process_username: anotheruser
+ process_password: anotherpassword
+
+- name: Run the process asynchronously
+ community.windows.psexec:
+ hostname: server
+ connection_username: username
+ connection_password: password
+ executable: cmd.exe
+ arguments: /c rmdir C:\temp
+ asynchronous: yes
+
+- name: Use Kerberos authentication for the connection (requires smbprotocol[kerberos])
+ community.windows.psexec:
+ hostname: host.domain.local
+ connection_username: user@DOMAIN.LOCAL
+ executable: C:\some\path\to\executable.exe
+ arguments: /s
+
+- name: Disable encryption to work with WIndows 7/Server 2008 (R2)
+ community.windows.psexec:
+ hostanme: windows-pc
+ connection_username: Administrator
+ connection_password: Password01
+ encrypt: no
+ integrity_level: elevated
+ process_username: Administrator
+ process_password: Password01
+ executable: powershell.exe
+ arguments: (New-Object -ComObject Microsoft.Update.Session).CreateUpdateInstaller().IsBusy
+
+- name: Download and run ConfigureRemotingForAnsible.ps1 to setup WinRM
+ community.windows.psexec:
+ hostname: '{{ hostvars[inventory_hostname]["ansible_host"] | default(inventory_hostname) }}'
+ connection_username: '{{ ansible_user }}'
+ connection_password: '{{ ansible_password }}'
+ encrypt: yes
+ executable: powershell.exe
+ arguments: '-'
+ stdin: |
+ $ErrorActionPreference = "Stop"
+ $sec_protocols = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::SystemDefault
+ $sec_protocols = $sec_protocols -bor [Net.SecurityProtocolType]::Tls12
+ [Net.ServicePointManager]::SecurityProtocol = $sec_protocols
+ $url = "https://github.com/ansible/ansible/raw/devel/examples/scripts/ConfigureRemotingForAnsible.ps1"
+ Invoke-Expression ((New-Object Net.WebClient).DownloadString($url))
+ exit
+ delegate_to: localhost
+'''
+
+RETURN = r'''
+msg:
+ description: Any exception details when trying to run the process
+ returned: module failed
+ type: str
+ sample: 'Received exception from remote PAExec service: Failed to start "invalid.exe". The system cannot find the file specified. [Err=0x2, 2]'
+stdout:
+ description: The stdout from the remote process
+ returned: success and interactive or asynchronous is 'no'
+ type: str
+ sample: Hello World
+stderr:
+ description: The stderr from the remote process
+ returned: success and interactive or asynchronous is 'no'
+ type: str
+ sample: Error [10] running process
+pid:
+ description: The process ID of the asynchronous process that was created
+ returned: success and asynchronous is 'yes'
+ type: int
+ sample: 719
+rc:
+ description: The return code of the remote process
+ returned: success and asynchronous is 'no'
+ type: int
+ sample: 0
+'''
+
+import traceback
+
+from ansible.module_utils.basic import AnsibleModule, missing_required_lib
+from ansible.module_utils._text import to_bytes, to_text
+
+PYPSEXEC_IMP_ERR = None
+try:
+ from pypsexec import client
+ from pypsexec.exceptions import PypsexecException, PAExecException, \
+ PDUException, SCMRException
+ from pypsexec.paexec import ProcessPriority
+ from smbprotocol.exceptions import SMBException, SMBAuthenticationError, \
+ SMBResponseException
+ import socket
+ HAS_PYPSEXEC = True
+except ImportError:
+ PYPSEXEC_IMP_ERR = traceback.format_exc()
+ HAS_PYPSEXEC = False
+
+KERBEROS_IMP_ERR = None
+try:
+ import gssapi
+ # GSSAPI extension required for Kerberos Auth in SMB
+ from gssapi.raw import inquire_sec_context_by_oid
+ HAS_KERBEROS = True
+except ImportError:
+ KERBEROS_IMP_ERR = traceback.format_exc()
+ HAS_KERBEROS = False
+
+
+def remove_artifacts(module, client):
+ try:
+ client.remove_service()
+ except (SMBException, PypsexecException) as exc:
+ module.warn("Failed to cleanup PAExec service and executable: %s"
+ % to_text(exc))
+
+
+def main():
+ module_args = dict(
+ hostname=dict(type='str', required=True),
+ connection_username=dict(type='str'),
+ connection_password=dict(type='str', no_log=True),
+ port=dict(type='int', required=False, default=445),
+ encrypt=dict(type='bool', default=True),
+ connection_timeout=dict(type='int', default=60),
+ executable=dict(type='str', required=True),
+ arguments=dict(type='str'),
+ working_directory=dict(type='str', default=r'C:\Windows\System32'),
+ asynchronous=dict(type='bool', default=False),
+ load_profile=dict(type='bool', default=True),
+ process_username=dict(type='str'),
+ process_password=dict(type='str', no_log=True),
+ integrity_level=dict(type='str', default='default',
+ choices=['default', 'elevated', 'limited']),
+ interactive=dict(type='bool', default=False),
+ interactive_session=dict(type='int', default=0),
+ priority=dict(type='str', default='normal',
+ choices=['above_normal', 'below_normal', 'high',
+ 'idle', 'normal', 'realtime']),
+ show_ui_on_logon_screen=dict(type='bool', default=False),
+ process_timeout=dict(type='int', default=0),
+ stdin=dict(type='str')
+ )
+ result = dict(
+ changed=False,
+ )
+ module = AnsibleModule(
+ argument_spec=module_args,
+ supports_check_mode=False,
+ )
+
+ process_username = module.params['process_username']
+ process_password = module.params['process_password']
+ use_system = False
+ if process_username is not None and process_username.lower() == "system":
+ use_system = True
+ process_username = None
+ process_password = None
+
+ if process_username is not None and process_password is None:
+ module.fail_json(msg='parameters are required together when not '
+ 'running as System: process_username, '
+ 'process_password')
+ if not HAS_PYPSEXEC:
+ module.fail_json(msg=missing_required_lib("pypsexec"),
+ exception=PYPSEXEC_IMP_ERR)
+
+ hostname = module.params['hostname']
+ connection_username = module.params['connection_username']
+ connection_password = module.params['connection_password']
+ port = module.params['port']
+ encrypt = module.params['encrypt']
+ connection_timeout = module.params['connection_timeout']
+ executable = module.params['executable']
+ arguments = module.params['arguments']
+ working_directory = module.params['working_directory']
+ asynchronous = module.params['asynchronous']
+ load_profile = module.params['load_profile']
+ elevated = module.params['integrity_level'] == "elevated"
+ limited = module.params['integrity_level'] == "limited"
+ interactive = module.params['interactive']
+ interactive_session = module.params['interactive_session']
+
+ priority = {
+ "above_normal": ProcessPriority.ABOVE_NORMAL_PRIORITY_CLASS,
+ "below_normal": ProcessPriority.BELOW_NORMAL_PRIORITY_CLASS,
+ "high": ProcessPriority.HIGH_PRIORITY_CLASS,
+ "idle": ProcessPriority.IDLE_PRIORITY_CLASS,
+ "normal": ProcessPriority.NORMAL_PRIORITY_CLASS,
+ "realtime": ProcessPriority.REALTIME_PRIORITY_CLASS
+ }[module.params['priority']]
+ show_ui_on_logon_screen = module.params['show_ui_on_logon_screen']
+
+ process_timeout = module.params['process_timeout']
+ stdin = module.params['stdin']
+
+ if (connection_username is None or connection_password is None) and \
+ not HAS_KERBEROS:
+ module.fail_json(msg=missing_required_lib("gssapi"),
+ execption=KERBEROS_IMP_ERR)
+
+ win_client = client.Client(server=hostname, username=connection_username,
+ password=connection_password, port=port,
+ encrypt=encrypt)
+
+ try:
+ win_client.connect(timeout=connection_timeout)
+ except SMBAuthenticationError as exc:
+ module.fail_json(msg='Failed to authenticate over SMB: %s'
+ % to_text(exc))
+ except SMBResponseException as exc:
+ module.fail_json(msg='Received unexpected SMB response when opening '
+ 'the connection: %s' % to_text(exc))
+ except PDUException as exc:
+ module.fail_json(msg='Received an exception with RPC PDU message: %s'
+ % to_text(exc))
+ except SCMRException as exc:
+ module.fail_json(msg='Received an exception when dealing with SCMR on '
+ 'the Windows host: %s' % to_text(exc))
+ except (SMBException, PypsexecException) as exc:
+ module.fail_json(msg=to_text(exc))
+ except socket.error as exc:
+ module.fail_json(msg=to_text(exc))
+
+ # create PAExec service and run the process
+ result['changed'] = True
+ b_stdin = to_bytes(stdin, encoding='utf-8') if stdin else None
+ run_args = dict(
+ executable=executable, arguments=arguments, asynchronous=asynchronous,
+ load_profile=load_profile, interactive=interactive,
+ interactive_session=interactive_session,
+ run_elevated=elevated, run_limited=limited,
+ username=process_username, password=process_password,
+ use_system_account=use_system, working_dir=working_directory,
+ priority=priority, show_ui_on_win_logon=show_ui_on_logon_screen,
+ timeout_seconds=process_timeout, stdin=b_stdin
+ )
+ try:
+ win_client.create_service()
+ except (SMBException, PypsexecException) as exc:
+ module.fail_json(msg='Failed to create PAExec service: %s'
+ % to_text(exc))
+
+ try:
+ proc_result = win_client.run_executable(**run_args)
+ except (SMBException, PypsexecException) as exc:
+ module.fail_json(msg='Received error when running remote process: %s'
+ % to_text(exc))
+ finally:
+ remove_artifacts(module, win_client)
+
+ if asynchronous:
+ result['pid'] = proc_result[2]
+ elif interactive:
+ result['rc'] = proc_result[2]
+ else:
+ result['stdout'] = proc_result[0]
+ result['stderr'] = proc_result[1]
+ result['rc'] = proc_result[2]
+
+ # close the SMB connection
+ try:
+ win_client.disconnect()
+ except (SMBException, PypsexecException) as exc:
+ module.warn("Failed to close the SMB connection: %s" % to_text(exc))
+
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/windows/plugins/modules/win_audit_policy_system.ps1 b/ansible_collections/community/windows/plugins/modules/win_audit_policy_system.ps1
new file mode 100644
index 00000000..07640cee
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_audit_policy_system.ps1
@@ -0,0 +1,132 @@
+#!powershell
+
+# Copyright: (c) 2017, Noah Sparks <nsparks@outlook.com>
+# 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
+
+$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
+
+$results = @{
+ changed = $false
+}
+
+######################################
+### populate sets for -validateset ###
+######################################
+$categories_rc = run-command -command 'auditpol /list /category /r'
+$subcategories_rc = run-command -command 'auditpol /list /subcategory:* /r'
+
+If ($categories_rc.item('rc') -eq 0) {
+ $categories = ConvertFrom-Csv $categories_rc.item('stdout') | Select-Object -expand Category*
+}
+Else {
+ Fail-Json -obj $results -message "Failed to retrive audit policy categories. Please make sure the auditpol command is functional on
+ the system and that the account ansible is running under is able to retrieve them. $($_.Exception.Message)"
+}
+
+If ($subcategories_rc.item('rc') -eq 0) {
+ $subcategories = ConvertFrom-Csv $subcategories_rc.item('stdout') | Select-Object -expand Category* |
+ Where-Object { $_ -notin $categories }
+}
+Else {
+ Fail-Json -obj $results -message "Failed to retrive audit policy subcategories. Please make sure the auditpol command is functional on
+ the system and that the account ansible is running under is able to retrieve them. $($_.Exception.Message)"
+}
+
+######################
+### ansible params ###
+######################
+$category = Get-AnsibleParam -obj $params -name "category" -type "str" -ValidateSet $categories
+$subcategory = Get-AnsibleParam -obj $params -name "subcategory" -type "str" -ValidateSet $subcategories
+$audit_type = Get-AnsibleParam -obj $params -name "audit_type" -type "list" -failifempty -
+
+########################
+### Start Processing ###
+########################
+Function Get-AuditPolicy ($GetString) {
+ $auditpolcsv = Run-Command -command $GetString
+ If ($auditpolcsv.item('rc') -eq 0) {
+ $Obj = ConvertFrom-CSV $auditpolcsv.item('stdout') | Select-Object @{n = 'subcategory'; e = { $_.Subcategory.ToLower() } },
+ @{ n = 'audit_type'; e = { $_."Inclusion Setting".ToLower() } }
+ }
+ Else {
+ return $auditpolcsv.item('stderr')
+ }
+
+ $HT = @{}
+ Foreach ( $Item in $Obj ) {
+ $HT.Add($Item.subcategory, $Item.audit_type)
+ }
+ $HT
+}
+
+################
+### Validate ###
+################
+
+#make sure category and subcategory are valid
+If (-Not $category -and -Not $subcategory) { Fail-Json -obj $results -message "You must provide either a Category or Subcategory parameter" }
+If ($category -and $subcategory) { Fail-Json -obj $results -message "Must pick either a specific subcategory or category. You cannot define both" }
+
+
+$possible_audit_types = 'success', 'failure', 'none'
+$audit_type | ForEach-Object {
+ If ($_ -notin $possible_audit_types) {
+ Fail-Json -obj $result -message "$_ is not a valid audit_type. Please choose from $($possible_audit_types -join ',')"
+ }
+}
+
+#############################################################
+### build lists for setting, getting, and comparing rules ###
+#############################################################
+$audit_type_string = $audit_type -join ' and '
+
+$SetString = 'auditpol /set'
+$GetString = 'auditpol /get /r'
+
+If ($category) { $SetString = "$SetString /category:`"$category`""; $GetString = "$GetString /category:`"$category`"" }
+If ($subcategory) { $SetString = "$SetString /subcategory:`"$subcategory`""; $GetString = "$GetString /subcategory:`"$subcategory`"" }
+
+
+Switch ($audit_type_string) {
+ 'success and failure' { $SetString = "$SetString /success:enable /failure:enable"; $audit_type_check = $audit_type_string }
+ 'failure' { $SetString = "$SetString /success:disable /failure:enable"; $audit_type_check = $audit_type_string }
+ 'success' { $SetString = "$SetString /success:enable /failure:disable"; $audit_type_check = $audit_type_string }
+ 'none' { $SetString = "$SetString /success:disable /failure:disable"; $audit_type_check = 'No Auditing' }
+ default { Fail-Json -obj $result -message "It seems you have specified an invalid combination of items for audit_type. Please review documentation" }
+}
+
+#########################
+### check Idempotence ###
+#########################
+
+$CurrentRule = Get-AuditPolicy $GetString
+
+#exit if the audit_type is already set properly for the category
+If (-not ($CurrentRule.Values | Where-Object { $_ -ne $audit_type_check }) ) {
+ $results.current_audit_policy = Get-AuditPolicy $GetString
+ Exit-Json -obj $results
+}
+
+####################
+### Apply Change ###
+####################
+
+If (-not $check_mode) {
+ $ApplyPolicy = Run-Command -command $SetString
+
+ If ($ApplyPolicy.Item('rc') -ne 0) {
+ $results.current_audit_policy = Get-AuditPolicy $GetString
+ Fail-Json $results "Failed to set audit policy - $($_.Exception.Message)"
+ }
+}
+
+$results.changed = $true
+$results.current_audit_policy = Get-AuditPolicy $GetString
+Exit-Json $results
diff --git a/ansible_collections/community/windows/plugins/modules/win_audit_policy_system.py b/ansible_collections/community/windows/plugins/modules/win_audit_policy_system.py
new file mode 100644
index 00000000..18a91e7b
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_audit_policy_system.py
@@ -0,0 +1,69 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2017, Noah Sparks <nsparks@outlook.com>
+# Copyright: (c) 2017, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_audit_policy_system
+short_description: Used to make changes to the system wide Audit Policy
+description:
+ - Used to make changes to the system wide Audit Policy.
+options:
+ category:
+ description:
+ - Single string value for the category you would like to adjust the policy on.
+ - Cannot be used with I(subcategory). You must define one or the other.
+ - Changing this setting causes all subcategories to be adjusted to the defined I(audit_type).
+ type: str
+ subcategory:
+ description:
+ - Single string value for the subcategory you would like to adjust the policy on.
+ - Cannot be used with I(category). You must define one or the other.
+ type: str
+ audit_type:
+ description:
+ - The type of event you would like to audit for.
+ - Accepts a list. See examples.
+ type: list
+ elements: str
+ required: yes
+ choices: [ failure, none, success ]
+notes:
+ - It is recommended to take a backup of the policies before adjusting them for the first time.
+ - See this page for in depth information U(https://technet.microsoft.com/en-us/library/cc766468.aspx).
+seealso:
+- module: community.windows.win_audit_rule
+author:
+ - Noah Sparks (@nwsparks)
+'''
+
+EXAMPLES = r'''
+- name: Enable failure auditing for the subcategory "File System"
+ community.windows.win_audit_policy_system:
+ subcategory: File System
+ audit_type: failure
+
+- name: Enable all auditing types for the category "Account logon events"
+ community.windows.win_audit_policy_system:
+ category: Account logon events
+ audit_type: success, failure
+
+- name: Disable auditing for the subcategory "File System"
+ community.windows.win_audit_policy_system:
+ subcategory: File System
+ audit_type: none
+'''
+
+RETURN = r'''
+current_audit_policy:
+ description: details on the policy being targetted
+ returned: always
+ type: dict
+ sample: |-
+ {
+ "File Share":"failure"
+ }
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_audit_rule.ps1 b/ansible_collections/community/windows/plugins/modules/win_audit_rule.ps1
new file mode 100644
index 00000000..c36c8f9c
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_audit_rule.ps1
@@ -0,0 +1,174 @@
+#!powershell
+
+# Copyright: (c) 2017, Noah Sparks <nsparks@outlook.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.SID
+
+$params = Parse-Args -arguments $args -supports_check_mode $true
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
+
+# module parameters
+$path = Get-AnsibleParam -obj $params -name "path" -type "path" -failifempty $true -aliases "destination", "dest"
+$user = Get-AnsibleParam -obj $params -name "user" -type "str" -failifempty $true
+$rights = Get-AnsibleParam -obj $params -name "rights" -type "list"
+$inheritance_flags = Get-AnsibleParam -obj $params -name "inheritance_flags" -type "list" -default 'ContainerInherit', 'ObjectInherit'
+$prop_options = 'InheritOnly', 'None', 'NoPropagateInherit'
+$propagation_flags = Get-AnsibleParam -obj $params -name "propagation_flags" -type "str" -default "none" -ValidateSet $prop_options
+$audit_flags = Get-AnsibleParam -obj $params -name "audit_flags" -type "list" -default 'success'
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset 'present', 'absent'
+
+#Make sure target path is valid
+If (-not (Test-Path -Path $path) ) {
+ Fail-Json -obj $result -message "defined path ($path) is not found/invalid"
+}
+
+#function get current audit rules and convert to hashtable
+Function Get-CurrentAuditRule ($path) {
+ Try {
+ $ACL = Get-Acl $path -Audit
+ }
+ Catch {
+ Return "Unable to retrieve the ACL on $Path"
+ }
+
+ $HT = Foreach ($Obj in $ACL.Audit) {
+ @{
+ user = $Obj.IdentityReference.ToString()
+ rights = ($Obj | Select-Object -expand "*rights").ToString()
+ audit_flags = $Obj.AuditFlags.ToString()
+ is_inherited = $Obj.IsInherited.ToString()
+ inheritance_flags = $Obj.InheritanceFlags.ToString()
+ propagation_flags = $Obj.PropagationFlags.ToString()
+ }
+ }
+
+ If (-Not $HT) {
+ "No audit rules defined on $path"
+ }
+ Else { $HT }
+}
+
+$result = @{
+ changed = $false
+ current_audit_rules = Get-CurrentAuditRule $path
+}
+
+#Make sure identity is valid and can be looked up
+Try {
+ $SID = Convert-ToSid $user
+}
+Catch {
+ Fail-Json -obj $result -message "Failed to lookup the identity ($user) - $($_.exception.message)"
+}
+
+#get the path type
+$ItemType = (Get-Item $path -Force).GetType()
+switch ($ItemType) {
+ ([Microsoft.Win32.RegistryKey]) { $registry = $true; $result.path_type = 'registry' }
+ ([System.IO.FileInfo]) { $file = $true; $result.path_type = 'file' }
+ ([System.IO.DirectoryInfo]) { $result.path_type = 'directory' }
+}
+
+#Get current acl/audit rules on the target
+Try {
+ $ACL = Get-Acl $path -Audit
+}
+Catch {
+ Fail-Json -obj $result -message "Unable to retrieve the ACL on $Path - $($_.Exception.Message)"
+}
+
+#configure acl object to remove the specified user
+If ($state -eq 'absent') {
+ #Try and find an identity on the object that matches user
+ #We skip inherited items since we can't remove those
+ $ToRemove = ($ACL.Audit | Where-Object { $_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -eq $SID -and
+ $_.IsInherited -eq $false }).IdentityReference
+
+ #Exit with changed false if no identity is found
+ If (-Not $ToRemove) {
+ $result.current_audit_rules = Get-CurrentAuditRule $path
+ Exit-Json -obj $result
+ }
+
+ #update the ACL object if identity found
+ Try {
+ $ToRemove | ForEach-Object { $ACL.PurgeAuditRules($_) }
+ }
+ Catch {
+ $result.current_audit_rules = Get-CurrentAuditRule $path
+ Fail-Json -obj $result -message "Failed to remove audit rule: $($_.Exception.Message)"
+ }
+}
+
+Else {
+ If ($registry) {
+ $PossibleRights = [System.Enum]::GetNames([System.Security.AccessControl.RegistryRights])
+
+ Foreach ($right in $rights) {
+ if ($right -notin $PossibleRights) {
+ Fail-Json -obj $result -message "$right does not seem to be a valid REGISTRY right"
+ }
+ }
+
+ $NewAccessRule = New-Object System.Security.AccessControl.RegistryAuditRule($user, $rights, $inheritance_flags, $propagation_flags, $audit_flags)
+ }
+ Else {
+ $PossibleRights = [System.Enum]::GetNames([System.Security.AccessControl.FileSystemRights])
+
+ Foreach ($right in $rights) {
+ if ($right -notin $PossibleRights) {
+ Fail-Json -obj $result -message "$right does not seem to be a valid FILE SYSTEM right"
+ }
+ }
+
+ If ($file -and $inheritance_flags -ne 'none') {
+ Fail-Json -obj $result -message "The target type is a file. inheritance_flags must be changed to 'none'"
+ }
+
+ $NewAccessRule = New-Object System.Security.AccessControl.FileSystemAuditRule($user, $rights, $inheritance_flags, $propagation_flags, $audit_flags)
+ }
+
+ #exit here if any existing rule matches defined rule since no change is needed
+ #if we need to ignore inherited rules in the future, this would be where to do it
+ #Just filter out inherited rules from $ACL.Audit
+ Foreach ($group in $ACL.Audit | Where-Object { $_.IsInherited -eq $false }) {
+ If (
+ ($group | Select-Object -expand "*Rights") -eq ($NewAccessRule | Select-Object -expand "*Rights") -and
+ $group.AuditFlags -eq $NewAccessRule.AuditFlags -and
+ $group.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -eq $SID -and
+ $group.InheritanceFlags -eq $NewAccessRule.InheritanceFlags -and
+ $group.PropagationFlags -eq $NewAccessRule.PropagationFlags
+ ) {
+ $result.current_audit_rules = Get-CurrentAuditRule $path
+ Exit-Json -obj $result
+ }
+ }
+
+ #try and set the acl object. AddAuditRule allows for multiple entries to exist under the same
+ #identity...so if someone wanted success: write and failure: delete for example, that setup would be
+ #possible. The alternative is SetAuditRule which would instead modify an existing rule and not allow
+ #for setting the above example.
+ Try {
+ $ACL.AddAuditRule($NewAccessRule)
+ }
+ Catch {
+ Fail-Json -obj $result -message "Failed to set the audit rule: $($_.Exception.Message)"
+ }
+}
+
+
+#finally set the permissions
+Try {
+ Set-Acl -Path $path -ACLObject $ACL -WhatIf:$check_mode
+}
+Catch {
+ $result.current_audit_rules = Get-CurrentAuditRule $path
+ Fail-Json -obj $result -message "Failed to apply audit change: $($_.Exception.Message)"
+}
+
+#exit here after a change is applied
+$result.current_audit_rules = Get-CurrentAuditRule $path
+$result.changed = $true
+Exit-Json -obj $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_audit_rule.py b/ansible_collections/community/windows/plugins/modules/win_audit_rule.py
new file mode 100644
index 00000000..fa9bd18a
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_audit_rule.py
@@ -0,0 +1,140 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2017, Noah Sparks <nsparks@outlook.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_audit_rule
+short_description: Adds an audit rule to files, folders, or registry keys
+description:
+ - Used to apply audit rules to files, folders or registry keys.
+ - Once applied, it will begin recording the user who performed the operation defined into the Security
+ Log in the Event viewer.
+ - The behavior is designed to ignore inherited rules since those cannot be adjusted without first disabling
+ the inheritance behavior. It will still print inherited rules in the output though for debugging purposes.
+options:
+ path:
+ description:
+ - Path to the file, folder, or registry key.
+ - Registry paths should be in Powershell format, beginning with an abbreviation for the root
+ such as, C(HKLM:\Software).
+ type: path
+ required: yes
+ aliases: [ dest, destination ]
+ user:
+ description:
+ - The user or group to adjust rules for.
+ type: str
+ required: yes
+ rights:
+ description:
+ - Comma separated list of the rights desired. Only required for adding a rule.
+ - If I(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 I(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: list
+ elements: str
+ required: yes
+ inheritance_flags:
+ description:
+ - Defines what objects inside of a folder or registry key will inherit the settings.
+ - If you are setting a rule on a file, this value has to be changed to C(none).
+ - For more information on the choices see MSDN PropagationFlags enumeration
+ at U(https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.inheritanceflags.aspx).
+ type: list
+ elements: str
+ choices: [ ContainerInherit, ObjectInherit ]
+ default: ContainerInherit,ObjectInherit
+ propagation_flags:
+ description:
+ - Propagation flag on the audit rules.
+ - This value is ignored when the path type is a file.
+ - For more information on the choices see MSDN PropagationFlags enumeration
+ at U(https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.propagationflags.aspx).
+ choices: [ None, InherityOnly, NoPropagateInherit ]
+ default: "None"
+ audit_flags:
+ description:
+ - Defines whether to log on failure, success, or both.
+ - To log both define as comma separated list "Success, Failure".
+ type: list
+ elements: str
+ required: yes
+ choices: [ Failure, Success ]
+ state:
+ description:
+ - Whether the rule should be C(present) or C(absent).
+ - For absent, only I(path), I(user), and I(state) are required.
+ - Specifying C(absent) will remove all rules matching the defined I(user).
+ type: str
+ choices: [ absent, present ]
+ default: present
+seealso:
+- module: community.windows.win_audit_policy_system
+author:
+ - Noah Sparks (@nwsparks)
+'''
+
+EXAMPLES = r'''
+- name: Add filesystem audit rule for a folder
+ community.windows.win_audit_rule:
+ path: C:\inetpub\wwwroot\website
+ user: BUILTIN\Users
+ rights: write,delete,changepermissions
+ audit_flags: success,failure
+ inheritance_flags: ContainerInherit,ObjectInherit
+
+- name: Add filesystem audit rule for a file
+ community.windows.win_audit_rule:
+ path: C:\inetpub\wwwroot\website\web.config
+ user: BUILTIN\Users
+ rights: write,delete,changepermissions
+ audit_flags: success,failure
+ inheritance_flags: None
+
+- name: Add registry audit rule
+ community.windows.win_audit_rule:
+ path: HKLM:\software
+ user: BUILTIN\Users
+ rights: delete
+ audit_flags: 'success'
+
+- name: Remove filesystem audit rule
+ community.windows.win_audit_rule:
+ path: C:\inetpub\wwwroot\website
+ user: BUILTIN\Users
+ state: absent
+
+- name: Remove registry audit rule
+ community.windows.win_audit_rule:
+ path: HKLM:\software
+ user: BUILTIN\Users
+ state: absent
+'''
+
+RETURN = r'''
+current_audit_rules:
+ description:
+ - The current rules on the defined I(path)
+ - Will return "No audit rules defined on I(path)"
+ returned: always
+ type: dict
+ sample: |
+ {
+ "audit_flags": "Success",
+ "user": "Everyone",
+ "inheritance_flags": "False",
+ "is_inherited": "False",
+ "propagation_flags": "None",
+ "rights": "Delete"
+ }
+path_type:
+ description:
+ - The type of I(path) being targetted.
+ - Will be one of file, directory, registry.
+ returned: always
+ type: str
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_auto_logon.ps1 b/ansible_collections/community/windows/plugins/modules/win_auto_logon.ps1
new file mode 100644
index 00000000..66a20dd9
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_auto_logon.ps1
@@ -0,0 +1,404 @@
+#!powershell
+
+# Copyright: (c) 2019, Prasoon Karunan V (@prasoonkarunan) <kvprasoon@Live.in>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# All helper methods are written in a binary module and has to be loaded for consuming them.
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#Requires -Module Ansible.ModuleUtils.AddType
+
+Set-StrictMode -Version 2.0
+
+$spec = @{
+ options = @{
+ logon_count = @{ type = "int" }
+ password = @{ type = "str"; no_log = $true }
+ state = @{ type = "str"; choices = "absent", "present"; default = "present" }
+ username = @{ type = "str" }
+ }
+ required_if = @(
+ , @("state", "present", @("username", "password"))
+ )
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$logonCount = $module.Params.logon_count
+$password = $module.Params.password
+$state = $module.Params.state
+$username = $module.Params.username
+$domain = $null
+
+if ($username) {
+ # Try and get the Netlogon form of the username specified. Translating to and from a SID gives us an NTAccount
+ # in the Netlogon form that we desire.
+ $ntAccount = New-Object -TypeName System.Security.Principal.NTAccount -ArgumentList $username
+ try {
+ $accountSid = $ntAccount.Translate([System.Security.Principal.SecurityIdentifier])
+ }
+ catch [System.Security.Principal.IdentityNotMappedException] {
+ $module.FailJson("Failed to find a local or domain user with the name '$username'", $_)
+ }
+ $ntAccount = $accountSid.Translate([System.Security.Principal.NTAccount])
+
+ $domain, $username = $ntAccount.Value -split '\\'
+}
+
+# Make sure $null regardless of any input value if state: absent
+if ($state -eq 'absent') {
+ $password = $null
+}
+
+Add-CSharpType -AnsibleModule $module -References @'
+using Microsoft.Win32.SafeHandles;
+using System;
+using System.Runtime.ConstrainedExecution;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Ansible.WinAutoLogon
+{
+ internal class NativeHelpers
+ {
+ [StructLayout(LayoutKind.Sequential)]
+ public class LSA_OBJECT_ATTRIBUTES
+ {
+ public UInt32 Length = 0;
+ public IntPtr RootDirectory = IntPtr.Zero;
+ public IntPtr ObjectName = IntPtr.Zero;
+ public UInt32 Attributes = 0;
+ public IntPtr SecurityDescriptor = IntPtr.Zero;
+ public IntPtr SecurityQualityOfService = IntPtr.Zero;
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ internal struct LSA_UNICODE_STRING
+ {
+ public UInt16 Length;
+ public UInt16 MaximumLength;
+ public IntPtr Buffer;
+
+ public static explicit operator string(LSA_UNICODE_STRING s)
+ {
+ byte[] strBytes = new byte[s.Length];
+ Marshal.Copy(s.Buffer, strBytes, 0, s.Length);
+ return Encoding.Unicode.GetString(strBytes);
+ }
+
+ public static SafeMemoryBuffer CreateSafeBuffer(string s)
+ {
+ if (s == null)
+ return new SafeMemoryBuffer(IntPtr.Zero);
+
+ byte[] stringBytes = Encoding.Unicode.GetBytes(s);
+ int structSize = Marshal.SizeOf(typeof(LSA_UNICODE_STRING));
+ IntPtr buffer = Marshal.AllocHGlobal(structSize + stringBytes.Length);
+ try
+ {
+ LSA_UNICODE_STRING lsaString = new LSA_UNICODE_STRING()
+ {
+ Length = (UInt16)(stringBytes.Length),
+ MaximumLength = (UInt16)(stringBytes.Length),
+ Buffer = IntPtr.Add(buffer, structSize),
+ };
+ Marshal.StructureToPtr(lsaString, buffer, false);
+ Marshal.Copy(stringBytes, 0, lsaString.Buffer, stringBytes.Length);
+ return new SafeMemoryBuffer(buffer);
+ }
+ catch
+ {
+ // Make sure we free the pointer before raising the exception.
+ Marshal.FreeHGlobal(buffer);
+ throw;
+ }
+ }
+ }
+ }
+
+ internal class NativeMethods
+ {
+ [DllImport("Advapi32.dll")]
+ public static extern UInt32 LsaClose(
+ IntPtr ObjectHandle);
+
+ [DllImport("Advapi32.dll")]
+ public static extern UInt32 LsaFreeMemory(
+ IntPtr Buffer);
+
+ [DllImport("Advapi32.dll")]
+ internal static extern Int32 LsaNtStatusToWinError(
+ UInt32 Status);
+
+ [DllImport("Advapi32.dll")]
+ public static extern UInt32 LsaOpenPolicy(
+ IntPtr SystemName,
+ NativeHelpers.LSA_OBJECT_ATTRIBUTES ObjectAttributes,
+ LsaPolicyAccessMask AccessMask,
+ out SafeLsaHandle PolicyHandle);
+
+ [DllImport("Advapi32.dll")]
+ public static extern UInt32 LsaRetrievePrivateData(
+ SafeLsaHandle PolicyHandle,
+ SafeMemoryBuffer KeyName,
+ out SafeLsaMemory PrivateData);
+
+ [DllImport("Advapi32.dll")]
+ public static extern UInt32 LsaStorePrivateData(
+ SafeLsaHandle PolicyHandle,
+ SafeMemoryBuffer KeyName,
+ SafeMemoryBuffer PrivateData);
+ }
+
+ internal class SafeLsaMemory : SafeBuffer
+ {
+ internal SafeLsaMemory() : base(true) { }
+
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
+
+ protected override bool ReleaseHandle()
+ {
+ return NativeMethods.LsaFreeMemory(handle) == 0;
+ }
+ }
+
+ internal class SafeMemoryBuffer : SafeBuffer
+ {
+ internal SafeMemoryBuffer() : base(true) { }
+
+ internal SafeMemoryBuffer(IntPtr ptr) : base(true)
+ {
+ base.SetHandle(ptr);
+ }
+
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
+
+ protected override bool ReleaseHandle()
+ {
+ if (handle != IntPtr.Zero)
+ Marshal.FreeHGlobal(handle);
+ return true;
+ }
+ }
+
+ public class SafeLsaHandle : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ internal SafeLsaHandle() : base(true) { }
+
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
+
+ protected override bool ReleaseHandle()
+ {
+ return NativeMethods.LsaClose(handle) == 0;
+ }
+ }
+
+ public class Win32Exception : System.ComponentModel.Win32Exception
+ {
+ private string _exception_msg;
+ public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
+ public Win32Exception(int errorCode, string message) : base(errorCode)
+ {
+ _exception_msg = String.Format("{0} - {1} (Win32 Error Code {2}: 0x{3})", message, base.Message, errorCode, errorCode.ToString("X8"));
+ }
+ public override string Message { get { return _exception_msg; } }
+ public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
+ }
+
+ [Flags]
+ public enum LsaPolicyAccessMask : uint
+ {
+ ViewLocalInformation = 0x00000001,
+ ViewAuditInformation = 0x00000002,
+ GetPrivateInformation = 0x00000004,
+ TrustAdmin = 0x00000008,
+ CreateAccount = 0x00000010,
+ CreateSecret = 0x00000020,
+ CreatePrivilege = 0x00000040,
+ SetDefaultQuotaLimits = 0x00000080,
+ SetAuditRequirements = 0x00000100,
+ AuditLogAdmin = 0x00000200,
+ ServerAdmin = 0x00000400,
+ LookupNames = 0x00000800,
+ Read = 0x00020006,
+ Write = 0x000207F8,
+ Execute = 0x00020801,
+ AllAccess = 0x000F0FFF,
+ }
+
+ public class LsaUtil
+ {
+ public static SafeLsaHandle OpenPolicy(LsaPolicyAccessMask access)
+ {
+ NativeHelpers.LSA_OBJECT_ATTRIBUTES oa = new NativeHelpers.LSA_OBJECT_ATTRIBUTES();
+ SafeLsaHandle lsaHandle;
+ UInt32 res = NativeMethods.LsaOpenPolicy(IntPtr.Zero, oa, access, out lsaHandle);
+ if (res != 0)
+ throw new Win32Exception(NativeMethods.LsaNtStatusToWinError(res),
+ String.Format("LsaOpenPolicy({0}) failed", access.ToString()));
+ return lsaHandle;
+ }
+
+ public static string RetrievePrivateData(SafeLsaHandle handle, string key)
+ {
+ using (SafeMemoryBuffer keyBuffer = NativeHelpers.LSA_UNICODE_STRING.CreateSafeBuffer(key))
+ {
+ SafeLsaMemory buffer;
+ UInt32 res = NativeMethods.LsaRetrievePrivateData(handle, keyBuffer, out buffer);
+ using (buffer)
+ {
+ if (res != 0)
+ {
+ // If the data object was not found we return null to indicate it isn't set.
+ if (res == 0xC0000034) // STATUS_OBJECT_NAME_NOT_FOUND
+ return null;
+
+ throw new Win32Exception(NativeMethods.LsaNtStatusToWinError(res),
+ String.Format("LsaRetrievePrivateData({0}) failed", key));
+ }
+
+ NativeHelpers.LSA_UNICODE_STRING lsaString = (NativeHelpers.LSA_UNICODE_STRING)
+ Marshal.PtrToStructure(buffer.DangerousGetHandle(),
+ typeof(NativeHelpers.LSA_UNICODE_STRING));
+ return (string)lsaString;
+ }
+ }
+ }
+
+ public static void StorePrivateData(SafeLsaHandle handle, string key, string data)
+ {
+ using (SafeMemoryBuffer keyBuffer = NativeHelpers.LSA_UNICODE_STRING.CreateSafeBuffer(key))
+ using (SafeMemoryBuffer dataBuffer = NativeHelpers.LSA_UNICODE_STRING.CreateSafeBuffer(data))
+ {
+ UInt32 res = NativeMethods.LsaStorePrivateData(handle, keyBuffer, dataBuffer);
+ if (res != 0)
+ {
+ // When clearing the private data with null it may return this error which we can ignore.
+ if (data == null && res == 0xC0000034) // STATUS_OBJECT_NAME_NOT_FOUND
+ return;
+
+ throw new Win32Exception(NativeMethods.LsaNtStatusToWinError(res),
+ String.Format("LsaStorePrivateData({0}) failed", key));
+ }
+ }
+ }
+ }
+}
+'@
+
+$autoLogonRegPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'
+$logonDetails = Get-ItemProperty -LiteralPath $autoLogonRegPath
+
+$before = @{
+ state = 'absent'
+}
+if ('AutoAdminLogon' -in $logonDetails.PSObject.Properties.Name -and $logonDetails.AutoAdminLogon -eq 1) {
+ $before.state = 'present'
+}
+
+$mapping = @{
+ DefaultUserName = 'username'
+ DefaultDomainName = 'domain'
+ AutoLogonCount = 'logon_count'
+}
+foreach ($map_detail in $mapping.GetEnumerator()) {
+ if ($map_detail.Key -in $logonDetails.PSObject.Properties.Name) {
+ $before."$($map_detail.Value)" = $logonDetails."$($map_detail.Key)"
+ }
+}
+
+$module.Diff.before = $before
+
+$propParams = @{
+ LiteralPath = $autoLogonRegPath
+ WhatIf = $module.CheckMode
+ Force = $true
+}
+
+# First set the registry information
+# The DefaultPassword reg key should never be set, we use LSA to store the password in a more secure way.
+if ('DefaultPassword' -in (Get-Item -LiteralPath $autoLogonRegPath).Property) {
+ # Bug on older Windows hosts where -WhatIf causes it fail to find the property
+ if (-not $module.CheckMode) {
+ Remove-ItemProperty -Name 'DefaultPassword' @propParams
+ }
+ $module.Result.changed = $true
+}
+
+$autoLogonKeyList = @{
+ DefaultUserName = @{
+ before = if ($before.ContainsKey('username')) { $before.username } else { $null }
+ after = $username
+ }
+ DefaultDomainName = @{
+ before = if ($before.ContainsKey('domain')) { $before.domain } else { $null }
+ after = $domain
+ }
+ AutoLogonCount = @{
+ before = if ($before.ContainsKey('logon_count')) { $before.logon_count } else { $null }
+ after = $logonCount
+ }
+}
+
+# Check AutoAdminLogon separately as it has different logic (key must exist)
+if ($state -ne $before.state) {
+ $newValue = if ($state -eq 'present') { 1 } else { 0 }
+ $null = New-ItemProperty -Name 'AutoAdminLogon' -Value $newValue -PropertyType DWord @propParams
+ $module.Result.changed = $true
+}
+
+foreach ($key in $autoLogonKeyList.GetEnumerator()) {
+ $beforeVal = $key.Value.before
+ $after = $key.Value.after
+
+ if ($state -eq 'present' -and $beforeVal -cne $after) {
+ if ($null -ne $after) {
+ $null = New-ItemProperty -Name $key.Key -Value $after @propParams
+ }
+ elseif (-not $module.CheckMode) {
+ Remove-ItemProperty -Name $key.Key @propParams
+ }
+ $module.Result.changed = $true
+ }
+ elseif ($state -eq 'absent' -and $null -ne $beforeVal) {
+ if (-not $module.CheckMode) {
+ Remove-ItemProperty -Name $key.Key @propParams
+ }
+ $module.Result.changed = $true
+ }
+}
+
+# Finally update the password in the LSA private store.
+$lsaHandle = [Ansible.WinAutoLogon.LsaUtil]::OpenPolicy('CreateSecret, GetPrivateInformation')
+try {
+ $beforePass = [Ansible.WinAutoLogon.LsaUtil]::RetrievePrivateData($lsaHandle, 'DefaultPassword')
+
+ if ($beforePass -cne $password) {
+ # Due to .NET marshaling we need to pass in $null as NullString.Value so it's truly a null value.
+ if ($null -eq $password) {
+ $password = [NullString]::Value
+ }
+ if (-not $module.CheckMode) {
+ [Ansible.WinAutoLogon.LsaUtil]::StorePrivateData($lsaHandle, 'DefaultPassword', $password)
+ }
+ $module.Result.changed = $true
+ }
+}
+finally {
+ $lsaHandle.Dispose()
+}
+
+# Need to manually craft the after diff in case we are running in check mode
+$module.Diff.after = @{
+ state = $state
+}
+if ($state -eq 'present') {
+ $module.Diff.after.username = $username
+ $module.Diff.after.domain = $domain
+ if ($null -ne $logonCount) {
+ $module.Diff.after.logon_count = $logonCount
+ }
+}
+
+$module.ExitJson()
+
diff --git a/ansible_collections/community/windows/plugins/modules/win_auto_logon.py b/ansible_collections/community/windows/plugins/modules/win_auto_logon.py
new file mode 100644
index 00000000..127725fb
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_auto_logon.py
@@ -0,0 +1,72 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2019, Prasoon Karunan V (@prasoonkarunan)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_auto_logon
+short_description: Adds or Sets auto logon registry keys.
+description:
+ - Used to apply auto logon registry setting.
+options:
+ logon_count:
+ description:
+ - The number of times to do an automatic logon.
+ - This count is deremented by Windows everytime an automatic logon is
+ performed.
+ - Once the count reaches C(0) then the automatic logon process is
+ disabled.
+ type: int
+ username:
+ description:
+ - Username to login automatically.
+ - Must be set when C(state=present).
+ - This can be the Netlogon or UPN of a domain account and is
+ automatically parsed to the C(DefaultUserName) and C(DefaultDomainName)
+ registry properties.
+ type: str
+ password:
+ description:
+ - Password to be used for automatic login.
+ - Must be set when C(state=present).
+ - Value of this input will be used as password for I(username).
+ - While this value is encrypted by LSA it is decryptable to any user who
+ is an Administrator on the remote host.
+ type: str
+ state:
+ description:
+ - Whether the registry key should be C(present) or C(absent).
+ type: str
+ choices: [ absent, present ]
+ default: present
+author:
+ - Prasoon Karunan V (@prasoonkarunan)
+'''
+
+EXAMPLES = r'''
+- name: Set autologon for user1
+ community.windows.win_auto_logon:
+ username: User1
+ password: str0ngp@ssword
+
+- name: Set autologon for abc.com\user1
+ community.windows.win_auto_logon:
+ username: abc.com\User1
+ password: str0ngp@ssword
+
+- name: Remove autologon for user1
+ community.windows.win_auto_logon:
+ state: absent
+
+- name: Set autologon for user1 with a limited logon count
+ community.windows.win_auto_logon:
+ username: User1
+ password: str0ngp@ssword
+ logon_count: 5
+'''
+
+RETURN = r'''
+#
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_certificate_info.ps1 b/ansible_collections/community/windows/plugins/modules/win_certificate_info.ps1
new file mode 100644
index 00000000..004cd3e3
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_certificate_info.ps1
@@ -0,0 +1,115 @@
+#!powershell
+
+# Copyright: (c) 2019, Micah Hunsberger
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+function ConvertTo-Timestamp($start_date, $end_date) {
+ if ($start_date -and $end_date) {
+ return (New-TimeSpan -Start $start_date -End $end_date).TotalSeconds
+ }
+}
+
+function Format-Date([DateTime]$date) {
+ return $date.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssK')
+}
+
+function Get-CertificateInfo ($cert) {
+ $epoch_date = Get-Date -Date "01/01/1970"
+
+ $cert_info = @{ extensions = @() }
+ $cert_info.friendly_name = $cert.FriendlyName
+ $cert_info.thumbprint = $cert.Thumbprint
+ $cert_info.subject = $cert.Subject
+ $cert_info.issuer = $cert.Issuer
+ $cert_info.valid_from = (ConvertTo-Timestamp -start_date $epoch_date -end_date $cert.NotBefore.ToUniversalTime())
+ $cert_info.valid_from_iso8601 = Format-Date -date $cert.NotBefore
+ $cert_info.valid_to = (ConvertTo-Timestamp -start_date $epoch_date -end_date $cert.NotAfter.ToUniversalTime())
+ $cert_info.valid_to_iso8601 = Format-Date -date $cert.NotAfter
+ $cert_info.serial_number = $cert.SerialNumber
+ $cert_info.archived = $cert.Archived
+ $cert_info.version = $cert.Version
+ $cert_info.has_private_key = $cert.HasPrivateKey
+ $cert_info.issued_by = $cert.GetNameInfo('SimpleName', $true)
+ $cert_info.issued_to = $cert.GetNameInfo('SimpleName', $false)
+ $cert_info.signature_algorithm = $cert.SignatureAlgorithm.FriendlyName
+ $cert_info.dns_names = [System.Collections.Generic.List`1[String]]@($cert_info.issued_to)
+ $cert_info.raw = [System.Convert]::ToBase64String($cert.GetRawCertData())
+ $cert_info.public_key = [System.Convert]::ToBase64String($cert.GetPublicKey())
+ if ($cert.Extensions.Count -gt 0) {
+ [array]$cert_info.extensions = foreach ($extension in $cert.Extensions) {
+ $extension_info = @{
+ critical = $extension.Critical
+ field = $extension.Oid.FriendlyName
+ value = $extension.Format($false)
+ }
+ if ($extension -is [System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension]) {
+ $cert_info.is_ca = $extension.CertificateAuthority
+ $cert_info.path_length_constraint = $extension.PathLengthConstraint
+ }
+ elseif ($extension -is [System.Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension]) {
+ $cert_info.intended_purposes = $extension.EnhancedKeyUsages.FriendlyName -as [string[]]
+ }
+ elseif ($extension -is [System.Security.Cryptography.X509Certificates.X509KeyUsageExtension]) {
+ $cert_info.key_usages = $extension.KeyUsages.ToString().Split(',').Trim() -as [string[]]
+ }
+ elseif ($extension -is [System.Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierExtension]) {
+ $cert_info.ski = $extension.SubjectKeyIdentifier
+ }
+ elseif ($extension.Oid.value -eq '2.5.29.17') {
+ $sans = $extension.Format($true).Split("`r`n", [System.StringSplitOptions]::RemoveEmptyEntries)
+ foreach ($san in $sans) {
+ $san_parts = $san.Split("=")
+ if ($san_parts.Length -ge 2 -and $san_parts[0].Trim() -eq 'DNS Name') {
+ $cert_info.dns_names.Add($san_parts[1].Trim())
+ }
+ }
+ }
+ $extension_info
+ }
+ }
+ return $cert_info
+}
+
+$store_location_values = ([System.Security.Cryptography.X509Certificates.StoreLocation]).GetEnumValues() | ForEach-Object { $_.ToString() }
+
+$spec = @{
+ options = @{
+ thumbprint = @{ type = "str"; required = $false }
+ store_name = @{ type = "str"; default = "My"; }
+ store_location = @{ type = "str"; default = "LocalMachine"; choices = $store_location_values; }
+ }
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$thumbprint = $module.Params.thumbprint
+$store_name = $module.Params.store_name
+$store_location = [System.Security.Cryptography.X509Certificates.Storelocation]"$($module.Params.store_location)"
+
+$store = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $store_name, $store_location
+$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly)
+
+$module.Result.exists = $false
+$module.Result.certificates = @()
+
+try {
+ if ($null -ne $thumbprint) {
+ $found_certs = $store.Certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $thumbprint, $false)
+ }
+ else {
+ $found_certs = $store.Certificates
+ }
+
+ if ($found_certs.Count -gt 0) {
+ $module.Result.exists = $true
+ [array]$module.Result.certificates = $found_certs | ForEach-Object { Get-CertificateInfo -cert $_ } | Sort-Object -Property { $_.thumbprint }
+ }
+}
+finally {
+ $store.Close()
+}
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_certificate_info.py b/ansible_collections/community/windows/plugins/modules/win_certificate_info.py
new file mode 100644
index 00000000..494a6ef8
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_certificate_info.py
@@ -0,0 +1,228 @@
+#!/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)
+
+DOCUMENTATION = r'''
+---
+module: win_certificate_info
+short_description: Get information on certificates from a Windows Certificate Store
+description:
+- Returns information about certificates in a Windows Certificate Store.
+options:
+ thumbprint:
+ description:
+ - The thumbprint as a hex string of a certificate to find.
+ - When specified, filters the I(certificates) return value to a single certificate
+ - See the examples for how to format the thumbprint.
+ type: str
+ required: no
+ store_name:
+ description:
+ - The name of the store to search.
+ - See U(https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.x509certificates.storename)
+ for a list of built-in store names.
+ type: str
+ default: My
+ store_location:
+ description:
+ - The location of the store to search.
+ type: str
+ choices: [ CurrentUser, LocalMachine ]
+ default: LocalMachine
+seealso:
+- module: ansible.windows.win_certificate_store
+author:
+- Micah Hunsberger (@mhunsber)
+'''
+
+EXAMPLES = r'''
+- name: Obtain information about a particular certificate in the computer's personal store
+ community.windows.win_certificate_info:
+ thumbprint: BD7AF104CF1872BDB518D95C9534EA941665FD27
+ register: mycert
+
+# thumbprint can also be lower case
+- name: Obtain information about a particular certificate in the computer's personal store
+ community.windows.win_certificate_info:
+ thumbprint: bd7af104cf1872bdb518d95c9534ea941665fd27
+ register: mycert
+
+- name: Obtain information about all certificates in the root store
+ community.windows.win_certificate_info:
+ store_name: Root
+ register: ca
+
+# Import a pfx and then get information on the certificates
+- name: Import pfx certificate that is password protected
+ ansible.windows.win_certificate_store:
+ path: C:\Temp\cert.pfx
+ state: present
+ password: VeryStrongPasswordHere!
+ become: yes
+ become_method: runas
+ register: mycert
+
+- name: Obtain information on each certificate that was touched
+ community.windows.win_certificate_info:
+ thumbprint: "{{ item }}"
+ register: mycert_stats
+ loop: "{{ mycert.thumbprints }}"
+'''
+
+RETURN = r'''
+exists:
+ description:
+ - Whether any certificates were found in the store.
+ - When I(thumbprint) is specified, returns true only if the certificate mathing the thumbprint exists.
+ returned: success
+ type: bool
+ sample: true
+certificates:
+ description:
+ - A list of information about certificates found in the store, sorted by thumbprint.
+ returned: success
+ type: list
+ elements: dict
+ contains:
+ archived:
+ description: Indicates that the certificate is archived.
+ type: bool
+ sample: false
+ dns_names:
+ description: Lists the registered dns names for the certificate.
+ type: list
+ elements: str
+ sample: [ '*.m.wikiquote.org', '*.wikipedia.org' ]
+ extensions:
+ description: The collection of the certificates extensions.
+ type: list
+ elements: dict
+ sample: [
+ {
+ "critical": false,
+ "field": "Subject Key Identifier",
+ "value": "88 27 17 09 a9 b6 18 60 8b ec eb ba f6 47 59 c5 52 54 a3 b7"
+ },
+ {
+ "critical": true,
+ "field": "Basic Constraints",
+ "value": "Subject Type=CA, Path Length Constraint=None"
+ },
+ {
+ "critical": false,
+ "field": "Authority Key Identifier",
+ "value": "KeyID=2b d0 69 47 94 76 09 fe f4 6b 8d 2e 40 a6 f7 47 4d 7f 08 5e"
+ },
+ {
+ "critical": false,
+ "field": "CRL Distribution Points",
+ "value": "[1]CRL Distribution Point: Distribution Point Name:Full Name:URL=http://crl.apple.com/root.crl"
+ },
+ {
+ "critical": true,
+ "field": "Key Usage",
+ "value": "Digital Signature, Certificate Signing, Off-line CRL Signing, CRL Signing (86)"
+ },
+ {
+ "critical": false,
+ "field": null,
+ "value": "05 00"
+ }
+ ]
+ friendly_name:
+ description: The associated alias for the certificate.
+ type: str
+ sample: Microsoft Root Authority
+ has_private_key:
+ description: Indicates that the certificate contains a private key.
+ type: bool
+ sample: false
+ intended_purposes:
+ description: lists the intended applications for the certificate.
+ returned: enhanced key usages extension exists.
+ type: list
+ sample: [ "Server Authentication" ]
+ is_ca:
+ description: Indicates that the certificate is a certificate authority (CA) certificate.
+ returned: basic constraints extension exists.
+ type: bool
+ sample: true
+ issued_by:
+ description: The certificate issuer's common name.
+ type: str
+ sample: Apple Root CA
+ issued_to:
+ description: The certificate's common name.
+ type: str
+ sample: Apple Worldwide Developer Relations Certification Authority
+ issuer:
+ description: The certificate issuer's distinguished name.
+ type: str
+ sample: 'CN=Apple Root CA, OU=Apple Certification Authority, O=Apple Inc., C=US'
+ key_usages:
+ description:
+ - Defines how the certificate key can be used.
+ - If this value is not defined, the key can be used for any purpose.
+ returned: key usages extension exists.
+ type: list
+ elements: str
+ sample: [ "CrlSign", "KeyCertSign", "DigitalSignature" ]
+ path_length_constraint:
+ description:
+ - The number of levels allowed in a certificates path.
+ - If this value is 0, the certificate does not have a restriction.
+ returned: basic constraints extension exists
+ type: int
+ sample: 0
+ public_key:
+ description: The base64 encoded public key of the certificate.
+ type: str
+ cert_data:
+ description: The base64 encoded data of the entire certificate.
+ type: str
+ serial_number:
+ description: The serial number of the certificate represented as a hexadecimal string
+ type: str
+ sample: 01DEBCC4396DA010
+ signature_algorithm:
+ description: The algorithm used to create the certificate's signature
+ type: str
+ sample: sha1RSA
+ ski:
+ description: The certificate's subject key identifier
+ returned: subject key identifier extension exists.
+ type: str
+ sample: 88271709A9B618608BECEBBAF64759C55254A3B7
+ subject:
+ description: The certificate's distinguished name.
+ type: str
+ sample: 'CN=Apple Worldwide Developer Relations Certification Authority, OU=Apple Worldwide Developer Relations, O=Apple Inc., C=US'
+ thumbprint:
+ description:
+ - The thumbprint as a hex string of the certificate.
+ - The return format will always be upper case.
+ type: str
+ sample: FF6797793A3CD798DC5B2ABEF56F73EDC9F83A64
+ valid_from:
+ description: The start date of the certificate represented in seconds since epoch.
+ type: float
+ sample: 1360255727
+ valid_from_iso8601:
+ description: The start date of the certificate represented as an iso8601 formatted date.
+ type: str
+ sample: '2017-12-15T08:39:32Z'
+ valid_to:
+ description: The expiry date of the certificate represented in seconds since epoch.
+ type: float
+ sample: 1675788527
+ valid_to_iso8601:
+ description: The expiry date of the certificate represented as an iso8601 formatted date.
+ type: str
+ sample: '2086-01-02T08:39:32Z'
+ version:
+ description: The x509 format version of the certificate
+ type: int
+ sample: 3
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_computer_description.ps1 b/ansible_collections/community/windows/plugins/modules/win_computer_description.ps1
new file mode 100644
index 00000000..445e5c2a
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_computer_description.ps1
@@ -0,0 +1,54 @@
+#!powershell
+
+# Copyright: (c) 2019, RusoSova
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#AnsibleRequires -OSVersion 6.1
+
+$spec = @{
+ options = @{
+ owner = @{ type = "str" }
+ organization = @{ type = "str" }
+ description = @{ type = "str" }
+ }
+ required_one_of = @(
+ , @('owner', 'organization', 'description')
+ )
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$owner = $module.Params.owner
+$organization = $module.Params.organization
+$description = $module.Params.description
+$regPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\"
+
+#Change description
+if ($description -or $description -eq "") {
+ $descriptionObject = Get-CimInstance -class "Win32_OperatingSystem"
+ if ($description -cne $descriptionObject.description) {
+ Set-CimInstance -InputObject $descriptionObject -Property @{"Description" = "$description" } -WhatIf:$module.CheckMode
+ $module.Result.changed = $true
+ }
+}
+
+#Change owner
+if ($owner -or $owner -eq "") {
+ $curentOwner = (Get-ItemProperty -LiteralPath $regPath -Name RegisteredOwner).RegisteredOwner
+ if ($curentOwner -cne $owner) {
+ Set-ItemProperty -LiteralPath $regPath -Name "RegisteredOwner" -Value $owner -WhatIf:$module.CheckMode
+ $module.Result.changed = $true
+ }
+}
+
+#Change organization
+if ($organization -or $organization -eq "") {
+ $curentOrganization = (Get-ItemProperty -LiteralPath $regPath -Name RegisteredOrganization).RegisteredOrganization
+ if ($curentOrganization -cne $organization) {
+ Set-ItemProperty -LiteralPath $regPath -Name "RegisteredOrganization" -Value $organization -WhatIf:$module.CheckMode
+ $module.Result.changed = $true
+ }
+}
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_computer_description.py b/ansible_collections/community/windows/plugins/modules/win_computer_description.py
new file mode 100644
index 00000000..48eff886
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_computer_description.py
@@ -0,0 +1,62 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2019, RusoSova
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_computer_description
+short_description: Set windows description, owner and organization
+description:
+ - This module sets Windows description that is shown under My Computer properties. Module also sets
+ Windows license owner and organization. License information can be viewed by running winver commad.
+options:
+ description:
+ description:
+ - String value to apply to Windows descripton. Specify value of "" to clear the value.
+ required: false
+ type: str
+ organization:
+ description:
+ - String value of organization that the Windows is licensed to. Specify value of "" to clear the value.
+ required: false
+ type: str
+ owner:
+ description:
+ - String value of the persona that the Windows is licensed to. Specify value of "" to clear the value.
+ required: false
+ type: str
+author:
+ - RusoSova (@RusoSova)
+'''
+
+EXAMPLES = r'''
+- name: Set Windows description, owner and organization
+ community.windows.win_computer_description:
+ description: Best Box
+ owner: RusoSova
+ organization: MyOrg
+ register: result
+
+- name: Set Windows description only
+ community.windows.win_computer_description:
+ description: This is my Windows machine
+ register: result
+
+- name: Set organization and clear owner field
+ community.windows.win_computer_description:
+ owner: ''
+ organization: Black Mesa
+
+- name: Clear organization, description and owner
+ community.windows.win_computer_description:
+ organization: ""
+ owner: ""
+ description: ""
+ register: result
+'''
+
+RETURN = r'''
+#
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_credential.ps1 b/ansible_collections/community/windows/plugins/modules/win_credential.ps1
new file mode 100644
index 00000000..b39bff57
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_credential.ps1
@@ -0,0 +1,724 @@
+#!powershell
+
+# Copyright: (c) 2018, 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.AddType
+
+$spec = @{
+ options = @{
+ alias = @{ type = "str" }
+ attributes = @{
+ type = "list"
+ elements = "dict"
+ options = @{
+ name = @{ type = "str"; required = $true }
+ data = @{ type = "str" }
+ data_format = @{ type = "str"; default = "text"; choices = @("base64", "text") }
+ }
+ }
+ comment = @{ type = "str" }
+ name = @{ type = "str"; required = $true }
+ persistence = @{ type = "str"; default = "local"; choices = @("enterprise", "local") }
+ secret = @{ type = "str"; no_log = $true }
+ secret_format = @{ type = "str"; default = "text"; choices = @("base64", "text") }
+ state = @{ type = "str"; default = "present"; choices = @("absent", "present") }
+ type = @{
+ type = "str"
+ required = $true
+ choices = @("domain_password", "domain_certificate", "generic_password", "generic_certificate")
+ }
+ update_secret = @{ type = "str"; default = "always"; choices = @("always", "on_create") }
+ username = @{ type = "str" }
+ }
+ required_if = @(
+ , @("state", "present", @("username"))
+ )
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$alias = $module.Params.alias
+$attributes = $module.Params.attributes
+$comment = $module.Params.comment
+$name = $module.Params.name
+$persistence = $module.Params.persistence
+$secret = $module.Params.secret
+$secret_format = $module.Params.secret_format
+$state = $module.Params.state
+$type = $module.Params.type
+$update_secret = $module.Params.update_secret
+$username = $module.Params.username
+
+$module.Diff.before = ""
+$module.Diff.after = ""
+
+Add-CSharpType -AnsibleModule $module -References @'
+using Microsoft.Win32.SafeHandles;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.ConstrainedExecution;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Ansible.CredentialManager
+{
+ internal class NativeHelpers
+ {
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ public class CREDENTIAL
+ {
+ public CredentialFlags Flags;
+ public CredentialType Type;
+ [MarshalAs(UnmanagedType.LPWStr)] public string TargetName;
+ [MarshalAs(UnmanagedType.LPWStr)] public string Comment;
+ public FILETIME LastWritten;
+ public UInt32 CredentialBlobSize;
+ public IntPtr CredentialBlob;
+ public CredentialPersist Persist;
+ public UInt32 AttributeCount;
+ public IntPtr Attributes;
+ [MarshalAs(UnmanagedType.LPWStr)] public string TargetAlias;
+ [MarshalAs(UnmanagedType.LPWStr)] public string UserName;
+
+ public static explicit operator Credential(CREDENTIAL v)
+ {
+ byte[] secret = new byte[(int)v.CredentialBlobSize];
+ if (v.CredentialBlob != IntPtr.Zero)
+ Marshal.Copy(v.CredentialBlob, secret, 0, secret.Length);
+
+ List<CredentialAttribute> attributes = new List<CredentialAttribute>();
+ if (v.AttributeCount > 0)
+ {
+ CREDENTIAL_ATTRIBUTE[] rawAttributes = new CREDENTIAL_ATTRIBUTE[v.AttributeCount];
+ Credential.PtrToStructureArray(rawAttributes, v.Attributes);
+ attributes = rawAttributes.Select(x => (CredentialAttribute)x).ToList();
+ }
+
+ string userName = v.UserName;
+ if (v.Type == CredentialType.DomainCertificate || v.Type == CredentialType.GenericCertificate)
+ userName = Credential.UnmarshalCertificateCredential(userName);
+
+ return new Credential
+ {
+ Type = v.Type,
+ TargetName = v.TargetName,
+ Comment = v.Comment,
+ LastWritten = (DateTimeOffset)v.LastWritten,
+ Secret = secret,
+ Persist = v.Persist,
+ Attributes = attributes,
+ TargetAlias = v.TargetAlias,
+ UserName = userName,
+ Loaded = true,
+ };
+ }
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct CREDENTIAL_ATTRIBUTE
+ {
+ [MarshalAs(UnmanagedType.LPWStr)] public string Keyword;
+ public UInt32 Flags; // Set to 0 and is reserved
+ public UInt32 ValueSize;
+ public IntPtr Value;
+
+ public static explicit operator CredentialAttribute(CREDENTIAL_ATTRIBUTE v)
+ {
+ byte[] value = new byte[v.ValueSize];
+ Marshal.Copy(v.Value, value, 0, (int)v.ValueSize);
+
+ return new CredentialAttribute
+ {
+ Keyword = v.Keyword,
+ Flags = v.Flags,
+ Value = value,
+ };
+ }
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ public struct FILETIME
+ {
+ internal UInt32 dwLowDateTime;
+ internal UInt32 dwHighDateTime;
+
+ public static implicit operator long(FILETIME v) { return ((long)v.dwHighDateTime << 32) + v.dwLowDateTime; }
+ public static explicit operator DateTimeOffset(FILETIME v) { return DateTimeOffset.FromFileTime(v); }
+ public static explicit operator FILETIME(DateTimeOffset v)
+ {
+ return new FILETIME()
+ {
+ dwLowDateTime = (UInt32)v.ToFileTime(),
+ dwHighDateTime = ((UInt32)v.ToFileTime() >> 32),
+ };
+ }
+ }
+
+ [Flags]
+ public enum CredentialCreateFlags : uint
+ {
+ PreserveCredentialBlob = 1,
+ }
+
+ [Flags]
+ public enum CredentialFlags
+ {
+ None = 0,
+ PromptNow = 2,
+ UsernameTarget = 4,
+ }
+
+ public enum CredMarshalType : uint
+ {
+ CertCredential = 1,
+ UsernameTargetCredential,
+ BinaryBlobCredential,
+ UsernameForPackedCredential,
+ BinaryBlobForSystem,
+ }
+ }
+
+ internal class NativeMethods
+ {
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ public static extern bool CredDeleteW(
+ [MarshalAs(UnmanagedType.LPWStr)] string TargetName,
+ CredentialType Type,
+ UInt32 Flags);
+
+ [DllImport("advapi32.dll")]
+ public static extern void CredFree(
+ IntPtr Buffer);
+
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ public static extern bool CredMarshalCredentialW(
+ NativeHelpers.CredMarshalType CredType,
+ SafeMemoryBuffer Credential,
+ out SafeCredentialBuffer MarshaledCredential);
+
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ public static extern bool CredReadW(
+ [MarshalAs(UnmanagedType.LPWStr)] string TargetName,
+ CredentialType Type,
+ UInt32 Flags,
+ out SafeCredentialBuffer Credential);
+
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ public static extern bool CredUnmarshalCredentialW(
+ [MarshalAs(UnmanagedType.LPWStr)] string MarshaledCredential,
+ out NativeHelpers.CredMarshalType CredType,
+ out SafeCredentialBuffer Credential);
+
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ public static extern bool CredWriteW(
+ NativeHelpers.CREDENTIAL Credential,
+ NativeHelpers.CredentialCreateFlags Flags);
+ }
+
+ internal class SafeCredentialBuffer : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ public SafeCredentialBuffer() : base(true) { }
+
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
+ protected override bool ReleaseHandle()
+ {
+ NativeMethods.CredFree(handle);
+ return true;
+ }
+ }
+
+ internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ public SafeMemoryBuffer() : base(true) { }
+ public SafeMemoryBuffer(int cb) : base(true)
+ {
+ base.SetHandle(Marshal.AllocHGlobal(cb));
+ }
+ public SafeMemoryBuffer(IntPtr handle) : base(true)
+ {
+ base.SetHandle(handle);
+ }
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
+ protected override bool ReleaseHandle()
+ {
+ Marshal.FreeHGlobal(handle);
+ return true;
+ }
+ }
+
+ public class Win32Exception : System.ComponentModel.Win32Exception
+ {
+ private string _exception_msg;
+ public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
+ public Win32Exception(int errorCode, string message) : base(errorCode)
+ {
+ _exception_msg = String.Format("{0} - {1} (Win32 Error Code {2}: 0x{3})", message, base.Message, errorCode, errorCode.ToString("X8"));
+ }
+ public override string Message { get { return _exception_msg; } }
+ public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
+ }
+
+ public enum CredentialPersist
+ {
+ Session = 1,
+ LocalMachine = 2,
+ Enterprise = 3,
+ }
+
+ public enum CredentialType
+ {
+ Generic = 1,
+ DomainPassword = 2,
+ DomainCertificate = 3,
+ DomainVisiblePassword = 4,
+ GenericCertificate = 5,
+ DomainExtended = 6,
+ Maximum = 7,
+ MaximumEx = 1007,
+ }
+
+ public class CredentialAttribute
+ {
+ public string Keyword;
+ public UInt32 Flags;
+ public byte[] Value;
+ }
+
+ public class Credential
+ {
+ public CredentialType Type;
+ public string TargetName;
+ public string Comment;
+ public DateTimeOffset LastWritten;
+ public byte[] Secret;
+ public CredentialPersist Persist;
+ public List<CredentialAttribute> Attributes = new List<CredentialAttribute>();
+ public string TargetAlias;
+ public string UserName;
+
+ // Used to track whether the credential has been loaded into the store or not
+ public bool Loaded { get; internal set; }
+
+ public void Delete()
+ {
+ if (!Loaded)
+ return;
+
+ if (!NativeMethods.CredDeleteW(TargetName, Type, 0))
+ throw new Win32Exception(String.Format("CredDeleteW({0}) failed", TargetName));
+ Loaded = false;
+ }
+
+ public void Write(bool preserveExisting)
+ {
+ string userName = UserName;
+ // Convert the certificate thumbprint to the string expected
+ if (Type == CredentialType.DomainCertificate || Type == CredentialType.GenericCertificate)
+ userName = Credential.MarshalCertificateCredential(userName);
+
+ NativeHelpers.CREDENTIAL credential = new NativeHelpers.CREDENTIAL
+ {
+ Flags = NativeHelpers.CredentialFlags.None,
+ Type = Type,
+ TargetName = TargetName,
+ Comment = Comment,
+ LastWritten = new NativeHelpers.FILETIME(),
+ CredentialBlobSize = (UInt32)(Secret == null ? 0 : Secret.Length),
+ CredentialBlob = IntPtr.Zero, // Must be allocated and freed outside of this to ensure no memory leaks
+ Persist = Persist,
+ AttributeCount = (UInt32)(Attributes.Count),
+ Attributes = IntPtr.Zero, // Attributes must be allocated and freed outside of this to ensure no memory leaks
+ TargetAlias = TargetAlias,
+ UserName = userName,
+ };
+
+ using (SafeMemoryBuffer credentialBlob = new SafeMemoryBuffer((int)credential.CredentialBlobSize))
+ {
+ if (Secret != null)
+ Marshal.Copy(Secret, 0, credentialBlob.DangerousGetHandle(), Secret.Length);
+ credential.CredentialBlob = credentialBlob.DangerousGetHandle();
+
+ // Store the CREDENTIAL_ATTRIBUTE value in a safe memory buffer and make sure we dispose in all cases
+ List<SafeMemoryBuffer> attributeBuffers = new List<SafeMemoryBuffer>();
+ try
+ {
+ int attributeLength = Attributes.Sum(a => Marshal.SizeOf(typeof(NativeHelpers.CREDENTIAL_ATTRIBUTE)));
+ byte[] attributeBytes = new byte[attributeLength];
+ int offset = 0;
+ foreach (CredentialAttribute attribute in Attributes)
+ {
+ SafeMemoryBuffer attributeBuffer = new SafeMemoryBuffer(attribute.Value.Length);
+ attributeBuffers.Add(attributeBuffer);
+ if (attribute.Value != null)
+ Marshal.Copy(attribute.Value, 0, attributeBuffer.DangerousGetHandle(), attribute.Value.Length);
+
+ NativeHelpers.CREDENTIAL_ATTRIBUTE credentialAttribute = new NativeHelpers.CREDENTIAL_ATTRIBUTE
+ {
+ Keyword = attribute.Keyword,
+ Flags = attribute.Flags,
+ ValueSize = (UInt32)(attribute.Value == null ? 0 : attribute.Value.Length),
+ Value = attributeBuffer.DangerousGetHandle(),
+ };
+ int attributeStructLength = Marshal.SizeOf(typeof(NativeHelpers.CREDENTIAL_ATTRIBUTE));
+
+ byte[] attrBytes = new byte[attributeStructLength];
+ using (SafeMemoryBuffer tempBuffer = new SafeMemoryBuffer(attributeStructLength))
+ {
+ Marshal.StructureToPtr(credentialAttribute, tempBuffer.DangerousGetHandle(), false);
+ Marshal.Copy(tempBuffer.DangerousGetHandle(), attrBytes, 0, attributeStructLength);
+ }
+ Buffer.BlockCopy(attrBytes, 0, attributeBytes, offset, attributeStructLength);
+ offset += attributeStructLength;
+ }
+
+ using (SafeMemoryBuffer attributes = new SafeMemoryBuffer(attributeBytes.Length))
+ {
+ if (attributeBytes.Length != 0)
+ {
+ Marshal.Copy(attributeBytes, 0, attributes.DangerousGetHandle(), attributeBytes.Length);
+ credential.Attributes = attributes.DangerousGetHandle();
+ }
+
+ NativeHelpers.CredentialCreateFlags createFlags = 0;
+ if (preserveExisting)
+ createFlags |= NativeHelpers.CredentialCreateFlags.PreserveCredentialBlob;
+
+ if (!NativeMethods.CredWriteW(credential, createFlags))
+ throw new Win32Exception(String.Format("CredWriteW({0}) failed", TargetName));
+ }
+ }
+ finally
+ {
+ foreach (SafeMemoryBuffer attributeBuffer in attributeBuffers)
+ attributeBuffer.Dispose();
+ }
+ }
+ Loaded = true;
+ }
+
+ public static Credential GetCredential(string target, CredentialType type)
+ {
+ SafeCredentialBuffer buffer;
+ if (!NativeMethods.CredReadW(target, type, 0, out buffer))
+ {
+ int lastErr = Marshal.GetLastWin32Error();
+
+ // Not running with Become so cannot manage the user's credentials
+ if (lastErr == 0x00000520) // ERROR_NO_SUCH_LOGON_SESSION
+ throw new InvalidOperationException("Failed to access the user's credential store, run the module with become");
+ else if (lastErr == 0x00000490) // ERROR_NOT_FOUND
+ return null;
+ throw new Win32Exception(lastErr, "CredEnumerateW() failed");
+ }
+
+ using (buffer)
+ {
+ NativeHelpers.CREDENTIAL credential = (NativeHelpers.CREDENTIAL)Marshal.PtrToStructure(
+ buffer.DangerousGetHandle(), typeof(NativeHelpers.CREDENTIAL));
+ return (Credential)credential;
+ }
+ }
+
+ public static string MarshalCertificateCredential(string thumbprint)
+ {
+ // CredWriteW requires the UserName field to be the value of CredMarshalCredentialW() when writting a
+ // certificate auth. This converts the UserName property to the format required.
+
+ // While CERT_CREDENTIAL_INFO is the correct structure, we manually marshal the data in order to
+ // support different cert hash lengths in the future.
+ // https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_cert_credential_info
+ int hexLength = thumbprint.Length;
+ byte[] credInfo = new byte[sizeof(UInt32) + (hexLength / 2)];
+
+ // First field is cbSize which is a UInt32 value denoting the size of the total structure
+ Array.Copy(BitConverter.GetBytes((UInt32)credInfo.Length), credInfo, sizeof(UInt32));
+
+ // Now copy the byte representation of the thumbprint to the rest of the struct bytes
+ for (int i = 0; i < hexLength; i += 2)
+ credInfo[sizeof(UInt32) + (i / 2)] = Convert.ToByte(thumbprint.Substring(i, 2), 16);
+
+ IntPtr pCredInfo = Marshal.AllocHGlobal(credInfo.Length);
+ Marshal.Copy(credInfo, 0, pCredInfo, credInfo.Length);
+ SafeMemoryBuffer pCredential = new SafeMemoryBuffer(pCredInfo);
+
+ NativeHelpers.CredMarshalType marshalType = NativeHelpers.CredMarshalType.CertCredential;
+ using (pCredential)
+ {
+ SafeCredentialBuffer marshaledCredential;
+ if (!NativeMethods.CredMarshalCredentialW(marshalType, pCredential, out marshaledCredential))
+ throw new Win32Exception("CredMarshalCredentialW() failed");
+ using (marshaledCredential)
+ return Marshal.PtrToStringUni(marshaledCredential.DangerousGetHandle());
+ }
+ }
+
+ public static string UnmarshalCertificateCredential(string value)
+ {
+ NativeHelpers.CredMarshalType credType;
+ SafeCredentialBuffer pCredInfo;
+ if (!NativeMethods.CredUnmarshalCredentialW(value, out credType, out pCredInfo))
+ throw new Win32Exception("CredUnmarshalCredentialW() failed");
+
+ using (pCredInfo)
+ {
+ if (credType != NativeHelpers.CredMarshalType.CertCredential)
+ throw new InvalidOperationException(String.Format("Expected unmarshalled cred type of CertCredential, received {0}", credType));
+
+ byte[] structSizeBytes = new byte[sizeof(UInt32)];
+ Marshal.Copy(pCredInfo.DangerousGetHandle(), structSizeBytes, 0, sizeof(UInt32));
+ UInt32 structSize = BitConverter.ToUInt32(structSizeBytes, 0);
+
+ byte[] certInfoBytes = new byte[structSize];
+ Marshal.Copy(pCredInfo.DangerousGetHandle(), certInfoBytes, 0, certInfoBytes.Length);
+
+ StringBuilder hex = new StringBuilder((certInfoBytes.Length - sizeof(UInt32)) * 2);
+ for (int i = 4; i < certInfoBytes.Length; i++)
+ hex.AppendFormat("{0:x2}", certInfoBytes[i]);
+
+ return hex.ToString().ToUpperInvariant();
+ }
+ }
+
+ internal static void PtrToStructureArray<T>(T[] array, IntPtr ptr)
+ {
+ IntPtr ptrOffset = ptr;
+ for (int i = 0; i < array.Length; i++, ptrOffset = IntPtr.Add(ptrOffset, Marshal.SizeOf(typeof(T))))
+ array[i] = (T)Marshal.PtrToStructure(ptrOffset, typeof(T));
+ }
+ }
+}
+'@
+
+Function ConvertTo-CredentialAttribute {
+ param($Attributes)
+
+ $converted_attributes = [System.Collections.Generic.List`1[Ansible.CredentialManager.CredentialAttribute]]@()
+ foreach ($attribute in $Attributes) {
+ $new_attribute = New-Object -TypeName Ansible.CredentialManager.CredentialAttribute
+ $new_attribute.Keyword = $attribute.name
+
+ if ($null -ne $attribute.data) {
+ if ($attribute.data_format -eq "base64") {
+ $new_attribute.Value = [System.Convert]::FromBase64String($attribute.data)
+ }
+ else {
+ $new_attribute.Value = [System.Text.Encoding]::UTF8.GetBytes($attribute.data)
+ }
+ }
+ $converted_attributes.Add($new_attribute) > $null
+ }
+
+ return , $converted_attributes
+}
+
+Function Get-DiffInfo {
+ param($AnsibleCredential)
+
+ $diff = @{
+ alias = $AnsibleCredential.TargetAlias
+ attributes = [System.Collections.ArrayList]@()
+ comment = $AnsibleCredential.Comment
+ name = $AnsibleCredential.TargetName
+ persistence = $AnsibleCredential.Persist.ToString()
+ type = $AnsibleCredential.Type.ToString()
+ username = $AnsibleCredential.UserName
+ }
+
+ foreach ($attribute in $AnsibleCredential.Attributes) {
+ $attribute_info = @{
+ name = $attribute.Keyword
+ data = $null
+ }
+ if ($null -ne $attribute.Value) {
+ $attribute_info.data = [System.Convert]::ToBase64String($attribute.Value)
+ }
+ $diff.attributes.Add($attribute_info) > $null
+ }
+
+ return , $diff
+}
+
+# If the username is a certificate thumbprint, verify it's a valid cert in the CurrentUser/Personal store
+if ($null -ne $username -and $type -in @("domain_certificate", "generic_certificate")) {
+ # Ensure the thumbprint is upper case with no spaces or hyphens
+ $username = $username.ToUpperInvariant().Replace(" ", "").Replace("-", "")
+
+ $certificate = Get-Item -LiteralPath Cert:\CurrentUser\My\$username -ErrorAction SilentlyContinue
+ if ($null -eq $certificate) {
+ $module.FailJson("Failed to find certificate with the thumbprint $username in the CurrentUser\My store")
+ }
+}
+
+# Convert the input secret to a byte array
+if ($null -ne $secret) {
+ if ($secret_format -eq "base64") {
+ $secret = [System.Convert]::FromBase64String($secret)
+ }
+ else {
+ $secret = [System.Text.Encoding]::Unicode.GetBytes($secret)
+ }
+}
+
+$persistence = switch ($persistence) {
+ "local" { [Ansible.CredentialManager.CredentialPersist]::LocalMachine }
+ "enterprise" { [Ansible.CredentialManager.CredentialPersist]::Enterprise }
+}
+
+$type = switch ($type) {
+ "domain_password" { [Ansible.CredentialManager.CredentialType]::DomainPassword }
+ "domain_certificate" { [Ansible.CredentialManager.CredentialType]::DomainCertificate }
+ "generic_password" { [Ansible.CredentialManager.CredentialType]::Generic }
+ "generic_certificate" { [Ansible.CredentialManager.CredentialType]::GenericCertificate }
+}
+
+$existing_credential = [Ansible.CredentialManager.Credential]::GetCredential($name, $type)
+if ($null -ne $existing_credential) {
+ $module.Diff.before = Get-DiffInfo -AnsibleCredential $existing_credential
+}
+
+if ($state -eq "absent") {
+ if ($null -ne $existing_credential) {
+ if (-not $module.CheckMode) {
+ $existing_credential.Delete()
+ }
+ $module.Result.changed = $true
+ }
+}
+else {
+ if ($null -eq $existing_credential) {
+ $new_credential = New-Object -TypeName Ansible.CredentialManager.Credential
+ $new_credential.Type = $type
+ $new_credential.TargetName = $name
+ $new_credential.Comment = if ($comment) { $comment } else { [NullString]::Value }
+ $new_credential.Secret = $secret
+ $new_credential.Persist = $persistence
+ $new_credential.TargetAlias = if ($alias) { $alias } else { [NullString]::Value }
+ $new_credential.UserName = $username
+
+ if ($null -ne $attributes) {
+ $new_credential.Attributes = ConvertTo-CredentialAttribute -Attributes $attributes
+ }
+
+ if (-not $module.CheckMode) {
+ $new_credential.Write($false)
+ }
+ $module.Result.changed = $true
+ }
+ else {
+ $changed = $false
+ $preserve_blob = $false
+
+ # make sure we do case comparison for the comment
+ if ($existing_credential.Comment -cne $comment) {
+ $existing_credential.Comment = $comment
+ $changed = $true
+ }
+
+ if ($existing_credential.Persist -ne $persistence) {
+ $existing_credential.Persist = $persistence
+ $changed = $true
+ }
+
+ if ($existing_credential.TargetAlias -ne $alias) {
+ $existing_credential.TargetAlias = $alias
+ $changed = $true
+ }
+
+ if ($existing_credential.UserName -ne $username) {
+ $existing_credential.UserName = $username
+ $changed = $true
+ }
+
+ if ($null -ne $attributes) {
+ $attribute_changed = $false
+
+ $new_attributes = ConvertTo-CredentialAttribute -Attributes $attributes
+ if ($new_attributes.Count -ne $existing_credential.Attributes.Count) {
+ $attribute_changed = $true
+ }
+ else {
+ for ($i = 0; $i -lt $new_attributes.Count; $i++) {
+ $new_keyword = $new_attributes[$i].Keyword
+ $new_value = $new_attributes[$i].Value
+ if ($null -eq $new_value) {
+ $new_value = ""
+ }
+ else {
+ $new_value = [System.Convert]::ToBase64String($new_value)
+ }
+
+ $existing_keyword = $existing_credential.Attributes[$i].Keyword
+ $existing_value = $existing_credential.Attributes[$i].Value
+ if ($null -eq $existing_value) {
+ $existing_value = ""
+ }
+ else {
+ $existing_value = [System.Convert]::ToBase64String($existing_value)
+ }
+
+ if (($new_keyword -cne $existing_keyword) -or ($new_value -ne $existing_value)) {
+ $attribute_changed = $true
+ break
+ }
+ }
+ }
+
+ if ($attribute_changed) {
+ $existing_credential.Attributes = $new_attributes
+ $changed = $true
+ }
+ }
+
+ if ($null -eq $secret) {
+ # If we haven't explicitly set a secret, tell Windows to preserve the existing blob
+ $preserve_blob = $true
+ $existing_credential.Secret = $null
+ }
+ elseif ($update_secret -eq "always") {
+ # We should only set the password if we can't read the existing one or it doesn't match our secret
+ if ($existing_credential.Secret.Length -eq 0) {
+ # We cannot read the secret so don't know if its the configured secret
+ $existing_credential.Secret = $secret
+ $changed = $true
+ }
+ else {
+ # We can read the secret so compare with our input
+ $input_secret_b64 = [System.Convert]::ToBase64String($secret)
+ $actual_secret_b64 = [System.Convert]::ToBase64String($existing_credential.Secret)
+ if ($input_secret_b64 -ne $actual_secret_b64) {
+ $existing_credential.Secret = $secret
+ $changed = $true
+ }
+ }
+ }
+
+ if ($changed -and -not $module.CheckMode) {
+ $existing_credential.Write($preserve_blob)
+ }
+ $module.Result.changed = $changed
+ }
+
+ if ($module.CheckMode) {
+ # We cannot reliably get the credential in check mode, set it based on the input
+ $module.Diff.after = @{
+ alias = $alias
+ attributes = $attributes
+ comment = $comment
+ name = $name
+ persistence = $persistence.ToString()
+ type = $type.ToString()
+ username = $username
+ }
+ }
+ else {
+ # Get a new copy of the credential and use that to set the after diff
+ $new_credential = [Ansible.CredentialManager.Credential]::GetCredential($name, $type)
+ $module.Diff.after = Get-DiffInfo -AnsibleCredential $new_credential
+ }
+}
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_credential.py b/ansible_collections/community/windows/plugins/modules/win_credential.py
new file mode 100644
index 00000000..fd605f0d
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_credential.py
@@ -0,0 +1,206 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_credential
+short_description: Manages Windows Credentials in the Credential Manager
+description:
+- Used to create and remove Windows Credentials in the Credential Manager.
+- This module can manage both standard username/password credentials as well as
+ certificate credentials.
+options:
+ alias:
+ description:
+ - Adds an alias for the credential.
+ - Typically this is the NetBIOS name of a host if I(name) is set to the DNS
+ name.
+ type: str
+ attributes:
+ description:
+ - A list of dicts that set application specific attributes for a
+ credential.
+ - When set, existing attributes will be compared to the list as a whole,
+ any differences means all attributes will be replaced.
+ type: list
+ elements: dict
+ suboptions:
+ name:
+ description:
+ - The key for the attribute.
+ - This is not a unique identifier as multiple attributes can have the
+ same key.
+ type: str
+ required: true
+ data:
+ description:
+ - The value for the attribute.
+ type: str
+ data_format:
+ description:
+ - Controls the input type for I(data).
+ - If C(text), I(data) is a text string that is UTF-16LE encoded to
+ bytes.
+ - If C(base64), I(data) is a base64 string that is base64 decoded to
+ bytes.
+ type: str
+ choices: [ base64, text ]
+ default: text
+ comment:
+ description:
+ - A user defined comment for the credential.
+ type: str
+ name:
+ description:
+ - The target that identifies the server or servers that the credential is
+ to be used for.
+ - If the value can be a NetBIOS name, DNS server name, DNS host name suffix
+ with a wildcard character (C(*)), a NetBIOS of DNS domain name that
+ contains a wildcard character sequence, or an asterisk.
+ - See C(TargetName) in U(https://docs.microsoft.com/en-us/windows/win32/api/wincred/ns-wincred-credentiala)
+ for more details on what this value can be.
+ - This is used with I(type) to produce a unique credential.
+ type: str
+ required: true
+ persistence:
+ description:
+ - Defines the persistence of the credential.
+ - If C(local), the credential will persist for all logons of the same user
+ on the same host.
+ - C(enterprise) is the same as C(local) but the credential is visible to
+ the same domain user when running on other hosts and not just localhost.
+ type: str
+ choices: [ enterprise, local ]
+ default: local
+ secret:
+ description:
+ - The secret for the credential.
+ - When omitted, then no secret is used for the credential if a new
+ credentials is created.
+ - When I(type) is a password type, this is the password for I(username).
+ - When I(type) is a certificate type, this is the pin for the certificate.
+ type: str
+ secret_format:
+ description:
+ - Controls the input type for I(secret).
+ - If C(text), I(secret) is a text string that is UTF-16LE encoded to bytes.
+ - If C(base64), I(secret) is a base64 string that is base64 decoded to
+ bytes.
+ type: str
+ choices: [ base64, text ]
+ default: text
+ state:
+ description:
+ - When C(absent), the credential specified by I(name) and I(type) is
+ removed.
+ - When C(present), the credential specified by I(name) and I(type) is
+ removed.
+ type: str
+ choices: [ absent, present ]
+ default: present
+ type:
+ description:
+ - The type of credential to store.
+ - This is used with I(name) to produce a unique credential.
+ - When the type is a C(domain) type, the credential is used by Microsoft
+ authentication packages like Negotiate.
+ - When the type is a C(generic) type, the credential is not used by any
+ particular authentication package.
+ - It is recommended to use a C(domain) type as only authentication
+ providers can access the secret.
+ type: str
+ required: true
+ choices: [ domain_certificate, domain_password, generic_certificate, generic_password ]
+ update_secret:
+ description:
+ - When C(always), the secret will always be updated if they differ.
+ - When C(on_create), the secret will only be checked/updated when it is
+ first created.
+ - If the secret cannot be retrieved and this is set to C(always), the
+ module will always result in a change.
+ type: str
+ choices: [ always, on_create ]
+ default: always
+ username:
+ description:
+ - When I(type) is a password type, then this is the username to store for
+ the credential.
+ - When I(type) is a credential type, then this is the thumbprint as a hex
+ string of the certificate to use.
+ - When C(type=domain_password), this should be in the form of a Netlogon
+ (DOMAIN\Username) or a UPN (username@DOMAIN).
+ - If using a certificate thumbprint, the certificate must exist in the
+ C(CurrentUser\My) certificate store for the executing user.
+ type: str
+notes:
+- This module requires to be run with C(become) so it can access the
+ user's credential store.
+- There can only be one credential per host and type. if a second credential is
+ defined that uses the same host and type, then the original credential is
+ overwritten.
+seealso:
+- module: ansible.windows.win_user_right
+- module: ansible.windows.win_whoami
+author:
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = r'''
+- name: Create a local only credential
+ community.windows.win_credential:
+ name: server.domain.com
+ type: domain_password
+ username: DOMAIN\username
+ secret: Password01
+ state: present
+
+- name: Remove a credential
+ community.windows.win_credential:
+ name: server.domain.com
+ type: domain_password
+ state: absent
+
+- name: Create a credential with full values
+ community.windows.win_credential:
+ name: server.domain.com
+ type: domain_password
+ alias: server
+ username: username@DOMAIN.COM
+ secret: Password01
+ comment: Credential for server.domain.com
+ persistence: enterprise
+ attributes:
+ - name: Source
+ data: Ansible
+ - name: Unique Identifier
+ data: Y3VzdG9tIGF0dHJpYnV0ZQ==
+ data_format: base64
+
+- name: Create a certificate credential
+ community.windows.win_credential:
+ name: '*.domain.com'
+ type: domain_certificate
+ username: 0074CC4F200D27DC3877C24A92BA8EA21E6C7AF4
+ state: present
+
+- name: Create a generic credential
+ community.windows.win_credential:
+ name: smbhost
+ type: generic_password
+ username: smbuser
+ secret: smbuser
+ state: present
+
+- name: Remove a generic credential
+ community.windows.win_credential:
+ name: smbhost
+ type: generic_password
+ state: absent
+'''
+
+RETURN = r'''
+#
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_data_deduplication.ps1 b/ansible_collections/community/windows/plugins/modules/win_data_deduplication.ps1
new file mode 100644
index 00000000..b326ea54
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_data_deduplication.ps1
@@ -0,0 +1,132 @@
+#!powershell
+
+# Copyright: 2019, rnsc(@rnsc) <github@rnsc.be>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#AnsibleRequires -OSVersion 6.3
+
+$spec = @{
+ options = @{
+ drive_letter = @{ type = "str"; required = $true }
+ state = @{ type = "str"; choices = "absent", "present"; default = "present"; }
+ settings = @{
+ type = "dict"
+ required = $false
+ options = @{
+ minimum_file_size = @{ type = "int"; default = 32768 }
+ minimum_file_age_days = @{ type = "int"; default = 2 }
+ no_compress = @{ type = "bool"; required = $false; default = $false }
+ optimize_in_use_files = @{ type = "bool"; required = $false; default = $false }
+ verify = @{ type = "bool"; required = $false; default = $false }
+ }
+ }
+ }
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$drive_letter = $module.Params.drive_letter
+$state = $module.Params.state
+$settings = $module.Params.settings
+
+$module.Result.changed = $false
+$module.Result.reboot_required = $false
+$module.Result.msg = ""
+
+function Set-DataDeduplication($volume, $state, $settings, $dedup_job) {
+
+ $current_state = 'absent'
+
+ try {
+ $dedup_info = Get-DedupVolume -Volume "$($volume.DriveLetter):"
+ }
+ catch {
+ $dedup_info = $null
+ }
+
+ if ($dedup_info.Enabled) {
+ $current_state = 'present'
+ }
+
+ if ( $state -ne $current_state ) {
+ if ( -not $module.CheckMode) {
+ if ($state -eq 'present') {
+ # Enable-DedupVolume -Volume <String>
+ Enable-DedupVolume -Volume "$($volume.DriveLetter):"
+ }
+ elseif ($state -eq 'absent') {
+ Disable-DedupVolume -Volume "$($volume.DriveLetter):"
+ }
+ }
+ $module.Result.changed = $true
+ }
+
+ if ($state -eq 'present') {
+ if ($null -ne $settings) {
+ Set-DataDedupJobSetting -volume $volume -settings $settings
+ }
+ }
+}
+
+function Set-DataDedupJobSetting ($volume, $settings) {
+
+ try {
+ $dedup_info = Get-DedupVolume -Volume "$($volume.DriveLetter):"
+ }
+ catch {
+ $dedup_info = $null
+ }
+
+ ForEach ($key in $settings.keys) {
+
+ # See Microsoft documentation:
+ # https://docs.microsoft.com/en-us/powershell/module/deduplication/set-dedupvolume?view=win10-ps
+
+ $update_key = $key
+ $update_value = $settings.$($key)
+ # Transform Ansible style options to Powershell params
+ $update_key = $update_key -replace ('_', '')
+
+ if ($update_key -eq "MinimumFileSize" -and $update_value -lt 32768) {
+ $update_value = 32768
+ }
+
+ $current_value = ($dedup_info | Select-Object -ExpandProperty $update_key)
+
+ if ($update_value -ne $current_value) {
+ $command_param = @{
+ $($update_key) = $update_value
+ }
+
+ # Set-DedupVolume -Volume <String>`
+ # -NoCompress <bool> `
+ # -MinimumFileAgeDays <UInt32> `
+ # -MinimumFileSize <UInt32> (minimum 32768)
+ if ( -not $module.CheckMode ) {
+ Set-DedupVolume -Volume "$($volume.DriveLetter):" @command_param
+ }
+
+ $module.Result.changed = $true
+ }
+ }
+
+}
+
+# Install required feature
+$feature_name = "FS-Data-Deduplication"
+if ( -not $module.CheckMode) {
+ $feature = Install-WindowsFeature -Name $feature_name
+
+ if ($feature.RestartNeeded -eq 'Yes') {
+ $module.Result.reboot_required = $true
+ $module.FailJson("$feature_name was installed but requires Windows to be rebooted to work.")
+ }
+}
+
+$volume = Get-Volume -DriveLetter $drive_letter
+
+Set-DataDeduplication -volume $volume -state $state -settings $settings -dedup_job $dedup_job
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_data_deduplication.py b/ansible_collections/community/windows/plugins/modules/win_data_deduplication.py
new file mode 100644
index 00000000..f4347462
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_data_deduplication.py
@@ -0,0 +1,82 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: 2019, rnsc(@rnsc) <github@rnsc.be>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_data_deduplication
+short_description: Module to enable Data Deduplication on a volume.
+description:
+- This module can be used to enable Data Deduplication on a Windows volume.
+- The module will install the FS-Data-Deduplication feature (a reboot will be necessary).
+options:
+ drive_letter:
+ description:
+ - Windows drive letter on which to enable data deduplication.
+ required: yes
+ type: str
+ state:
+ description:
+ - Wether to enable or disable data deduplication on the selected volume.
+ default: present
+ type: str
+ choices: [ present, absent ]
+ settings:
+ description:
+ - Dictionary of settings to pass to the Set-DedupVolume powershell command.
+ type: dict
+ suboptions:
+ minimum_file_size:
+ description:
+ - Minimum file size you want to target for deduplication.
+ - It will default to 32768 if not defined or if the value is less than 32768.
+ type: int
+ default: 32768
+ minimum_file_age_days:
+ description:
+ - Minimum file age you want to target for deduplication.
+ type: int
+ default: 2
+ no_compress:
+ description:
+ - Wether you want to enabled filesystem compression or not.
+ type: bool
+ default: no
+ optimize_in_use_files:
+ description:
+ - Indicates that the server attempts to optimize currently open files.
+ type: bool
+ default: no
+ verify:
+ description:
+ - Indicates whether the deduplication engine performs a byte-for-byte verification for each duplicate chunk
+ that optimization creates, rather than relying on a cryptographically strong hash.
+ - This option is not recommend.
+ - Setting this parameter to True can degrade optimization performance.
+ type: bool
+ default: no
+author:
+- rnsc (@rnsc)
+'''
+
+EXAMPLES = r'''
+- name: Enable Data Deduplication on D
+ community.windows.win_data_deduplication:
+ drive_letter: 'D'
+ state: present
+
+- name: Enable Data Deduplication on D
+ community.windows.win_data_deduplication:
+ drive_letter: 'D'
+ state: present
+ settings:
+ no_compress: true
+ minimum_file_age_days: 1
+ minimum_file_size: 0
+'''
+
+RETURN = r'''
+#
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_defrag.ps1 b/ansible_collections/community/windows/plugins/modules/win_defrag.ps1
new file mode 100644
index 00000000..067e4908
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_defrag.ps1
@@ -0,0 +1,101 @@
+#!powershell
+
+# Copyright: (c) 2017, Dag Wieers (@dagwieers) <dag@wieers.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#Requires -Module Ansible.ModuleUtils.ArgvParser
+#Requires -Module Ansible.ModuleUtils.CommandUtil
+
+$spec = @{
+ options = @{
+ include_volumes = @{ type = 'list'; elements = 'str' }
+ exclude_volumes = @{ type = 'list'; elements = 'str' }
+ freespace_consolidation = @{ type = 'bool'; default = $false }
+ priority = @{ type = 'str'; default = 'low'; choices = @( 'low', 'normal') }
+ parallel = @{ type = 'bool'; default = $false }
+ }
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$include_volumes = $module.Params.include_volumes
+$exclude_volumes = $module.Params.exclude_volumes
+$freespace_consolidation = $module.Params.freespace_consolidation
+$priority = $module.Params.priority
+$parallel = $module.Params.parallel
+
+$module.Result.changed = $false
+
+$executable = "defrag.exe"
+
+if (-not (Get-Command -Name $executable -ErrorAction SilentlyContinue)) {
+ $module.FailJson("Command '$executable' not found in $env:PATH.")
+}
+
+$arguments = @()
+
+if ($include_volumes) {
+ foreach ($volume in $include_volumes) {
+ if ($volume.Length -eq 1) {
+ $arguments += "$($volume):"
+ }
+ else {
+ $arguments += $volume
+ }
+ }
+}
+else {
+ $arguments += "/C"
+}
+
+if ($exclude_volumes) {
+ $arguments += "/E"
+ foreach ($volume in $exclude_volumes) {
+ if ($volume.Length -eq 1) {
+ $arguments += "$($volume):"
+ }
+ else {
+ $arguments += $volume
+ }
+ }
+}
+
+if ($module.CheckMode) {
+ $arguments += "/A"
+}
+elseif ($freespace_consolidation) {
+ $arguments += "/X"
+}
+
+if ($priority -eq "normal") {
+ $arguments += "/H"
+}
+
+if ($parallel) {
+ $arguments += "/M"
+}
+
+$arguments += "/V"
+
+$argument_string = Argv-ToString -arguments $arguments
+
+$start_datetime = [DateTime]::UtcNow
+$module.Result.cmd = "$executable $argument_string"
+
+$command_result = Run-Command -command "$executable $argument_string"
+
+$end_datetime = [DateTime]::UtcNow
+
+$module.Result.stdout = $command_result.stdout
+$module.Result.stderr = $command_result.stderr
+$module.Result.rc = $command_result.rc
+
+$module.Result.start = $start_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff")
+$module.Result.end = $end_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff")
+$module.Result.delta = $($end_datetime - $start_datetime).ToString("h\:mm\:ss\.ffffff")
+
+$module.Result.changed = $true
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_defrag.py b/ansible_collections/community/windows/plugins/modules/win_defrag.py
new file mode 100644
index 00000000..7a268d50
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_defrag.py
@@ -0,0 +1,98 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: 2017, Dag Wieers (@dagwieers) <dag@wieers.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_defrag
+short_description: Consolidate fragmented files on local volumes
+description:
+- Locates and consolidates fragmented files on local volumes to improve system performance.
+- 'More information regarding C(win_defrag) is available from: U(https://technet.microsoft.com/en-us/library/cc731650%28v%3Dws.11.aspx%29)'
+requirements:
+- defrag.exe
+options:
+ include_volumes:
+ description:
+ - A list of drive letters or mount point paths of the volumes to be defragmented.
+ - If this parameter is omitted, all volumes (not excluded) will be fragmented.
+ type: list
+ elements: str
+ exclude_volumes:
+ description:
+ - A list of drive letters or mount point paths to exclude from defragmentation.
+ type: list
+ elements: str
+ freespace_consolidation:
+ description:
+ - Perform free space consolidation on the specified volumes.
+ type: bool
+ default: no
+ priority:
+ description:
+ - Run the operation at low or normal priority.
+ type: str
+ choices: [ low, normal ]
+ default: low
+ parallel:
+ description:
+ - Run the operation on each volume in parallel in the background.
+ type: bool
+ default: no
+author:
+- Dag Wieers (@dagwieers)
+'''
+
+EXAMPLES = r'''
+- name: Defragment all local volumes (in parallel)
+ community.windows.win_defrag:
+ parallel: yes
+
+- name: 'Defragment all local volumes, except C: and D:'
+ community.windows.win_defrag:
+ exclude_volumes: [ C, D ]
+
+- name: 'Defragment volume D: with normal priority'
+ community.windows.win_defrag:
+ include_volumes: D
+ priority: normal
+
+- name: Consolidate free space (useful when reducing volumes)
+ community.windows.win_defrag:
+ freespace_consolidation: yes
+'''
+
+RETURN = r'''
+cmd:
+ description: The complete command line used by the module.
+ returned: always
+ type: str
+ sample: defrag.exe /C /V
+rc:
+ description: The return code for the command.
+ returned: always
+ type: int
+ sample: 0
+stdout:
+ description: The standard output from the command.
+ returned: always
+ type: str
+ sample: Success.
+stderr:
+ description: The error output from the command.
+ returned: always
+ type: str
+ sample:
+msg:
+ description: Possible error message on failure.
+ returned: failed
+ type: str
+ sample: Command 'defrag.exe' not found in $env:PATH.
+changed:
+ description: Whether or not any changes were made.
+ returned: always
+ type: bool
+ sample: true
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_dhcp_lease.ps1 b/ansible_collections/community/windows/plugins/modules/win_dhcp_lease.ps1
new file mode 100644
index 00000000..004e16dc
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_dhcp_lease.ps1
@@ -0,0 +1,446 @@
+#!powershell
+
+# Copyright: (c) 2020 VMware, Inc. All Rights Reserved.
+# SPDX-License-Identifier: GPL-3.0-only
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+$spec = @{
+ options = @{
+ type = @{ type = "str"; choices = "reservation", "lease"; default = "reservation" }
+ ip = @{ type = "str" }
+ scope_id = @{ type = "str" }
+ mac = @{ type = "str" }
+ duration = @{ type = "int" }
+ dns_hostname = @{ type = "str"; }
+ dns_regtype = @{ type = "str"; choices = "aptr", "a", "noreg"; default = "aptr" }
+ reservation_name = @{ type = "str"; }
+ description = @{ type = "str"; }
+ state = @{ type = "str"; choices = "absent", "present"; default = "present" }
+ }
+ required_if = @(
+ @("state", "present", @("mac", "ip"), $true),
+ @("state", "absent", @("mac", "ip"), $true)
+ )
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+$check_mode = $module.CheckMode
+
+$type = $module.Params.type
+$ip = $module.Params.ip
+$scope_id = $module.Params.scope_id
+$mac = $module.Params.mac
+$duration = $module.Params.duration
+$dns_hostname = $module.Params.dns_hostname
+$dns_regtype = $module.Params.dns_regtype
+$reservation_name = $module.Params.reservation_name
+$description = $module.Params.description
+$state = $module.Params.state
+
+Function Convert-MacAddress {
+ Param(
+ [string]$mac
+ )
+
+ # Evaluate Length
+ if ($mac.Length -eq 12) {
+ # Insert Dashes
+ $mac = $mac.Insert(2, "-").Insert(5, "-").Insert(8, "-").Insert(11, "-").Insert(14, "-")
+ return $mac
+ }
+ elseif ($mac.Length -eq 17) {
+ # Replace Colons by Dashes
+ return ($mac -replace ':', '-')
+ }
+ else {
+ return $false
+ }
+}
+
+Function Compare-DhcpLease {
+ Param(
+ [PSObject]$Original,
+ [PSObject]$Updated
+ )
+
+ # Compare values that we care about
+ -not (
+ ($Original.AddressState -eq $Updated.AddressState) -and
+ ($Original.IPAddress -eq $Updated.IPAddress) -and
+ ($Original.ScopeId -eq $Updated.ScopeId) -and
+ ($Original.Name -eq $Updated.Name) -and
+ ($Original.Description -eq $Updated.Description)
+ )
+}
+
+Function Convert-ReturnValue {
+ Param(
+ $Object
+ )
+
+ return @{
+ address_state = $Object.AddressState
+ client_id = $Object.ClientId
+ ip_address = $Object.IPAddress.IPAddressToString
+ scope_id = $Object.ScopeId.IPAddressToString
+ name = $Object.Name
+ description = $Object.Description
+ }
+}
+
+# Parse Regtype
+if ($dns_regtype) {
+ Switch ($dns_regtype) {
+ "aptr" { $dns_regtype = "AandPTR"; break }
+ "a" { $dns_regtype = "A"; break }
+ "noreg" { $dns_regtype = "NoRegistration"; break }
+ default { $dns_regtype = "NoRegistration"; break }
+ }
+}
+
+Try {
+ # Import DHCP Server PS Module
+ Import-Module DhcpServer
+}
+Catch {
+ # Couldn't load the DhcpServer Module
+ $module.FailJson("The DhcpServer module failed to load properly: $($_.Exception.Message)", $_)
+}
+
+# Find existing lease by MAC address
+if ($mac) {
+ $mac = Convert-MacAddress -mac $mac
+
+ if ($mac -eq $false) {
+ $module.FailJson("The MAC Address is not properly formatted")
+ }
+ else {
+ $current_lease = Get-DhcpServerv4Scope | Get-DhcpServerv4Lease | Where-Object ClientId -eq $mac
+ }
+}
+
+# Find existing lease by IP address
+if ($ip -and (-not $current_lease)) {
+ $current_lease = Get-DhcpServerv4Scope | Get-DhcpServerv4Lease | Where-Object IPAddress -eq $ip
+}
+
+# Did we find a lease/reservation
+if ($current_lease) {
+ $current_lease_exists = $true
+ $original_lease = $current_lease
+ $module.Diff.before = Convert-ReturnValue -Object $original_lease
+}
+else {
+ $current_lease_exists = $false
+}
+
+# If we found a lease, is it a reservation?
+if ($current_lease_exists -eq $true -and ($current_lease.AddressState -like "*Reservation*")) {
+ $current_lease_reservation = $true
+}
+else {
+ $current_lease_reservation = $false
+}
+
+# State: Absent
+# Ensure the DHCP Lease/Reservation is not present
+if ($state -eq "absent") {
+ # If the lease doesn't exist, our work here is done
+ if ($current_lease_exists -eq $false) {
+ $module.Result.msg = "The lease doesn't exist."
+ }
+ else {
+ # If the lease exists, we need to destroy it
+ if ($current_lease_reservation -eq $true) {
+ # Try to remove reservation
+ Try {
+ $current_lease | Remove-DhcpServerv4Reservation -WhatIf:$check_mode
+ $state_absent_removed = $true
+ }
+ Catch {
+ $state_absent_removed = $false
+ $remove_err = $_
+ }
+ }
+ else {
+ # Try to remove lease
+ Try {
+ $current_lease | Remove-DhcpServerv4Lease -WhatIf:$check_mode
+ $state_absent_removed = $true
+ }
+ Catch {
+ $state_absent_removed = $false
+ $remove_err = $_
+ }
+ }
+
+ # See if we removed the lease/reservation
+ if ($state_absent_removed) {
+ $module.Result.changed = $true
+ }
+ else {
+ $module.Result.lease = Convert-ReturnValue -Object $current_lease
+ $module.FailJson("Unable to remove lease/reservation: $($remove_err.Exception.Message)", $remove_err)
+ }
+ }
+}
+
+# State: Present
+# Ensure the DHCP Lease/Reservation is present, and consistent
+if ($state -eq "present") {
+ # Current lease exists, and is not a reservation
+ if (($current_lease_reservation -eq $false) -and ($current_lease_exists -eq $true)) {
+ if ($type -eq "reservation") {
+ Try {
+ # Update parameters
+ $params = @{ }
+
+ if ($mac) {
+ $params.ClientId = $mac
+ }
+ else {
+ $params.ClientId = $current_lease.ClientId
+ }
+
+ if ($description) {
+ $params.Description = $description
+ }
+ else {
+ $params.Description = $current_lease.Description
+ }
+
+ if ($reservation_name) {
+ $params.Name = $reservation_name
+ }
+ else {
+ $params.Name = "reservation-" + $params.ClientId
+ }
+
+ # Desired type is reservation
+ $current_lease | Add-DhcpServerv4Reservation -WhatIf:$check_mode
+
+ if (-not $check_mode) {
+ $current_reservation = Get-DhcpServerv4Lease -ClientId $params.ClientId -ScopeId $current_lease.ScopeId
+ }
+
+ # Update the reservation with new values
+ $current_reservation | Set-DhcpServerv4Reservation @params -WhatIf:$check_mode
+
+ if (-not $check_mode) {
+ $updated_reservation = Get-DhcpServerv4Lease -ClientId $params.ClientId -ScopeId $current_reservation.ScopeId
+ }
+
+ if (-not $check_mode) {
+ # Compare Values
+ $module.Result.changed = Compare-DhcpLease -Original $original_lease -Updated $updated_reservation
+ $module.Result.lease = Convert-ReturnValue -Object $updated_reservation
+ }
+ else {
+ $module.Result.changed = $true
+ }
+
+ $module.ExitJson()
+ }
+ Catch {
+ $module.FailJson("Could not convert lease to a reservation", $_)
+ }
+ }
+ }
+
+ # Current lease exists, and is a reservation
+ if (($current_lease_reservation -eq $true) -and ($current_lease_exists -eq $true)) {
+ if ($type -eq "lease") {
+ Try {
+ # Desired type is a lease, remove the reservation
+ $current_lease | Remove-DhcpServerv4Reservation -WhatIf:$check_mode
+
+ # Build a new lease object with remnants of the reservation
+ $lease_params = @{
+ ClientId = $original_lease.ClientId
+ IPAddress = $original_lease.IPAddress.IPAddressToString
+ ScopeId = $original_lease.ScopeId.IPAddressToString
+ HostName = $original_lease.HostName
+ AddressState = 'Active'
+ }
+
+ # Create new lease
+ Try {
+ Add-DhcpServerv4Lease @lease_params -WhatIf:$check_mode
+ }
+ Catch {
+ $module.FailJson("Unable to convert the reservation to a lease", $_)
+ }
+
+ # Get the lease we just created
+ if (-not $check_mode) {
+ Try {
+ $new_lease = Get-DhcpServerv4Lease -ClientId $lease_params.ClientId -ScopeId $lease_params.ScopeId
+ }
+ Catch {
+ $module.FailJson("Unable to retreive the newly created lease", $_)
+ }
+ }
+
+ if (-not $check_mode) {
+ $module.Result.lease = Convert-ReturnValue -Object $new_lease
+ }
+
+ $module.Result.changed = $true
+ $module.ExitJson()
+ }
+ Catch {
+ $module.FailJson("Could not convert reservation to lease", $_)
+ }
+ }
+
+ # Already in the desired state
+ if ($type -eq "reservation") {
+
+ # Update parameters
+ $params = @{ }
+
+ if ($mac) {
+ $params.ClientId = $mac
+ }
+ else {
+ $params.ClientId = $current_lease.ClientId
+ }
+
+ if ($description) {
+ $params.Description = $description
+ }
+ else {
+ $params.Description = $current_lease.Description
+ }
+
+ if ($reservation_name) {
+ $params.Name = $reservation_name
+ }
+ else {
+ # Original lease had a null name so let's generate one
+ if ($null -eq $original_lease.Name) {
+ $params.Name = "reservation-" + $original_lease.ClientId
+ }
+ else {
+ $params.Name = $original_lease.Name
+ }
+ }
+
+ # Update the reservation with new values
+ $current_lease | Set-DhcpServerv4Reservation @params -WhatIf:$check_mode
+
+ if (-not $check_mode) {
+ $reservation = Get-DhcpServerv4Lease -ClientId $current_lease.ClientId -ScopeId $current_lease.ScopeId
+ $module.Result.changed = Compare-DhcpLease -Original $original_lease -Updated $reservation
+ $module.Result.lease = Convert-ReturnValue -Object $reservation
+ }
+ else {
+ $module.Result.changed = $true
+ }
+
+ # Return values
+ $module.ExitJson()
+ }
+ }
+
+ # Lease Doesn't Exist - Create
+ if ($current_lease_exists -eq $false) {
+ # Required: Scope ID
+ if (-not $scope_id) {
+ $module.Result.changed = $false
+ $module.FailJson("The scope_id parameter is required for state=present when a lease or reservation doesn't already exist")
+ }
+
+ # Required Parameters
+ $lease_params = @{
+ ClientId = $mac
+ IPAddress = $ip
+ ScopeId = $scope_id
+ AddressState = 'Active'
+ Confirm = $false
+ }
+
+ if ($duration) {
+ $lease_params.LeaseExpiryTime = (Get-Date).AddDays($duration)
+ }
+
+ if ($dns_hostname) {
+ $lease_params.HostName = $dns_hostname
+ }
+
+ if ($dns_regtype) {
+ $lease_params.DnsRR = $dns_regtype
+ }
+
+ if ($description) {
+ $lease_params.Description = $description
+ }
+
+ # Create Lease
+ Try {
+ # Create lease based on parameters
+ Add-DhcpServerv4Lease @lease_params -WhatIf:$check_mode
+
+ # Retreive the lease
+ if (-not $check_mode) {
+ $new_lease = Get-DhcpServerv4Lease -ClientId $mac -ScopeId $scope_id
+ $module.Result.lease = Convert-ReturnValue -Object $new_lease
+ }
+
+ # If lease is the desired type
+ if ($type -eq "lease") {
+ $module.Result.changed = $true
+ $module.ExitJson()
+ }
+ }
+ Catch {
+ # Failed to create lease
+ $module.FailJson("Could not create DHCP lease: $($_.Exception.Message)", $_)
+ }
+
+ # Create Reservation
+ Try {
+ # If reservation is the desired type
+ if ($type -eq "reservation") {
+ if ($reservation_name) {
+ $lease_params.Name = $reservation_name
+ }
+ else {
+ $lease_params.Name = "reservation-" + $mac
+ }
+
+ Try {
+ if ($check_mode) {
+ # In check mode, a lease won't exist for conversion, make one manually
+ Add-DhcpServerv4Reservation -ScopeId $scope_id -ClientId $mac -IPAddress $ip -WhatIf:$check_mode
+ }
+ else {
+ # Convert to Reservation
+ $new_lease | Add-DhcpServerv4Reservation -WhatIf:$check_mode
+ }
+ }
+ Catch {
+ # Failed to create reservation
+ $module.FailJson("Could not create DHCP reservation: $($_.Exception.Message)", $_)
+ }
+
+ if (-not $check_mode) {
+ # Get DHCP reservation object
+ $new_lease = Get-DhcpServerv4Reservation -ClientId $mac -ScopeId $scope_id
+ $module.Result.lease = Convert-ReturnValue -Object $new_lease
+ }
+
+ $module.Result.changed = $true
+ }
+ }
+ Catch {
+ # Failed to create reservation
+ $module.FailJson("Could not create DHCP reservation: $($_.Exception.Message)", $_)
+ }
+ }
+}
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_dhcp_lease.py b/ansible_collections/community/windows/plugins/modules/win_dhcp_lease.py
new file mode 100644
index 00000000..d70bcf2f
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_dhcp_lease.py
@@ -0,0 +1,135 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2020 VMware, Inc. All Rights Reserved.
+# SPDX-License-Identifier: GPL-3.0-only
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_dhcp_lease
+short_description: Manage Windows Server DHCP Leases
+author: Joe Zollo (@joezollo)
+requirements:
+ - This module requires Windows Server 2012 or Newer
+description:
+ - Manage Windows Server DHCP Leases (IPv4 Only)
+ - Adds, Removes and Modifies DHCP Leases and Reservations
+ - Task should be delegated to a Windows DHCP Server
+options:
+ type:
+ description:
+ - The type of DHCP address.
+ - Leases expire as defined by l(duration).
+ - When l(duration) is not specified, the server default is used.
+ - Reservations are permanent.
+ type: str
+ default: reservation
+ choices: [ reservation, lease ]
+ state:
+ description:
+ - Specifies the desired state of the DHCP lease or reservation.
+ type: str
+ default: present
+ choices: [ present, absent ]
+ ip:
+ description:
+ - The IPv4 address of the client server/computer.
+ - This is a required parameter, if l(mac) is not set.
+ - Can be used to identify an existing lease/reservation, instead of l(mac).
+ type: str
+ required: no
+ scope_id:
+ description:
+ - Specifies the scope identifier as defined by the DHCP server.
+ - This is a required parameter, if l(state=present) and the reservation or lease
+ doesn't already exist. Not required if updating an existing lease or reservation.
+ type: str
+ mac:
+ description:
+ - Specifies the client identifier to be set on the IPv4 address.
+ - This is a required parameter, if l(ip) is not set.
+ - Windows clients use the MAC address as the client ID.
+ - Linux and other operating systems can use other types of identifiers.
+ - Can be used to identify an existing lease/reservation, instead of l(ip).
+ type: str
+ duration:
+ description:
+ - Specifies the duration of the DHCP lease in days.
+ - The duration value only applies to l(type=lease).
+ - Defaults to the duration specified by the DHCP server
+ configuration.
+ - Only applicable to l(type=lease).
+ type: int
+ dns_hostname:
+ description:
+ - Specifies the DNS hostname of the client for which the IP address
+ lease is to be added.
+ type: str
+ dns_regtype:
+ description:
+ - Indicates the type of DNS record to be registered by the DHCP.
+ server service for this lease.
+ - l(a) results in an A record being registered.
+ - l(aptr) results in both A and PTR records to be registered.
+ - l(noreg) results in no DNS records being registered.
+ type: str
+ default: aptr
+ choices: [ aptr, a, noreg ]
+ reservation_name:
+ description:
+ - Specifies the name of the reservation being created.
+ - Only applicable to l(type=reservation).
+ type: str
+ description:
+ description:
+ - Specifies the description for reservation being created.
+ - Only applicable to l(type=reservation).
+ type: str
+'''
+
+EXAMPLES = r'''
+- name: Ensure DHCP reservation exists
+ community.windows.win_dhcp_lease:
+ type: reservation
+ ip: 192.168.100.205
+ scope_id: 192.168.100.0
+ mac: 00:B1:8A:D1:5A:1F
+ dns_hostname: "{{ ansible_inventory }}"
+ description: Testing Server
+
+- name: Ensure DHCP lease or reservation does not exist
+ community.windows.win_dhcp_lease:
+ mac: 00:B1:8A:D1:5A:1F
+ state: absent
+
+- name: Ensure DHCP lease or reservation does not exist
+ community.windows.win_dhcp_lease:
+ ip: 192.168.100.205
+ state: absent
+
+- name: Convert DHCP lease to reservation & update description
+ community.windows.win_dhcp_lease:
+ type: reservation
+ ip: 192.168.100.205
+ description: Testing Server
+
+- name: Convert DHCP reservation to lease
+ community.windows.win_dhcp_lease:
+ type: lease
+ ip: 192.168.100.205
+'''
+
+RETURN = r'''
+lease:
+ description: New/Updated DHCP object parameters
+ returned: When l(state=present)
+ type: dict
+ sample:
+ address_state: InactiveReservation
+ client_id: 0a-0b-0c-04-05-aa
+ description: Really Fancy
+ ip_address: 172.16.98.230
+ name: null
+ scope_id: 172.16.98.0
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_disk_facts.ps1 b/ansible_collections/community/windows/plugins/modules/win_disk_facts.ps1
new file mode 100644
index 00000000..af61af52
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_disk_facts.ps1
@@ -0,0 +1,280 @@
+#!powershell
+
+# Copyright: (c) 2017, Marc Tschapek <marc.tschapek@itelligence.de>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#AnsibleRequires -OSVersion 6.2
+
+$ErrorActionPreference = "Stop"
+Set-StrictMode -Version 2.0
+
+$spec = @{
+ options = @{
+ filter = @{
+ type = "list"
+ elements = "str"
+ choices = "physical_disk", "virtual_disk", "win32_disk_drive", "partitions", "volumes"
+ default = "physical_disk", "virtual_disk", "win32_disk_drive", "partitions", "volumes"
+ }
+ }
+ supports_check_mode = $true
+}
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+# Functions
+function Test-Admin {
+ $CurrentUser = New-Object Security.Principal.WindowsPrincipal $([Security.Principal.WindowsIdentity]::GetCurrent())
+ $IsAdmin = $CurrentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
+ return $IsAdmin
+}
+
+# Check admin rights
+if (-not (Test-Admin)) {
+ $module.FailJson("Module was not started with elevated rights")
+}
+
+
+# Create a new result object
+$module.Result.changed = $false
+$module.Result.ansible_facts = @{ ansible_disks = @() }
+
+# Search disks
+try {
+ $disks = Get-Disk
+}
+catch {
+ $module.FailJson("Failed to search the disks on the target: $($_.Exception.Message)", $_)
+}
+
+foreach ($disk in $disks) {
+ $disk_info = @{}
+ if ("physical_disk" -in $module.Params.filter) {
+
+ $pdisk = Get-PhysicalDisk -ErrorAction SilentlyContinue | Where-Object {
+ $_.DeviceId -eq $disk.Number
+
+ }
+ if ($pdisk) {
+ $disk_info["physical_disk"] += @{
+ size = $pdisk.Size
+ allocated_size = $pdisk.AllocatedSize
+ device_id = $pdisk.DeviceId
+ friendly_name = $pdisk.FriendlyName
+ operational_status = $pdisk.OperationalStatus
+ health_status = $pdisk.HealthStatus
+ bus_type = $pdisk.BusType
+ usage_type = $pdisk.Usage
+ supported_usages = $pdisk.SupportedUsages
+ spindle_speed = $pdisk.SpindleSpeed
+ firmware_version = $pdisk.FirmwareVersion
+ physical_location = $pdisk.PhysicalLocation
+ manufacturer = $pdisk.Manufacturer
+ model = $pdisk.Model
+ can_pool = $pdisk.CanPool
+ indication_enabled = $pdisk.IsIndicationEnabled
+ partial = $pdisk.IsPartial
+ serial_number = $pdisk.SerialNumber
+ object_id = $pdisk.ObjectId
+ unique_id = $pdisk.UniqueId
+ }
+ if ([single]"$([System.Environment]::OSVersion.Version.Major).$([System.Environment]::OSVersion.Version.Minor)" -ge 6.3) {
+ $disk_info.physical_disk.media_type = $pdisk.MediaType
+ }
+ if (-not $pdisk.CanPool) {
+ $disk_info.physical_disk.cannot_pool_reason = $pdisk.CannotPoolReason
+ }
+ }
+ if ("virtual_disk" -in $module.Params.filter) {
+ $vdisk = Get-VirtualDisk -PhysicalDisk $pdisk -ErrorAction SilentlyContinue
+ if ($vdisk) {
+ $disk_info["virtual_disk"] += @{
+ size = $vdisk.Size
+ allocated_size = $vdisk.AllocatedSize
+ footprint_on_pool = $vdisk.FootprintOnPool
+ name = $vdisk.name
+ friendly_name = $vdisk.FriendlyName
+ operational_status = $vdisk.OperationalStatus
+ health_status = $vdisk.HealthStatus
+ provisioning_type = $vdisk.ProvisioningType
+ allocation_unit_size = $vdisk.AllocationUnitSize
+ media_type = $vdisk.MediaType
+ parity_layout = $vdisk.ParityLayout
+ access = $vdisk.Access
+ detached_reason = $vdisk.DetachedReason
+ write_cache_size = $vdisk.WriteCacheSize
+ fault_domain_awareness = $vdisk.FaultDomainAwareness
+ inter_leave = $vdisk.InterLeave
+ deduplication_enabled = $vdisk.IsDeduplicationEnabled
+ enclosure_aware = $vdisk.IsEnclosureAware
+ manual_attach = $vdisk.IsManualAttach
+ snapshot = $vdisk.IsSnapshot
+ tiered = $vdisk.IsTiered
+ physical_sector_size = $vdisk.PhysicalSectorSize
+ logical_sector_size = $vdisk.LogicalSectorSize
+ available_copies = $vdisk.NumberOfAvailableCopies
+ columns = $vdisk.NumberOfColumns
+ groups = $vdisk.NumberOfGroups
+ physical_disk_redundancy = $vdisk.PhysicalDiskRedundancy
+ read_cache_size = $vdisk.ReadCacheSize
+ request_no_spof = $vdisk.RequestNoSinglePointOfFailure
+ resiliency_setting_name = $vdisk.ResiliencySettingName
+ object_id = $vdisk.ObjectId
+ unique_id_format = $vdisk.UniqueIdFormat
+ unique_id = $vdisk.UniqueId
+ }
+ }
+ }
+ }
+ if ("win32_disk_drive" -in $module.Params.filter) {
+ $win32_disk_drive = Get-CimInstance -ClassName Win32_DiskDrive -ErrorAction SilentlyContinue | Where-Object {
+ if ($_.SerialNumber) {
+ $_.SerialNumber -eq $disk.SerialNumber
+ }
+ elseif ($disk.UniqueIdFormat -eq 'Vendor Specific') {
+ $_.PNPDeviceID -eq $disk.UniqueId.split(':')[0]
+ }
+ }
+ if ($win32_disk_drive) {
+ $disk_info["win32_disk_drive"] += @{
+ availability = $win32_disk_drive.Availability
+ bytes_per_sector = $win32_disk_drive.BytesPerSector
+ capabilities = $win32_disk_drive.Capabilities
+ capability_descriptions = $win32_disk_drive.CapabilityDescriptions
+ caption = $win32_disk_drive.Caption
+ compression_method = $win32_disk_drive.CompressionMethod
+ config_manager_error_code = $win32_disk_drive.ConfigManagerErrorCode
+ config_manager_user_config = $win32_disk_drive.ConfigManagerUserConfig
+ creation_class_name = $win32_disk_drive.CreationClassName
+ default_block_size = $win32_disk_drive.DefaultBlockSize
+ description = $win32_disk_drive.Description
+ device_id = $win32_disk_drive.DeviceID
+ error_cleared = $win32_disk_drive.ErrorCleared
+ error_description = $win32_disk_drive.ErrorDescription
+ error_methodology = $win32_disk_drive.ErrorMethodology
+ firmware_revision = $win32_disk_drive.FirmwareRevision
+ index = $win32_disk_drive.Index
+ install_date = $win32_disk_drive.InstallDate
+ interface_type = $win32_disk_drive.InterfaceType
+ last_error_code = $win32_disk_drive.LastErrorCode
+ manufacturer = $win32_disk_drive.Manufacturer
+ max_block_size = $win32_disk_drive.MaxBlockSize
+ max_media_size = $win32_disk_drive.MaxMediaSize
+ media_loaded = $win32_disk_drive.MediaLoaded
+ media_type = $win32_disk_drive.MediaType
+ min_block_size = $win32_disk_drive.MinBlockSize
+ model = $win32_disk_drive.Model
+ name = $win32_disk_drive.Name
+ needs_cleaning = $win32_disk_drive.NeedsCleaning
+ number_of_media_supported = $win32_disk_drive.NumberOfMediaSupported
+ partitions = $win32_disk_drive.Partitions
+ pnp_device_id = $win32_disk_drive.PNPDeviceID
+ power_management_capabilities = $win32_disk_drive.PowerManagementCapabilities
+ power_management_supported = $win32_disk_drive.PowerManagementSupported
+ scsi_bus = $win32_disk_drive.SCSIBus
+ scsi_logical_unit = $win32_disk_drive.SCSILogicalUnit
+ scsi_port = $win32_disk_drive.SCSIPort
+ scsi_target_id = $win32_disk_drive.SCSITargetId
+ sectors_per_track = $win32_disk_drive.SectorsPerTrack
+ serial_number = $win32_disk_drive.SerialNumber
+ signature = $win32_disk_drive.Signature
+ size = $win32_disk_drive.Size
+ status = $win32_disk_drive.status
+ status_info = $win32_disk_drive.StatusInfo
+ system_creation_class_name = $win32_disk_drive.SystemCreationClassName
+ system_name = $win32_disk_drive.SystemName
+ total_cylinders = $win32_disk_drive.TotalCylinders
+ total_heads = $win32_disk_drive.TotalHeads
+ total_sectors = $win32_disk_drive.TotalSectors
+ total_tracks = $win32_disk_drive.TotalTracks
+ tracks_per_cylinder = $win32_disk_drive.TracksPerCylinder
+ }
+ }
+ }
+ $disk_info.number = $disk.Number
+ $disk_info.size = $disk.Size
+ $disk_info.bus_type = $disk.BusType
+ $disk_info.friendly_name = $disk.FriendlyName
+ $disk_info.partition_style = $disk.PartitionStyle
+ $disk_info.partition_count = $disk.NumberOfPartitions
+ $disk_info.operational_status = $disk.OperationalStatus
+ $disk_info.sector_size = $disk.PhysicalSectorSize
+ $disk_info.read_only = $disk.IsReadOnly
+ $disk_info.bootable = $disk.IsBoot
+ $disk_info.system_disk = $disk.IsSystem
+ $disk_info.clustered = $disk.IsClustered
+ $disk_info.manufacturer = $disk.Manufacturer
+ $disk_info.model = $disk.Model
+ $disk_info.firmware_version = $disk.FirmwareVersion
+ $disk_info.location = $disk.Location
+ $disk_info.serial_number = $disk.SerialNumber
+ $disk_info.unique_id = $disk.UniqueId
+ $disk_info.guid = $disk.Guid
+ $disk_info.path = $disk.Path
+ if ("partitions" -in $module.Params.filter -or "volumes" -in $module.Params.filter) {
+ $parts = Get-Partition -DiskNumber $($disk.Number) -ErrorAction SilentlyContinue
+ if ($parts) {
+ $disk_info["partitions"] += @()
+ foreach ($part in $parts) {
+ $partition_info = @{
+ number = $part.PartitionNumber
+ size = $part.Size
+ type = $part.Type
+ drive_letter = $part.DriveLetter
+ transition_state = $part.TransitionState
+ offset = $part.Offset
+ hidden = $part.IsHidden
+ shadow_copy = $part.IsShadowCopy
+ guid = $part.Guid
+ access_paths = $part.AccessPaths
+ }
+ if ($disks.PartitionStyle -eq "GPT") {
+ $partition_info.gpt_type = $part.GptType
+ $partition_info.no_default_driveletter = $part.NoDefaultDriveLetter
+ }
+ elseif ($disks.PartitionStyle -eq "MBR") {
+ $partition_info.mbr_type = $part.MbrType
+ $partition_info.active = $part.IsActive
+ }
+ if ("volumes" -in $module.Params.filter) {
+ $vols = Get-Volume -Partition $part -ErrorAction SilentlyContinue
+ if ($vols) {
+ $partition_info["volumes"] += @()
+ foreach ($vol in $vols) {
+ $volume_info = @{
+ size = $vol.Size
+ size_remaining = $vol.SizeRemaining
+ type = $vol.FileSystem
+ label = $vol.FileSystemLabel
+ health_status = $vol.HealthStatus
+ drive_type = $vol.DriveType
+ object_id = $vol.ObjectId
+ path = $vol.Path
+ }
+ if ([System.Environment]::OSVersion.Version.Major -ge 10) {
+ $volume_info.allocation_unit_size = $vol.AllocationUnitSize
+ }
+ else {
+ $volPath = ($vol.Path.TrimStart("\\?\")).TrimEnd("\")
+ $BlockSize = (
+ Get-CimInstance `
+ -Query "SELECT BlockSize FROM Win32_Volume WHERE DeviceID like '%$volPath%'" `
+ -ErrorAction SilentlyContinue | Select-Object BlockSize).BlockSize
+ $volume_info.allocation_unit_size = $BlockSize
+ }
+ $partition_info.volumes += $volume_info
+ }
+ }
+ }
+ $disk_info.partitions += $partition_info
+ }
+ }
+ }
+ $module.Result.ansible_facts.ansible_disks += $disk_info
+}
+
+# Sort by disk number property
+$module.Result.ansible_facts.ansible_disks = @() + ($module.Result.ansible_facts.ansible_disks | Sort-Object -Property { $_.Number })
+
+# Return result
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_disk_facts.py b/ansible_collections/community/windows/plugins/modules/win_disk_facts.py
new file mode 100644
index 00000000..a303e571
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_disk_facts.py
@@ -0,0 +1,902 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2017, Marc Tschapek <marc.tschapek@itelligence.de>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_disk_facts
+short_description: Show the attached disks and disk information of the target host
+description:
+ - With the module you can retrieve and output detailed information about the attached disks of the target and
+ its volumes and partitions if existent.
+requirements:
+ - Windows 8.1 / Windows 2012 (NT 6.2)
+notes:
+ - In order to understand all the returned properties and values please visit the following site and open the respective MSFT class
+ U(https://msdn.microsoft.com/en-us/library/windows/desktop/hh830612.aspx)
+author:
+ - Marc Tschapek (@marqelme)
+options:
+ filter:
+ description:
+ - Allows to filter returned facts by type of disk information.
+ - If volumes are selected partitions will be returned as well.
+ type: list
+ elements: str
+ choices: [ physical_disk, virtual_disk, win32_disk_drive, partitions, volumes ]
+ default: [ physical_disk, virtual_disk, win32_disk_drive, partitions, volumes ]
+ version_added: 1.9.0
+'''
+
+EXAMPLES = r'''
+- name: Get disk facts
+ community.windows.win_disk_facts:
+
+- name: Output first disk size
+ debug:
+ var: ansible_facts.disks[0].size
+
+- name: Convert first system disk into various formats
+ debug:
+ msg: '{{ disksize_gib }} vs {{ disksize_gib_human }}'
+ vars:
+ # Get first system disk
+ disk: '{{ ansible_facts.disks|selectattr("system_disk")|first }}'
+
+ # Show disk size in Gibibytes
+ disksize_gib_human: '{{ disk.size|filesizeformat(true) }}' # returns "223.6 GiB" (human readable)
+ disksize_gib: '{{ (disk.size/1024|pow(3))|round|int }} GiB' # returns "224 GiB" (value in GiB)
+
+ # Show disk size in Gigabytes
+ disksize_gb_human: '{{ disk.size|filesizeformat }}' # returns "240.1 GB" (human readable)
+ disksize_gb: '{{ (disk.size/1000|pow(3))|round|int }} GB' # returns "240 GB" (value in GB)
+
+- name: Output second disk serial number
+ debug:
+ var: ansible_facts.disks[1].serial_number
+
+- name: get disk physical_disk and partition facts on the target
+ win_disk_facts:
+ filter:
+ - physical_disk
+ - partitions
+'''
+
+RETURN = r'''
+ansible_facts:
+ description: Dictionary containing all the detailed information about the disks of the target.
+ returned: always
+ type: complex
+ contains:
+ ansible_disks:
+ description: Detailed information about one particular disk.
+ returned: if disks were found
+ type: list
+ contains:
+ number:
+ description: Disk number of the particular disk.
+ returned: always
+ type: int
+ sample: 0
+ size:
+ description: Size in bytes of the particular disk.
+ returned: always
+ type: int
+ sample: 227727638528
+ bus_type:
+ description: Bus type of the particular disk.
+ returned: always
+ type: str
+ sample: "SCSI"
+ friendly_name:
+ description: Friendly name of the particular disk.
+ returned: always
+ type: str
+ sample: "Red Hat VirtIO SCSI Disk Device"
+ partition_style:
+ description: Partition style of the particular disk.
+ returned: always
+ type: str
+ sample: "MBR"
+ partition_count:
+ description: Number of partitions on the particular disk.
+ returned: always
+ type: int
+ sample: 4
+ operational_status:
+ description: Operational status of the particular disk.
+ returned: always
+ type: str
+ sample: "Online"
+ sector_size:
+ description: Sector size in bytes of the particular disk.
+ returned: always
+ type: int
+ sample: 4096
+ read_only:
+ description: Read only status of the particular disk.
+ returned: always
+ type: bool
+ sample: true
+ bootable:
+ description: Information whether the particular disk is a bootable disk.
+ returned: always
+ type: bool
+ sample: false
+ system_disk:
+ description: Information whether the particular disk is a system disk.
+ returned: always
+ type: bool
+ sample: true
+ clustered:
+ description: Information whether the particular disk is clustered (part of a failover cluster).
+ returned: always
+ type: bool
+ sample: false
+ manufacturer:
+ description: Manufacturer of the particular disk.
+ returned: always
+ type: str
+ sample: "Red Hat"
+ model:
+ description: Model specification of the particular disk.
+ returned: always
+ type: str
+ sample: "VirtIO"
+ firmware_version:
+ description: Firmware version of the particular disk.
+ returned: always
+ type: str
+ sample: "0001"
+ location:
+ description: Location of the particular disk on the target.
+ returned: always
+ type: str
+ sample: "PCIROOT(0)#PCI(0400)#SCSI(P00T00L00)"
+ serial_number:
+ description: Serial number of the particular disk on the target.
+ returned: always
+ type: str
+ sample: "b62beac80c3645e5877f"
+ unique_id:
+ description: Unique ID of the particular disk on the target.
+ returned: always
+ type: str
+ sample: "3141463431303031"
+ guid:
+ description: GUID of the particular disk on the target.
+ returned: if existent
+ type: str
+ sample: "{efa5f928-57b9-47fc-ae3e-902e85fbe77f}"
+ path:
+ description: Path of the particular disk on the target.
+ returned: always
+ type: str
+ sample: "\\\\?\\scsi#disk&ven_red_hat&prod_virtio#4&23208fd0&1&000000#{<id>}"
+ partitions:
+ description: Detailed information about one particular partition on the specified disk.
+ returned: if existent
+ type: list
+ contains:
+ number:
+ description: Number of the particular partition.
+ returned: always
+ type: int
+ sample: 1
+ size:
+ description:
+ - Size in bytes of the particular partition.
+ returned: always
+ type: int
+ sample: 838860800
+ type:
+ description: Type of the particular partition.
+ returned: always
+ type: str
+ sample: "IFS"
+ gpt_type:
+ description: gpt type of the particular partition.
+ returned: if partition_style property of the particular disk has value "GPT"
+ type: str
+ sample: "{e3c9e316-0b5c-4db8-817d-f92df00215ae}"
+ no_default_driveletter:
+ description: Information whether the particular partition has a default drive letter or not.
+ returned: if partition_style property of the particular disk has value "GPT"
+ type: bool
+ sample: true
+ mbr_type:
+ description: mbr type of the particular partition.
+ returned: if partition_style property of the particular disk has value "MBR"
+ type: int
+ sample: 7
+ active:
+ description: Information whether the particular partition is an active partition or not.
+ returned: if partition_style property of the particular disk has value "MBR"
+ type: bool
+ sample: true
+ drive_letter:
+ description: Drive letter of the particular partition.
+ returned: if existent
+ type: str
+ sample: "C"
+ transition_state:
+ description: Transition state of the particular partition.
+ returned: always
+ type: int
+ sample: 1
+ offset:
+ description: Offset of the particular partition.
+ returned: always
+ type: int
+ sample: 368050176
+ hidden:
+ description: Information whether the particular partition is hidden or not.
+ returned: always
+ type: bool
+ sample: true
+ shadow_copy:
+ description: Information whether the particular partition is a shadow copy of another partition.
+ returned: always
+ type: bool
+ sample: false
+ guid:
+ description: GUID of the particular partition.
+ returned: if existent
+ type: str
+ sample: "{302e475c-6e64-4674-a8e2-2f1c7018bf97}"
+ access_paths:
+ description: Access paths of the particular partition.
+ returned: if existent
+ type: str
+ sample: "\\\\?\\Volume{85bdc4a8-f8eb-11e6-80fa-806e6f6e6963}\\"
+ volumes:
+ description: Detailed information about one particular volume on the specified partition.
+ returned: if existent
+ type: list
+ contains:
+ size:
+ description:
+ - Size in bytes of the particular volume.
+ returned: always
+ type: int
+ sample: 838856704
+ size_remaining:
+ description:
+ - Remaining size in bytes of the particular volume.
+ returned: always
+ type: int
+ sample: 395620352
+ type:
+ description: File system type of the particular volume.
+ returned: always
+ type: str
+ sample: "NTFS"
+ label:
+ description: File system label of the particular volume.
+ returned: always
+ type: str
+ sample: "System Reserved"
+ health_status:
+ description: Health status of the particular volume.
+ returned: always
+ type: str
+ sample: "Healthy"
+ drive_type:
+ description: Drive type of the particular volume.
+ returned: always
+ type: str
+ sample: "Fixed"
+ allocation_unit_size:
+ description: Allocation unit size in bytes of the particular volume.
+ returned: always
+ type: int
+ sample: 4096
+ object_id:
+ description: Object ID of the particular volume.
+ returned: always
+ type: str
+ sample: "\\\\?\\Volume{85bdc4a9-f8eb-11e6-80fa-806e6f6e6963}\\"
+ path:
+ description: Path of the particular volume.
+ returned: always
+ type: str
+ sample: "\\\\?\\Volume{85bdc4a9-f8eb-11e6-80fa-806e6f6e6963}\\"
+ physical_disk:
+ description: Detailed information about physical disk properties of the particular disk.
+ returned: if existent
+ type: complex
+ contains:
+ media_type:
+ description: Media type of the particular physical disk.
+ returned: always
+ type: str
+ sample: "UnSpecified"
+ size:
+ description:
+ - Size in bytes of the particular physical disk.
+ returned: always
+ type: int
+ sample: 240057409536
+ allocated_size:
+ description:
+ - Allocated size in bytes of the particular physical disk.
+ returned: always
+ type: int
+ sample: 240057409536
+ device_id:
+ description: Device ID of the particular physical disk.
+ returned: always
+ type: str
+ sample: "0"
+ friendly_name:
+ description: Friendly name of the particular physical disk.
+ returned: always
+ type: str
+ sample: "PhysicalDisk0"
+ operational_status:
+ description: Operational status of the particular physical disk.
+ returned: always
+ type: str
+ sample: "OK"
+ health_status:
+ description: Health status of the particular physical disk.
+ returned: always
+ type: str
+ sample: "Healthy"
+ bus_type:
+ description: Bus type of the particular physical disk.
+ returned: always
+ type: str
+ sample: "SCSI"
+ usage_type:
+ description: Usage type of the particular physical disk.
+ returned: always
+ type: str
+ sample: "Auto-Select"
+ supported_usages:
+ description: Supported usage types of the particular physical disk.
+ returned: always
+ type: complex
+ contains:
+ Count:
+ description: Count of supported usage types.
+ returned: always
+ type: int
+ sample: 5
+ value:
+ description: List of supported usage types.
+ returned: always
+ type: str
+ sample: "Auto-Select, Hot Spare"
+ spindle_speed:
+ description: Spindle speed in rpm of the particular physical disk.
+ returned: always
+ type: int
+ sample: 4294967295
+ physical_location:
+ description: Physical location of the particular physical disk.
+ returned: always
+ type: str
+ sample: "Integrated : Adapter 3 : Port 0 : Target 0 : LUN 0"
+ manufacturer:
+ description: Manufacturer of the particular physical disk.
+ returned: always
+ type: str
+ sample: "SUSE"
+ model:
+ description: Model of the particular physical disk.
+ returned: always
+ type: str
+ sample: "Xen Block"
+ can_pool:
+ description: Information whether the particular physical disk can be added to a storage pool.
+ returned: always
+ type: bool
+ sample: false
+ cannot_pool_reason:
+ description: Information why the particular physical disk can not be added to a storage pool.
+ returned: if can_pool property has value false
+ type: str
+ sample: "Insufficient Capacity"
+ indication_enabled:
+ description: Information whether indication is enabled for the particular physical disk.
+ returned: always
+ type: bool
+ sample: true
+ partial:
+ description: Information whether the particular physical disk is partial.
+ returned: always
+ type: bool
+ sample: false
+ serial_number:
+ description: Serial number of the particular physical disk.
+ returned: always
+ type: str
+ sample: "b62beac80c3645e5877f"
+ object_id:
+ description: Object ID of the particular physical disk.
+ returned: always
+ type: str
+ sample: '{1}\\\\HOST\\root/Microsoft/Windows/Storage/Providers_v2\\SPACES_PhysicalDisk.ObjectId=\"{<object_id>}:PD:{<pd>}\"'
+ unique_id:
+ description: Unique ID of the particular physical disk.
+ returned: always
+ type: str
+ sample: "3141463431303031"
+ virtual_disk:
+ description: Detailed information about virtual disk properties of the particular disk.
+ returned: if existent
+ type: complex
+ contains:
+ size:
+ description:
+ - Size in bytes of the particular virtual disk.
+ returned: always
+ type: int
+ sample: 240057409536
+ allocated_size:
+ description:
+ - Allocated size in bytes of the particular virtual disk.
+ returned: always
+ type: int
+ sample: 240057409536
+ footprint_on_pool:
+ description:
+ - Footprint on pool in bytes of the particular virtual disk.
+ returned: always
+ type: int
+ sample: 240057409536
+ name:
+ description: Name of the particular virtual disk.
+ returned: always
+ type: str
+ sample: "vDisk1"
+ friendly_name:
+ description: Friendly name of the particular virtual disk.
+ returned: always
+ type: str
+ sample: "Prod2 Virtual Disk"
+ operational_status:
+ description: Operational status of the particular virtual disk.
+ returned: always
+ type: str
+ sample: "OK"
+ health_status:
+ description: Health status of the particular virtual disk.
+ returned: always
+ type: str
+ sample: "Healthy"
+ provisioning_type:
+ description: Provisioning type of the particular virtual disk.
+ returned: always
+ type: str
+ sample: "Thin"
+ allocation_unit_size:
+ description: Allocation unit size in bytes of the particular virtual disk.
+ returned: always
+ type: int
+ sample: 4096
+ media_type:
+ description: Media type of the particular virtual disk.
+ returned: always
+ type: str
+ sample: "Unspecified"
+ parity_layout:
+ description: Parity layout of the particular virtual disk.
+ returned: if existent
+ type: int
+ sample: 1
+ access:
+ description: Access of the particular virtual disk.
+ returned: always
+ type: str
+ sample: "Read/Write"
+ detached_reason:
+ description: Detached reason of the particular virtual disk.
+ returned: always
+ type: str
+ sample: "None"
+ write_cache_size:
+ description: Write cache size in byte of the particular virtual disk.
+ returned: always
+ type: int
+ sample: 100
+ fault_domain_awareness:
+ description: Fault domain awareness of the particular virtual disk.
+ returned: always
+ type: str
+ sample: "PhysicalDisk"
+ inter_leave:
+ description:
+ - Inter leave in bytes of the particular virtual disk.
+ returned: always
+ type: int
+ sample: 102400
+ deduplication_enabled:
+ description: Information whether deduplication is enabled for the particular virtual disk.
+ returned: always
+ type: bool
+ sample: true
+ enclosure_aware:
+ description: Information whether the particular virtual disk is enclosure aware.
+ returned: always
+ type: bool
+ sample: false
+ manual_attach:
+ description: Information whether the particular virtual disk is manual attached.
+ returned: always
+ type: bool
+ sample: true
+ snapshot:
+ description: Information whether the particular virtual disk is a snapshot.
+ returned: always
+ type: bool
+ sample: false
+ tiered:
+ description: Information whether the particular virtual disk is tiered.
+ returned: always
+ type: bool
+ sample: true
+ physical_sector_size:
+ description: Physical sector size in bytes of the particular virtual disk.
+ returned: always
+ type: int
+ sample: 4096
+ logical_sector_size:
+ description: Logical sector size in byte of the particular virtual disk.
+ returned: always
+ type: int
+ sample: 512
+ available_copies:
+ description: Number of the available copies of the particular virtual disk.
+ returned: if existent
+ type: int
+ sample: 1
+ columns:
+ description: Number of the columns of the particular virtual disk.
+ returned: always
+ type: int
+ sample: 2
+ groups:
+ description: Number of the groups of the particular virtual disk.
+ returned: always
+ type: int
+ sample: 1
+ physical_disk_redundancy:
+ description: Type of the physical disk redundancy of the particular virtual disk.
+ returned: always
+ type: int
+ sample: 1
+ read_cache_size:
+ description: Read cache size in byte of the particular virtual disk.
+ returned: always
+ type: int
+ sample: 0
+ request_no_spof:
+ description: Information whether the particular virtual disk requests no single point of failure.
+ returned: always
+ type: bool
+ sample: true
+ resiliency_setting_name:
+ description: Type of the physical disk redundancy of the particular virtual disk.
+ returned: always
+ type: int
+ sample: 1
+ object_id:
+ description: Object ID of the particular virtual disk.
+ returned: always
+ type: str
+ sample: '{1}\\\\HOST\\root/Microsoft/Windows/Storage/Providers_v2\\SPACES_VirtualDisk.ObjectId=\"{<object_id>}:VD:{<vd>}\"'
+ unique_id:
+ description: Unique ID of the particular virtual disk.
+ returned: always
+ type: str
+ sample: "260542E4C6B01D47A8FA7630FD90FFDE"
+ unique_id_format:
+ description: Unique ID format of the particular virtual disk.
+ returned: always
+ type: str
+ sample: "Vendor Specific"
+ win32_disk_drive:
+ description: Representation of the Win32_DiskDrive class.
+ returned: if existent
+ type: complex
+ contains:
+ availability:
+ description: Availability and status of the device.
+ returned: always
+ type: int
+ bytes_per_sector:
+ description: Number of bytes in each sector for the physical disk drive.
+ returned: always
+ type: int
+ sample: 512
+ capabilities:
+ description:
+ - Array of capabilities of the media access device.
+ - For example, the device may support random access (3), removable media (7), and automatic cleaning (9).
+ returned: always
+ type: list
+ sample:
+ - 3
+ - 4
+ capability_descriptions:
+ description:
+ - List of more detailed explanations for any of the access device features indicated in the Capabilities array.
+ - Note, each entry of this array is related to the entry in the Capabilities array that is located at the same index.
+ returned: always
+ type: list
+ sample:
+ - Random Access
+ - Supports Writing
+ caption:
+ description: Short description of the object.
+ returned: always
+ type: str
+ sample: VMware Virtual disk SCSI Disk Device
+ compression_method:
+ description: Algorithm or tool used by the device to support compression.
+ returned: always
+ type: str
+ sample: Compressed
+ config_manager_error_code:
+ description: Windows Configuration Manager error code.
+ returned: always
+ type: int
+ sample: 0
+ config_manager_user_config:
+ description: If True, the device is using a user-defined configuration.
+ returned: always
+ type: bool
+ sample: true
+ creation_class_name:
+ description:
+ - Name of the first concrete class to appear in the inheritance chain used in the creation of an instance.
+ - When used with the other key properties of the class, the property allows all instances of this class
+ - and its subclasses to be uniquely identified.
+ returned: always
+ type: str
+ sample: Win32_DiskDrive
+ default_block_size:
+ description: Default block size, in bytes, for this device.
+ returned: always
+ type: int
+ sample: 512
+ description:
+ description: Description of the object.
+ returned: always
+ type: str
+ sample: Disk drive
+ device_id:
+ description: Unique identifier of the disk drive with other devices on the system.
+ returned: always
+ type: str
+ sample: "\\\\.\\PHYSICALDRIVE0"
+ error_cleared:
+ description: If True, the error reported in LastErrorCode is now cleared.
+ returned: always
+ type: bool
+ sample: true
+ error_description:
+ description:
+ - More information about the error recorded in LastErrorCode,
+ - and information on any corrective actions that may be taken.
+ returned: always
+ type: str
+ error_methodology:
+ description: Type of error detection and correction supported by this device.
+ returned: always
+ type: str
+ firmware_revision:
+ description: Revision for the disk drive firmware that is assigned by the manufacturer.
+ returned: always
+ type: str
+ sample: 1.0
+ index:
+ description:
+ - Physical drive number of the given drive.
+ - This property is filled by the STORAGE_DEVICE_NUMBER structure returned from the IOCTL_STORAGE_GET_DEVICE_NUMBER control code
+ - A value of 0xffffffff indicates that the given drive does not map to a physical drive.
+ returned: always
+ type: int
+ sample: 0
+ install_date:
+ description: Date and time the object was installed. This property does not need a value to indicate that the object is installed.
+ returned: always
+ type: str
+ interface_type:
+ description: Interface type of physical disk drive.
+ returned: always
+ type: str
+ sample: SCSI
+ last_error_code:
+ description: Last error code reported by the logical device.
+ returned: always
+ type: int
+ manufacturer:
+ description: Name of the disk drive manufacturer.
+ returned: always
+ type: str
+ sample: Seagate
+ max_block_size:
+ description: Maximum block size, in bytes, for media accessed by this device.
+ returned: always
+ type: int
+ max_media_size:
+ description: Maximum media size, in kilobytes, of media supported by this device.
+ returned: always
+ type: int
+ media_loaded:
+ description:
+ - If True, the media for a disk drive is loaded, which means that the device has a readable file system and is accessible.
+ - For fixed disk drives, this property will always be TRUE.
+ returned: always
+ type: bool
+ sample: true
+ media_type:
+ description: Type of media used or accessed by this device.
+ returned: always
+ type: str
+ sample: Fixed hard disk media
+ min_block_size:
+ description: Minimum block size, in bytes, for media accessed by this device.
+ returned: always
+ type: int
+ model:
+ description: Manufacturer's model number of the disk drive.
+ returned: always
+ type: str
+ sample: ST32171W
+ name:
+ description: Label by which the object is known. When subclassed, the property can be overridden to be a key property.
+ returned: always
+ type: str
+ sample: \\\\.\\PHYSICALDRIVE0
+ needs_cleaning:
+ description:
+ - If True, the media access device needs cleaning.
+ - Whether manual or automatic cleaning is possible is indicated in the Capabilities property.
+ returned: always
+ type: bool
+ number_of_media_supported:
+ description:
+ - Maximum number of media which can be supported or inserted
+ - (when the media access device supports multiple individual media).
+ returned: always
+ type: int
+ partitions:
+ description: Number of partitions on this physical disk drive that are recognized by the operating system.
+ returned: always
+ type: int
+ sample: 3
+ pnp_device_id:
+ description: Windows Plug and Play device identifier of the logical device.
+ returned: always
+ type: str
+ sample: "SCSI\\DISK&VEN_VMWARE&PROD_VIRTUAL_DISK\\5&1982005&0&000000"
+ power_management_capabilities:
+ description: Array of the specific power-related capabilities of a logical device.
+ returned: always
+ type: list
+ power_management_supported:
+ description:
+ - If True, the device can be power-managed (can be put into suspend mode, and so on).
+ - The property does not indicate that power management features are currently enabled,
+ - only that the logical device is capable of power management.
+ returned: always
+ type: bool
+ scsi_bus:
+ description: SCSI bus number of the disk drive.
+ returned: always
+ type: int
+ sample: 0
+ scsi_logical_unit:
+ description: SCSI logical unit number (LUN) of the disk drive.
+ returned: always
+ type: int
+ sample: 0
+ scsi_port:
+ description: SCSI port number of the disk drive.
+ returned: always
+ type: int
+ sample: 0
+ scsi_target_id:
+ description: SCSI identifier number of the disk drive.
+ returned: always
+ type: int
+ sample: 0
+ sectors_per_track:
+ description: Number of sectors in each track for this physical disk drive.
+ returned: always
+ type: int
+ sample: 63
+ serial_number:
+ description: Number allocated by the manufacturer to identify the physical media.
+ returned: always
+ type: str
+ sample: 6000c298f34101b38cb2b2508926b9de
+ signature:
+ description: Disk identification. This property can be used to identify a shared resource.
+ returned: always
+ type: int
+ size:
+ description:
+ - Size of the disk drive. It is calculated by multiplying the total number of cylinders, tracks in each cylinder,
+ - sectors in each track, and bytes in each sector.
+ returned: always
+ type: int
+ sample: 53686402560
+ status:
+ description:
+ - Current status of the object. Various operational and nonoperational statuses can be defined.
+ - 'Operational statuses include: "OK", "Degraded", and "Pred Fail"'
+ - (an element, such as a SMART-enabled hard disk drive, may be functioning properly but predicting a failure in the near future).
+ - 'Nonoperational statuses include: "Error", "Starting", "Stopping", and "Service".'
+ - '"Service", could apply during mirror-resilvering of a disk, reload of a user permissions list, or other administrative work.'
+ - Not all such work is online, yet the managed element is neither "OK" nor in one of the other states.
+ returned: always
+ type: str
+ sample: OK
+ status_info:
+ description:
+ - State of the logical device. If this property does not apply to the logical device, the value 5 (Not Applicable) should be used.
+ returned: always
+ type: int
+ system_creation_class_name:
+ description: Value of the scoping computer's CreationClassName property.
+ returned: always
+ type: str
+ sample: Win32_ComputerSystem
+ system_name:
+ description: Name of the scoping system.
+ returned: always
+ type: str
+ sample: WILMAR-TEST-123
+ total_cylinders:
+ description:
+ - Total number of cylinders on the physical disk drive.
+ - 'Note: the value for this property is obtained through extended functions of BIOS interrupt 13h.'
+ - The value may be inaccurate if the drive uses a translation scheme to support high-capacity disk sizes.
+ - Consult the manufacturer for accurate drive specifications.
+ returned: always
+ type: int
+ sample: 6527
+ total_heads:
+ description:
+ - Total number of heads on the disk drive.
+ - 'Note: the value for this property is obtained through extended functions of BIOS interrupt 13h.'
+ - The value may be inaccurate if the drive uses a translation scheme to support high-capacity disk sizes.
+ - Consult the manufacturer for accurate drive specifications.
+ returned: always
+ type: int
+ sample: 255
+ total_sectors:
+ description:
+ - Total number of sectors on the physical disk drive.
+ - 'Note: the value for this property is obtained through extended functions of BIOS interrupt 13h.'
+ - The value may be inaccurate if the drive uses a translation scheme to support high-capacity disk sizes.
+ - Consult the manufacturer for accurate drive specifications.
+ returned: always
+ type: int
+ sample: 104856255
+ total_tracks:
+ description:
+ - Total number of tracks on the physical disk drive.
+ - 'Note: the value for this property is obtained through extended functions of BIOS interrupt 13h.'
+ - The value may be inaccurate if the drive uses a translation scheme to support high-capacity disk sizes.
+ - Consult the manufacturer for accurate drive specifications.
+ returned: always
+ type: int
+ sample: 1664385
+ tracks_per_cylinder:
+ description:
+ - Number of tracks in each cylinder on the physical disk drive.
+ - 'Note: the value for this property is obtained through extended functions of BIOS interrupt 13h.'
+ - The value may be inaccurate if the drive uses a translation scheme to support high-capacity disk sizes.
+ - Consult the manufacturer for accurate drive specifications.
+ returned: always
+ type: int
+ sample: 255
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_disk_image.ps1 b/ansible_collections/community/windows/plugins/modules/win_disk_image.ps1
new file mode 100644
index 00000000..99ef3b5f
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_disk_image.ps1
@@ -0,0 +1,79 @@
+#!powershell
+
+# Copyright: (c) 2017, Red Hat, Inc.
+# 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"
+Set-StrictMode -Version 2
+
+If (-not (Get-Command Get-DiskImage -ErrorAction SilentlyContinue)) {
+ Fail-Json -message "win_disk_image requires Windows 8+ or Windows Server 2012+"
+}
+
+$parsed_args = Parse-Args $args -supports_check_mode $true
+
+$result = @{ changed = $false }
+
+$image_path = Get-AnsibleParam $parsed_args "image_path" -failifempty $result
+$state = Get-AnsibleParam $parsed_args "state" -default "present" -validateset "present", "absent"
+$check_mode = Get-AnsibleParam $parsed_args "_ansible_check_mode" -default $false
+
+$di = Get-DiskImage $image_path
+
+If ($state -eq "present") {
+ If (-not $di.Attached) {
+ $result.changed = $true
+
+ If (-not $check_mode) {
+ $di = Mount-DiskImage $image_path -PassThru
+
+ # the actual mount is async, so the CIMInstance result may not immediately contain the data we need
+ $retry_count = 0
+ While (-not $di.Attached -and $retry_count -lt 5) {
+ Start-Sleep -Seconds 1 > $null
+ $di = $di | Get-DiskImage
+ $retry_count++
+ }
+
+ If (-not $di.Attached) {
+ Fail-Json $result -message "Timed out waiting for disk to attach"
+ }
+ }
+ }
+
+ # FUTURE: detect/handle "ejected" ISOs
+ # FUTURE: support explicit drive letter and NTFS in-volume mountpoints.
+ # VHDs don't always auto-assign, and other system settings can prevent automatic assignment
+
+ If ($di.Attached) {
+ # only try to get the mount_path if the disk is attached (
+ If ($di.StorageType -eq 1) {
+ # ISO, we can get the mountpoint directly from Get-Volume
+ $drive_letters = ($di | Get-Volume).DriveLetter
+ }
+ ElseIf ($di.StorageType -in @(2, 3)) {
+ # VHD/VHDX, need Get-Disk + Get-Partition to discover mountpoint
+ $drive_letters = ($di | Get-Disk | Get-Partition).DriveLetter
+ }
+ # remove any null entries (no drive letter)
+ $drive_letters = $drive_letters | Where-Object { $_ }
+
+ If (-not $drive_letters) {
+ Fail-Json -message "Unable to retrieve drive letter from mounted image"
+ }
+
+ $result.mount_paths = @($drive_letters | ForEach-Object { "$($_):\" })
+ }
+}
+ElseIf ($state -eq "absent") {
+ If ($di.Attached) {
+ $result.changed = $true
+ If (-not $check_mode) {
+ Dismount-DiskImage $image_path > $null
+ }
+ }
+}
+
+Exit-Json $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_disk_image.py b/ansible_collections/community/windows/plugins/modules/win_disk_image.py
new file mode 100644
index 00000000..e2037da3
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_disk_image.py
@@ -0,0 +1,57 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2017, Red Hat, Inc.
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+module: win_disk_image
+short_description: Manage ISO/VHD/VHDX mounts on Windows hosts
+description:
+ - Manages mount behavior for a specified ISO, VHD, or VHDX image on a Windows host. When C(state) is C(present),
+ the image will be mounted under a system-assigned drive letter, which will be returned in the C(mount_path) value
+ of the module result.
+ - Requires Windows 8+ or Windows Server 2012+.
+options:
+ image_path:
+ description:
+ - Path to an ISO, VHD, or VHDX image on the target Windows host (the file cannot reside on a network share)
+ type: str
+ required: yes
+ state:
+ description:
+ - Whether the image should be present as a drive-letter mount or not.
+ type: str
+ choices: [ absent, present ]
+ default: present
+author:
+ - Matt Davis (@nitzmahone)
+'''
+
+EXAMPLES = r'''
+# Run installer from mounted ISO, then unmount
+- name: Ensure an ISO is mounted
+ community.windows.win_disk_image:
+ image_path: C:\install.iso
+ state: present
+ register: disk_image_out
+
+- name: Run installer from mounted ISO
+ ansible.windows.win_package:
+ path: '{{ disk_image_out.mount_paths[0] }}setup\setup.exe'
+ product_id: 35a4e767-0161-46b0-979f-e61f282fee21
+ state: present
+
+- name: Unmount ISO
+ community.windows.win_disk_image:
+ image_path: C:\install.iso
+ state: absent
+'''
+
+RETURN = r'''
+mount_paths:
+ description: A list of filesystem paths mounted from the target image.
+ returned: when C(state) is C(present)
+ type: list
+ sample: [ 'E:\', 'F:\' ]
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_dns_record.ps1 b/ansible_collections/community/windows/plugins/modules/win_dns_record.ps1
new file mode 100644
index 00000000..84293253
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_dns_record.ps1
@@ -0,0 +1,202 @@
+#!powershell
+# Copyright: (c) 2021 Sebastian Gruber ,dacoso GmbH All Rights Reserved.
+# Copyright: (c) 2019, Hitachi ID Systems, Inc.
+# SPDX-License-Identifier: GPL-3.0-only
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+$spec = @{
+ options = @{
+ name = @{ type = "str"; required = $true }
+ port = @{ type = "int" }
+ priority = @{ type = "int" }
+ state = @{ type = "str"; choices = "absent", "present"; default = "present" }
+ ttl = @{ type = "int"; default = "3600" }
+ type = @{ type = "str"; choices = "A", "AAAA", "CNAME", "DHCID", "NS", "PTR", "SRV", "TXT"; required = $true }
+ value = @{ type = "list"; elements = "str"; default = @() ; aliases = @( 'values' ) }
+ weight = @{ type = "int" }
+ zone = @{ type = "str"; required = $true }
+ computer_name = @{ type = "str" }
+ }
+ required_if = @(, @("type", "SRV", @("port", "priority", "weight")))
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+$name = $module.Params.name
+$port = $module.Params.port
+$priority = $module.Params.priority
+$state = $module.Params.state
+$ttl = $module.Params.ttl
+$type = $module.Params.type
+$values = $module.Params.value
+$weight = $module.Params.weight
+$zone = $module.Params.zone
+$dns_computer_name = $module.Params.computer_name
+$extra_args = @{}
+
+if ($null -ne $dns_computer_name) {
+ $extra_args.ComputerName = $dns_computer_name
+}
+
+if ($state -eq 'present') {
+ if ($values.Count -eq 0) {
+ $module.FailJson("Parameter 'values' must be non-empty when state='present'")
+ }
+}
+else {
+ if ($values.Count -ne 0) {
+ $module.FailJson("Parameter 'values' must be undefined or empty when state='absent'")
+ }
+}
+
+# TODO: add warning for forest minTTL override -- see https://docs.microsoft.com/en-us/windows/desktop/ad/configuration-of-ttl-limits
+if ($ttl -lt 1 -or $ttl -gt 31557600) {
+ $module.FailJson("Parameter 'ttl' must be between 1 and 31557600")
+}
+
+$ttl = New-TimeSpan -Seconds $ttl
+
+if (($type -eq 'CNAME' -or $type -eq 'NS' -or $type -eq 'PTR' -or $type -eq 'SRV') -and $null -ne $values -and $values.Count -gt 0 -and $zone[-1] -ne '.') {
+ # CNAMEs and PTRs should be '.'-terminated, or record matching will fail
+ $values = $values | ForEach-Object {
+ if ($_ -Like "*.") { $_ } else { "$_." }
+ }
+}
+
+$record_argument_name = @{
+ A = "IPv4Address"
+ AAAA = "IPv6Address"
+ CNAME = "HostNameAlias"
+ DHCID = "DhcpIdentifier"
+ # MX = "MailExchange"
+ NS = "NameServer"
+ PTR = "PtrDomainName"
+ SRV = "DomainName"
+ TXT = "DescriptiveText"
+}[$type]
+
+function Get-DnsServerResourceRecordDataPropertyName {
+ Switch -Exact ($type) {
+ 'DHCID' {
+ 'DhcId'
+ }
+ default {
+ $record_argument_name
+ }
+ }
+}
+
+$changes = @{
+ before = ""
+ after = ""
+}
+
+$records = Get-DnsServerResourceRecord -ZoneName $zone -Name $name -RRType $type -Node -ErrorAction:Ignore @extra_args | Sort-Object
+
+if ($null -ne $records) {
+ # We use [Hashtable]$required_values below as a set rather than a map.
+ # It provides quick lookup to test existing DNS record against. By removing
+ # items as each is processed, whatever remains at the end is missing
+ # content (that needs to be added).
+ $required_values = @{}
+ foreach ($value in $values) {
+ $required_values[$value.ToString()] = $null
+ }
+
+ foreach ($record in $records) {
+ $record_value = $record.RecordData.$(Get-DnsServerResourceRecordDataPropertyName).ToString()
+
+ if (-Not $required_values.ContainsKey($record_value)) {
+ $record | Remove-DnsServerResourceRecord -ZoneName $zone -Force -WhatIf:$module.CheckMode @extra_args
+ $changes.before += "[$zone] $($record.HostName) $($record.TimeToLive.TotalSeconds) IN $type $record_value`n"
+ $module.Result.changed = $true
+ }
+ else {
+ if ($type -eq 'SRV') {
+ $record_port_old = $record.RecordData.Port.ToString()
+ $record_priority_old = $record.RecordData.Priority.ToString()
+ $record_weight_old = $record.RecordData.Weight.ToString()
+
+ if ($record.TimeToLive -ne $ttl -or $port -ne $record_port_old -or $priority -ne $record_priority_old -or $weight -ne $record_weight_old) {
+ $new_record = $record.Clone()
+ $new_record.TimeToLive = $ttl
+ $new_record.RecordData.Port = $port
+ $new_record.RecordData.Priority = $priority
+ $new_record.RecordData.Weight = $weight
+ Set-DnsServerResourceRecord -ZoneName $zone -OldInputObject $record -NewInputObject $new_record -WhatIf:$module.CheckMode @extra_args
+
+ $changes.before += -join @(
+ "[$zone] $($record.HostName) $($record.TimeToLive.TotalSeconds) IN "
+ "$type $record_value $record_port_old $record_weight_old $record_priority_old`n"
+ )
+ $changes.after += "[$zone] $($record.HostName) $($ttl.TotalSeconds) IN $type $record_value $port $weight $priority`n"
+ $module.Result.changed = $true
+ }
+ }
+ else {
+ # This record matches one of the values; but does it match the TTL?
+ if ($record.TimeToLive -ne $ttl) {
+ $new_record = $record.Clone()
+ $new_record.TimeToLive = $ttl
+ Set-DnsServerResourceRecord -ZoneName $zone -OldInputObject $record -NewInputObject $new_record -WhatIf:$module.CheckMode @extra_args
+ $changes.before += "[$zone] $($record.HostName) $($record.TimeToLive.TotalSeconds) IN $type $record_value`n"
+ $changes.after += "[$zone] $($record.HostName) $($ttl.TotalSeconds) IN $type $record_value`n"
+ $module.Result.changed = $true
+ }
+ }
+ # Cross this one off the list, so we don't try adding it late
+ $required_values.Remove($record_value)
+ # Whatever is left in $required_values needs to be added
+ $values = $required_values.Keys
+ }
+ }
+}
+
+if ($null -ne $values -and $values.Count -gt 0) {
+ foreach ($value in $values) {
+ $splat_args = @{ $type = $true; $record_argument_name = $value }
+ $module.Result.debug_splat_args = $splat_args
+ $srv_args = @{
+ DomainName = $value
+ Weight = $weight
+ Priority = $priority
+ Port = $port
+ }
+ try {
+ if ($type -eq 'SRV') {
+ Add-DnsServerResourceRecord -SRV -Name $name -ZoneName $zone @srv_args @extra_args -WhatIf:$module.CheckMode
+ }
+ else {
+ Add-DnsServerResourceRecord -Name $name -AllowUpdateAny -ZoneName $zone -TimeToLive $ttl @splat_args -WhatIf:$module.CheckMode @extra_args
+ }
+ }
+ catch {
+ $module.FailJson("Error adding DNS $type resource $name in zone $zone with value $value", $_)
+ }
+ $changes.after += "[$zone] $name $($ttl.TotalSeconds) IN $type $value`n"
+ }
+ $module.Result.changed = $true
+}
+
+if ($module.CheckMode) {
+ # Simulated changes
+ $module.Diff.before = $changes.before
+ $module.Diff.after = $changes.after
+}
+else {
+ # Real changes
+ $records_end = Get-DnsServerResourceRecord -ZoneName $zone -Name $name -RRType $type -Node -ErrorAction:Ignore @extra_args | Sort-Object
+ $module.Diff.before = @(
+ $records | ForEach-Object {
+ "[$zone] $($_.HostName) $($_.TimeToLive.TotalSeconds) IN $type $($_.RecordData.$(Get-DnsServerResourceRecordDataPropertyName).ToString())`n"
+ }
+ ) -join ''
+ $module.Diff.after = @(
+ $records_end | ForEach-Object {
+ "[$zone] $($_.HostName) $($_.TimeToLive.TotalSeconds) IN $type $($_.RecordData.$(Get-DnsServerResourceRecordDataPropertyName).ToString())`n"
+ }
+ ) -join ''
+}
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_dns_record.py b/ansible_collections/community/windows/plugins/modules/win_dns_record.py
new file mode 100644
index 00000000..4f3efd4a
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_dns_record.py
@@ -0,0 +1,192 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2021 Sebastian Gruber ,dacoso GmbH All Rights Reserved.
+# Copyright: (c) 2019, Hitachi ID Systems, Inc.
+# SPDX-License-Identifier: GPL-3.0-only
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_dns_record
+short_description: Manage Windows Server DNS records
+description:
+- Manage DNS records within an existing Windows Server DNS zone.
+author:
+ - Sebastian Gruber (@sgruber94)
+ - John Nelson (@johnboy2)
+requirements:
+ - This module requires Windows 8, Server 2012, or newer.
+options:
+ name:
+ description:
+ - The name of the record.
+ required: yes
+ type: str
+ port:
+ description:
+ - The port number of the record.
+ - Required when C(type=SRV).
+ - Supported only for C(type=SRV).
+ type: int
+ version_added: 1.0.0
+ priority:
+ description:
+ - The priority number for each service in SRV record.
+ - Required when C(type=SRV).
+ - Supported only for C(type=SRV).
+ type: int
+ version_added: 1.0.0
+ state:
+ description:
+ - Whether the record should exist or not.
+ choices: [ absent, present ]
+ default: present
+ type: str
+ ttl:
+ description:
+ - The "time to live" of the record, in seconds.
+ - Ignored when C(state=absent).
+ - Valid range is 1 - 31557600.
+ - Note that an Active Directory forest can specify a minimum TTL, and will
+ dynamically "round up" other values to that minimum.
+ default: 3600
+ type: int
+ type:
+ description:
+ - The type of DNS record to manage.
+ - C(SRV) was added in the 1.0.0 release of this collection.
+ - C(NS) was added in the 1.1.0 release of this collection.
+ - C(TXT) was added in the 1.6.0 release of this collection.
+ - C(DHCID) was added in the 1.12.0 release of this collection.
+ choices: [ A, AAAA, CNAME, DHCID, NS, PTR, SRV, TXT ]
+ required: yes
+ type: str
+ value:
+ description:
+ - The value(s) to specify. Required when C(state=present).
+ - When C(type=PTR) only the partial part of the IP should be given.
+ - Multiple values can be passed when C(type=NS)
+ aliases: [ values ]
+ default: []
+ type: list
+ elements: str
+ weight:
+ description:
+ - Weightage given to each service record in SRV record.
+ - Required when C(type=SRV).
+ - Supported only for C(type=SRV).
+ type: int
+ version_added: 1.0.0
+ zone:
+ description:
+ - The name of the zone to manage (eg C(example.com)).
+ - The zone must already exist.
+ required: yes
+ type: str
+ computer_name:
+ description:
+ - Specifies a DNS server.
+ - You can specify an IP address or any value that resolves to an IP
+ address, such as a fully qualified domain name (FQDN), host name, or
+ NETBIOS name.
+ type: str
+'''
+
+EXAMPLES = r'''
+# Demonstrate creating a matching A and PTR record.
+
+- name: Create database server record
+ community.windows.win_dns_record:
+ name: "cgyl1404p.amer.example.com"
+ type: "A"
+ value: "10.1.1.1"
+ zone: "amer.example.com"
+
+- name: Create matching PTR record
+ community.windows.win_dns_record:
+ name: "1.1.1"
+ type: "PTR"
+ value: "db1"
+ zone: "10.in-addr.arpa"
+
+# Demonstrate replacing an A record with a CNAME
+
+- name: Remove static record
+ community.windows.win_dns_record:
+ name: "db1"
+ type: "A"
+ state: absent
+ zone: "amer.example.com"
+
+- name: Create database server alias
+ community.windows.win_dns_record:
+ name: "db1"
+ type: "CNAME"
+ value: "cgyl1404p.amer.example.com"
+ zone: "amer.example.com"
+
+# Demonstrate creating multiple A records for the same name
+
+- name: Create multiple A record values for www
+ community.windows.win_dns_record:
+ name: "www"
+ type: "A"
+ values:
+ - 10.0.42.5
+ - 10.0.42.6
+ - 10.0.42.7
+ zone: "example.com"
+
+# Demonstrates a partial update (replace some existing values with new ones)
+# for a pre-existing name
+
+- name: Update www host with new addresses
+ community.windows.win_dns_record:
+ name: "www"
+ type: "A"
+ values:
+ - 10.0.42.5 # this old value was kept (others removed)
+ - 10.0.42.12 # this new value was added
+ zone: "example.com"
+
+# Demonstrate creating a SRV record
+
+- name: Creating a SRV record with port number and priority
+ community.windows.win_dns_record:
+ name: "test"
+ priority: 5
+ port: 995
+ state: present
+ type: "SRV"
+ weight: 2
+ value: "amer.example.com"
+ zone: "example.com"
+
+# Demonstrate creating a NS record with multiple values
+
+- name: Creating NS record
+ community.windows.win_dns_record:
+ name: "ansible.prog"
+ state: present
+ type: "NS"
+ values:
+ - 10.0.0.1
+ - 10.0.0.2
+ - 10.0.0.3
+ - 10.0.0.4
+ zone: "example.com"
+
+# Demonstrate creating a TXT record
+
+- name: Creating a TXT record with descriptive Text
+ community.windows.win_dns_record:
+ name: "test"
+ state: present
+ type: "TXT"
+ value: "justavalue"
+ zone: "example.com"
+'''
+
+RETURN = r'''
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_dns_zone.ps1 b/ansible_collections/community/windows/plugins/modules/win_dns_zone.ps1
new file mode 100644
index 00000000..9b84f9d9
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_dns_zone.ps1
@@ -0,0 +1,267 @@
+#!powershell
+
+# Copyright: (c) 2020 VMware, Inc. All Rights Reserved.
+# SPDX-License-Identifier: GPL-3.0-only
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+$spec = @{
+ options = @{
+ name = @{ type = "str"; required = $true }
+ type = @{ type = "str"; choices = "primary", "secondary", "forwarder", "stub" }
+ replication = @{ type = "str"; choices = "forest", "domain", "legacy", "none" }
+ dynamic_update = @{ type = "str"; choices = "secure", "none", "nonsecureandsecure" }
+ state = @{ type = "str"; choices = "absent", "present"; default = "present" }
+ forwarder_timeout = @{ type = "int" }
+ dns_servers = @{ type = "list"; elements = "str" }
+ }
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+$check_mode = $module.CheckMode
+
+$name = $module.Params.name
+$type = $module.Params.type
+$replication = $module.Params.replication
+$dynamic_update = $module.Params.dynamic_update
+$state = $module.Params.state
+$dns_servers = $module.Params.dns_servers
+$forwarder_timeout = $module.Params.forwarder_timeout
+
+$parms = @{ name = $name }
+
+Function Get-DnsZoneObject {
+ Param([PSObject]$Object)
+ $parms = @{
+ name = $Object.ZoneName.toLower()
+ type = $Object.ZoneType.toLower()
+ paused = $Object.IsPaused
+ shutdown = $Object.IsShutdown
+ }
+
+ if ($Object.DynamicUpdate) { $parms.dynamic_update = $Object.DynamicUpdate.toLower() }
+ if ($Object.IsReverseLookupZone) { $parms.reverse_lookup = $Object.IsReverseLookupZone }
+ if ($Object.ZoneType -like 'forwarder' ) { $parms.forwarder_timeout = $Object.ForwarderTimeout }
+ if ($Object.MasterServers) { $parms.dns_servers = $Object.MasterServers.IPAddressToString }
+ if (-not $Object.IsDsIntegrated) {
+ $parms.replication = "none"
+ $parms.zone_file = $Object.ZoneFile
+ }
+ else {
+ $parms.replication = $Object.ReplicationScope.toLower()
+ }
+
+ return $parms | Sort-Object
+}
+
+Function Compare-DnsZone {
+ Param(
+ [PSObject]$Original,
+ [PSObject]$Updated)
+
+ if ($Original -eq $false) { return $false }
+ $props = @('ZoneType', 'DynamicUpdate', 'IsDsIntegrated', 'MasterServers', 'ForwarderTimeout', 'ReplicationScope')
+ $x = Compare-Object $Original $Updated -Property $props
+ $x.Count -eq 0
+}
+
+# attempt import of module
+Try { Import-Module DnsServer }
+Catch { $module.FailJson("The DnsServer module failed to load properly: $($_.Exception.Message)", $_) }
+
+Try {
+ # determine current zone state
+ $current_zone = Get-DnsServerZone -name $name
+ $module.Diff.before = Get-DnsZoneObject -Object $current_zone
+ if (-not $type) { $type = $current_zone.ZoneType.toLower() }
+ if ($current_zone.ZoneType -like $type) { $current_zone_type_match = $true }
+ # check for fast fails
+ if ($current_zone.ReplicationScope -like 'none' -and $replication -in @('legacy', 'forest', 'domain')) {
+ $module.FailJson("Converting a file backed DNS zone to Active Directory integrated zone is unsupported")
+ }
+ if ($current_zone.ReplicationScope -in @('legacy', 'forest', 'domain') -and $replication -like 'none') {
+ $module.FailJson("Converting Active Directory integrated zone to a file backed DNS zone is unsupported")
+ }
+ if ($current_zone.IsDsIntegrated -eq $false -and $parms.DynamicUpdate -eq 'secure') {
+ $module.FailJson("The secure dynamic update option is only available for Active Directory integrated zones")
+ }
+}
+Catch {
+ $module.Diff.before = ""
+ $current_zone = $false
+}
+
+if ($state -eq "present") {
+ # parse replication/zonefile
+ if (-not $replication -and $current_zone) {
+ $parms.ReplicationScope = $current_zone.ReplicationScope
+ }
+ elseif ((($replication -eq 'none') -or (-not $replication)) -and (-not $current_zone)) {
+ $parms.ZoneFile = "$name.dns"
+ }
+ elseif (($replication -eq 'none') -and ($current_zone)) {
+ $parms.ZoneFile = "$name.dns"
+ }
+ else {
+ $parms.ReplicationScope = $replication
+ }
+ # parse param
+ if ($dynamic_update) { $parms.DynamicUpdate = $dynamic_update }
+ if ($dns_servers) { $parms.MasterServers = $dns_servers }
+ if ($type -in @('stub', 'forwarder', 'secondary') -and -not $current_zone -and -not $dns_servers) {
+ $module.FailJson("The dns_servers param is required when creating new stub, forwarder or secondary zones")
+ }
+ switch ($type) {
+ "primary" {
+ # remove irrelevant params
+ $parms.Remove('MasterServers')
+ if ($parms.ZoneFile -and ($dynamic_update -in @('secure', 'nonsecureandsecure'))) {
+ $parms.Remove('DynamicUpdate')
+ $module.Warn("Secure DNS updates are available only for Active Directory-integrated zones")
+ }
+ if (-not $current_zone) {
+ # create zone
+ Try { Add-DnsServerPrimaryZone @parms -WhatIf:$check_mode }
+ Catch { $module.FailJson("Failed to add $type zone $($name): $($_.Exception.Message)", $_) }
+ }
+ else {
+ # update zone
+ if (-not $current_zone_type_match) {
+ Try {
+ if ($current_zone.ReplicationScope) {
+ $parms.ReplicationScope = $current_zone.ReplicationScope
+ }
+ else {
+ $parms.Remove('ReplicationScope')
+ }
+ if ($current_zone.ZoneFile) { $parms.ZoneFile = $current_zone.ZoneFile } else { $parms.Remove('ReplicationScope') }
+ if ($current_zone.IsShutdown) { $module.FailJson("Failed to convert DNS zone $($name): this zone is shutdown and cannot be modified") }
+ ConvertTo-DnsServerPrimaryZone @parms -Force -WhatIf:$check_mode
+ }
+ Catch { $module.FailJson("Failed to convert DNS zone $($name): $($_.Exception.Message)", $_) }
+ }
+ Try {
+ if (-not $parms.ZoneFile) { Set-DnsServerPrimaryZone -Name $name -ReplicationScope $parms.ReplicationScope -WhatIf:$check_mode }
+ if ($dynamic_update) { Set-DnsServerPrimaryZone -Name $name -DynamicUpdate $parms.DynamicUpdate -WhatIf:$check_mode }
+ }
+ Catch { $module.FailJson("Failed to set properties on the zone $($name): $($_.Exception.Message)", $_) }
+ }
+ }
+ "secondary" {
+ # remove irrelevant params
+ $parms.Remove('ReplicationScope')
+ $parms.Remove('DynamicUpdate')
+ if (-not $current_zone) {
+ # enforce param
+ $parms.ZoneFile = "$name.dns"
+ # create zone
+ Try { Add-DnsServerSecondaryZone @parms -WhatIf:$check_mode }
+ Catch { $module.FailJson("Failed to add $type zone $($name): $($_.Exception.Message)", $_) }
+ }
+ else {
+ # update zone
+ if (-not $current_zone_type_match) {
+ $parms.MasterServers = $current_zone.MasterServers
+ $parms.ZoneFile = $current_zone.ZoneFile
+ if ($current_zone.IsShutdown) { $module.FailJson("Failed to convert DNS zone $($name): this zone is shutdown and cannot be modified") }
+ Try { ConvertTo-DnsServerSecondaryZone @parms -Force -WhatIf:$check_mode }
+ Catch { $module.FailJson("Failed to convert DNS zone $($name): $($_.Exception.Message)", $_) }
+ }
+ Try { if ($dns_servers) { Set-DnsServerSecondaryZone -Name $name -MasterServers $dns_servers -WhatIf:$check_mode } }
+ Catch { $module.FailJson("Failed to set properties on the zone $($name): $($_.Exception.Message)", $_) }
+ }
+ }
+ "stub" {
+ $parms.Remove('DynamicUpdate')
+ if (-not $current_zone) {
+ # create zone
+ Try { Add-DnsServerStubZone @parms -WhatIf:$check_mode }
+ Catch { $module.FailJson("Failed to add $type zone $($name): $($_.Exception.Message)", $_) }
+ }
+ else {
+ # update zone
+ if (-not $current_zone_type_match) { $module.FailJson("Failed to convert DNS zone $($name) to $type, unsupported conversion") }
+ Try {
+ if ($parms.ReplicationScope) { Set-DnsServerStubZone -Name $name -ReplicationScope $parms.ReplicationScope -WhatIf:$check_mode }
+ if ($forwarder_timeout) { Set-DnsServerStubZone -Name $name -ForwarderTimeout $forwarder_timeout -WhatIf:$check_mode }
+ if ($dns_servers) { Set-DnsServerStubZone -Name $name -MasterServers $dns_servers -WhatIf:$check_mode }
+ }
+ Catch { $module.FailJson("Failed to set properties on the zone $($name): $($_.Exception.Message)", $_) }
+ }
+ }
+ "forwarder" {
+ # remove irrelevant params
+ $parms.Remove('DynamicUpdate')
+ $parms.Remove('ZoneFile')
+ if ($forwarder_timeout -and ($forwarder_timeout -in 0..15)) {
+ $parms.ForwarderTimeout = $forwarder_timeout
+ }
+ if ($forwarder_timeout -and -not ($forwarder_timeout -in 0..15)) {
+ $module.Warn("The forwarder_timeout param must be an integer value between 0 and 15")
+ }
+ if ($parms.ReplicationScope -eq 'none') { $parms.Remove('ReplicationScope') }
+ if (-not $current_zone) {
+ # create zone
+ Try { Add-DnsServerConditionalForwarderZone @parms -WhatIf:$check_mode }
+ Catch { $module.FailJson("Failed to add $type zone $($name): $($_.Exception.Message)", $_) }
+ }
+ else {
+ # update zone
+ if (-not $current_zone_type_match) { $module.FailJson("Failed to convert DNS zone $($name) to $type, unsupported conversion") }
+ Try {
+ if ($parms.ReplicationScope) {
+ Set-DnsServerConditionalForwarderZone -Name $name -ReplicationScope $parms.ReplicationScope -WhatIf:$check_mode
+ }
+ if ($forwarder_timeout) { Set-DnsServerConditionalForwarderZone -Name $name -ForwarderTimeout $forwarder_timeout -WhatIf:$check_mode }
+ if ($dns_servers) { Set-DnsServerConditionalForwarderZone -Name $name -MasterServers $dns_servers -WhatIf:$check_mode }
+ }
+ Catch { $module.FailJson("Failed to set properties on the zone $($name): $($_.Exception.Message)", $_) }
+ }
+ }
+ }
+}
+
+if ($state -eq "absent") {
+ if ($current_zone -and -not $check_mode) {
+ Try {
+ Remove-DnsServerZone -Name $name -Force -WhatIf:$check_mode
+ $module.Result.changed = $true
+ $module.Diff.after = ""
+ }
+ Catch {
+ $module.FailJson("Failed to remove DNS zone: $($_.Exception.Message)", $_)
+ }
+ }
+ $module.ExitJson()
+}
+
+# determine if a change was made
+Try {
+ $new_zone = Get-DnsServerZone -Name $name
+ if (-not (Compare-DnsZone -Original $current_zone -Updated $new_zone)) {
+ $module.Result.changed = $true
+ $module.Result.zone = Get-DnsZoneObject -Object $new_zone
+ $module.Diff.after = Get-DnsZoneObject -Object $new_zone
+ }
+
+ # simulate changes if check mode
+ if ($check_mode) {
+ $new_zone = @{}
+ $current_zone.PSObject.Properties | ForEach-Object {
+ if ($parms[$_.Name]) {
+ $new_zone[$_.Name] = $parms[$_.Name]
+ }
+ else {
+ $new_zone[$_.Name] = $_.Value
+ }
+ }
+ $module.Diff.after = Get-DnsZoneObject -Object $new_zone
+ }
+}
+Catch {
+ $module.FailJson("Failed to lookup new zone $($name): $($_.Exception.Message)", $_)
+}
+
+$module.ExitJson() \ No newline at end of file
diff --git a/ansible_collections/community/windows/plugins/modules/win_dns_zone.py b/ansible_collections/community/windows/plugins/modules/win_dns_zone.py
new file mode 100644
index 00000000..194a3fc2
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_dns_zone.py
@@ -0,0 +1,182 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2020 VMware, Inc. All Rights Reserved.
+# SPDX-License-Identifier: GPL-3.0-only
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_dns_zone
+short_description: Manage Windows Server DNS Zones
+author: Joe Zollo (@joezollo)
+requirements:
+ - This module requires Windows Server 2012R2 or Newer
+description:
+ - Manage Windows Server DNS Zones
+ - Adds, Removes and Modifies DNS Zones - Primary, Secondary, Forwarder & Stub
+ - Task should be delegated to a Windows DNS Server
+options:
+ name:
+ description:
+ - Fully qualified name of the DNS zone.
+ type: str
+ required: true
+ type:
+ description:
+ - Specifies the type of DNS zone.
+ - When l(type=secondary), the DNS server will immediately attempt to
+ perform a zone transfer from the servers in this list. If this initial
+ transfer fails, then the zone will be left in an unworkable state.
+ This module does not verify the initial transfer.
+ type: str
+ choices: [ primary, secondary, stub, forwarder ]
+ dynamic_update:
+ description:
+ - Specifies how a zone handles dynamic updates.
+ - Secure DNS updates are available only for Active Directory-integrated
+ zones.
+ - When not specified during new zone creation, Windows will default this
+ to l(none).
+ type: str
+ choices: [ secure, none, nonsecureandsecure ]
+ state:
+ description:
+ - Specifies the desired state of the DNS zone.
+ - When l(state=present) the module will attempt to create the specified
+ DNS zone if it does not already exist.
+ - When l(state=absent), the module will remove the specified DNS
+ zone and all subsequent DNS records.
+ type: str
+ default: present
+ choices: [ present, absent ]
+ forwarder_timeout:
+ description:
+ - Specifies a length of time, in seconds, that a DNS server waits for a
+ remote DNS server to resolve a query.
+ - Accepts integer values between 0 and 15.
+ - If the provided value is not valid, it will be omitted and a warning
+ will be issued.
+ type: int
+ replication:
+ description:
+ - Specifies the replication scope for the DNS zone.
+ - l(replication=forest) will replicate the DNS zone to all domain
+ controllers in the Active Directory forest.
+ - l(replication=domain) will replicate the DNS zone to all domain
+ controllers in the Active Directory domain.
+ - l(replication=none) disables Active Directory integration and
+ creates a local file with the name of the zone.
+ - This is the equivalent of selecting l(store the zone in Active
+ Directory) in the GUI.
+ type: str
+ choices: [ forest, domain, legacy, none ]
+ dns_servers:
+ description:
+ - Specifies an list of IP addresses of the primary servers of the zone.
+ - DNS queries for a forwarded zone are sent to primary servers.
+ - Required if l(type=secondary), l(type=forwarder) or l(type=stub),
+ otherwise ignored.
+ - At least one server is required.
+ elements: str
+ type: list
+'''
+
+EXAMPLES = r'''
+- name: Ensure primary zone is present
+ community.windows.win_dns_zone:
+ name: wpinner.euc.vmware.com
+ replication: domain
+ type: primary
+ state: present
+
+- name: Ensure DNS zone is absent
+ community.windows.win_dns_zone:
+ name: jamals.euc.vmware.com
+ state: absent
+
+- name: Ensure forwarder has specific DNS servers
+ community.windows.win_dns_zone:
+ name: jamals.euc.vmware.com
+ type: forwarder
+ dns_servers:
+ - 10.245.51.100
+ - 10.245.51.101
+ - 10.245.51.102
+
+- name: Ensure stub zone has specific DNS servers
+ community.windows.win_dns_zone:
+ name: virajp.euc.vmware.com
+ type: stub
+ dns_servers:
+ - 10.58.2.100
+ - 10.58.2.101
+
+- name: Ensure stub zone is converted to a secondary zone
+ community.windows.win_dns_zone:
+ name: virajp.euc.vmware.com
+ type: secondary
+
+- name: Ensure secondary zone is present with no replication
+ community.windows.win_dns_zone:
+ name: dgemzer.euc.vmware.com
+ type: secondary
+ replication: none
+ dns_servers:
+ - 10.19.20.1
+
+- name: Ensure secondary zone is converted to a primary zone
+ community.windows.win_dns_zone:
+ name: dgemzer.euc.vmware.com
+ type: primary
+ replication: none
+ dns_servers:
+ - 10.19.20.1
+
+- name: Ensure primary DNS zone is present without replication
+ community.windows.win_dns_zone:
+ name: basavaraju.euc.vmware.com
+ replication: none
+ type: primary
+
+- name: Ensure primary DNS zone has nonsecureandsecure dynamic updates enabled
+ community.windows.win_dns_zone:
+ name: basavaraju.euc.vmware.com
+ replication: none
+ dynamic_update: nonsecureandsecure
+ type: primary
+
+- name: Ensure DNS zone is absent
+ community.windows.win_dns_zone:
+ name: marshallb.euc.vmware.com
+ state: absent
+
+- name: Ensure DNS zones are absent
+ community.windows.win_dns_zone:
+ name: "{{ item }}"
+ state: absent
+ loop:
+ - jamals.euc.vmware.com
+ - dgemzer.euc.vmware.com
+ - wpinner.euc.vmware.com
+ - marshallb.euc.vmware.com
+ - basavaraju.euc.vmware.com
+'''
+
+RETURN = r'''
+zone:
+ description: New/Updated DNS zone parameters
+ returned: When l(state=present)
+ type: dict
+ sample:
+ name:
+ type:
+ dynamic_update:
+ reverse_lookup:
+ forwarder_timeout:
+ paused:
+ shutdown:
+ zone_file:
+ replication:
+ dns_servers:
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_domain_computer.ps1 b/ansible_collections/community/windows/plugins/modules/win_domain_computer.ps1
new file mode 100644
index 00000000..ee2b16ea
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_domain_computer.ps1
@@ -0,0 +1,316 @@
+#!powershell
+
+# Copyright: (c) 2020, Brian Scholer (@briantist)
+# Copyright: (c) 2017, AMTEGA - Xunta de Galicia
+# 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.ArgvParser
+#Requires -Module Ansible.ModuleUtils.CommandUtil
+
+
+# ------------------------------------------------------------------------------
+$ErrorActionPreference = "Stop"
+
+# Preparing result
+$result = @{}
+$result.changed = $false
+
+# Parameter ingestion
+$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
+$temp = Get-AnsibleParam -obj $params -name '_ansible_remote_tmp' -type 'path' -default $env:TEMP
+
+$name = Get-AnsibleParam -obj $params -name "name" -failifempty $true -resultobj $result
+$sam_account_name = Get-AnsibleParam -obj $params -name "sam_account_name" -default "${name}$"
+If (-not $sam_account_name.EndsWith("$")) {
+ $sam_account_name = "${sam_account_name}$"
+}
+$enabled = Get-AnsibleParam -obj $params -name "enabled" -type "bool" -default $true
+$description = Get-AnsibleParam -obj $params -name "description" -default $null
+$domain_username = Get-AnsibleParam -obj $params -name "domain_username" -type "str"
+$domain_password = Get-AnsibleParam -obj $params -name "domain_password" -type "str" -failifempty ($null -ne $domain_username)
+$domain_server = Get-AnsibleParam -obj $params -name "domain_server" -type "str"
+$state = Get-AnsibleParam -obj $params -name "state" -ValidateSet "present", "absent" -default "present"
+$managed_by = Get-AnsibleParam -obj $params -name "managed_by" -type "str"
+
+$odj_action = Get-AnsibleParam -obj $params -name "offline_domain_join" -type "str" -ValidateSet "none", "output", "path" -default "none"
+$_default_blob_path = Join-Path -Path $temp -ChildPath ([System.IO.Path]::GetRandomFileName())
+$odj_blob_path = Get-AnsibleParam -obj $params -name "odj_blob_path" -type "str" -default $_default_blob_path
+
+$extra_args = @{}
+if ($null -ne $domain_username) {
+ $domain_password = ConvertTo-SecureString $domain_password -AsPlainText -Force
+ $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $domain_username, $domain_password
+ $extra_args.Credential = $credential
+}
+if ($null -ne $domain_server) {
+ $extra_args.Server = $domain_server
+}
+
+If ($state -eq "present") {
+ $dns_hostname = Get-AnsibleParam -obj $params -name "dns_hostname" -failifempty $true -resultobj $result
+ $ou = Get-AnsibleParam -obj $params -name "ou" -failifempty $true -resultobj $result
+ $distinguished_name = "CN=$name,$ou"
+
+ $desired_state = [ordered]@{
+ name = $name
+ sam_account_name = $sam_account_name
+ dns_hostname = $dns_hostname
+ ou = $ou
+ distinguished_name = $distinguished_name
+ description = $description
+ enabled = $enabled
+ state = $state
+ managed_by = $managed_by
+ }
+}
+Else {
+ $desired_state = [ordered]@{
+ name = $name
+ sam_account_name = $sam_account_name
+ state = $state
+ }
+}
+
+# ------------------------------------------------------------------------------
+Function Get-InitialState($desired_state) {
+ # Test computer exists
+ $computer = Try {
+ Get-ADComputer `
+ -Identity $desired_state.sam_account_name `
+ -Properties DistinguishedName, DNSHostName, Enabled, Name, SamAccountName, Description, ObjectClass, ManagedBy `
+ @extra_args
+ }
+ Catch { $null }
+ If ($computer) {
+ $null, $current_ou = $computer.DistinguishedName -split '(?<=[^\\](?:\\\\)*),'
+ $current_ou = $current_ou -join ','
+
+ $initial_state = [ordered]@{
+ name = $computer.Name
+ sam_account_name = $computer.SamAccountName
+ dns_hostname = $computer.DNSHostName
+ ou = $current_ou
+ distinguished_name = $computer.DistinguishedName
+ description = $computer.Description
+ enabled = $computer.Enabled
+ state = "present"
+ managed_by = $computer.ManagedBy
+ }
+ }
+ Else {
+ $initial_state = [ordered]@{
+ name = $desired_state.name
+ sam_account_name = $desired_state.sam_account_name
+ state = "absent"
+ }
+ }
+
+ return $initial_state
+}
+
+# ------------------------------------------------------------------------------
+Function Set-ConstructedState($initial_state, $desired_state) {
+ Try {
+ Set-ADComputer `
+ -Identity $desired_state.name `
+ -SamAccountName $desired_state.name `
+ -DNSHostName $desired_state.dns_hostname `
+ -Enabled $desired_state.enabled `
+ -Description $desired_state.description `
+ -ManagedBy $desired_state.managed_by `
+ -WhatIf:$check_mode `
+ @extra_args
+ }
+ Catch {
+ Fail-Json -obj $result -message "Failed to set the AD object $($desired_state.name): $($_.Exception.Message)"
+ }
+
+ If ($initial_state.distinguished_name -cne $desired_state.distinguished_name) {
+ # Move computer to OU
+ Try {
+ Get-ADComputer -Identity $desired_state.sam_account_name @extra_args |
+ Move-ADObject `
+ -TargetPath $desired_state.ou `
+ -Confirm:$False `
+ -WhatIf:$check_mode `
+ @extra_args
+ }
+ Catch {
+ $msg = "Failed to move the AD object $($initial_state.distinguished_name) to $($desired_state.distinguished_name): $($_.Exception.Message)"
+ Fail-Json -obj $result -message $msg
+ }
+ }
+ $result.changed = $true
+}
+
+# ------------------------------------------------------------------------------
+Function Add-ConstructedState($desired_state) {
+ Try {
+ New-ADComputer `
+ -Name $desired_state.name `
+ -SamAccountName $desired_state.sam_account_name `
+ -DNSHostName $desired_state.dns_hostname `
+ -Path $desired_state.ou `
+ -Enabled $desired_state.enabled `
+ -Description $desired_state.description `
+ -ManagedBy $desired_state.managed_by `
+ -WhatIf:$check_mode `
+ @extra_args
+ }
+ Catch {
+ Fail-Json -obj $result -message "Failed to create the AD object $($desired_state.name): $($_.Exception.Message)"
+ }
+
+ $result.changed = $true
+}
+
+Function Invoke-OfflineDomainJoin {
+ [CmdletBinding(SupportsShouldProcess = $true)]
+ param(
+ [Parameter(Mandatory = $true)]
+ [System.Collections.IDictionary]
+ $desired_state ,
+
+ [Parameter(Mandatory = $true)]
+ [ValidateSet('none', 'output', 'path')]
+ [String]
+ $Action ,
+
+ [Parameter()]
+ [System.IO.FileInfo]
+ $BlobPath
+ )
+
+ End {
+ if ($Action -eq 'none') {
+ return
+ }
+
+ $dns_domain = $desired_state.dns_hostname -replace '^[^.]+\.'
+
+ $output = $Action -eq 'output'
+
+ $arguments = @(
+ 'djoin.exe'
+ '/PROVISION'
+ '/REUSE' # we're pre-creating the machine normally to set other fields, then overwriting it with this
+ '/DOMAIN'
+ $dns_domain
+ '/MACHINE'
+ $desired_state.sam_account_name.TrimEnd('$') # this machine name is the short name
+ '/MACHINEOU'
+ $desired_state.ou
+ '/SAVEFILE'
+ $BlobPath.FullName
+ )
+
+ $invocation = Argv-ToString -arguments $arguments
+ $result.djoin = @{
+ invocation = $invocation
+ }
+ $result.odj_blob = ''
+
+ if ($Action -eq 'path') {
+ $result.odj_blob_path = $BlobPath.FullName
+ }
+
+ if (-not $BlobPath.Directory.Exists) {
+ Fail-Json -obj $result -message "BLOB path directory '$($BlobPath.Directory.FullName)' doesn't exist."
+ }
+
+ if ($PSCmdlet.ShouldProcess($argstring)) {
+ try {
+ $djoin_result = Run-Command -command $invocation
+ $result.djoin.rc = $djoin_result.rc
+ $result.djoin.stdout = $djoin_result.stdout
+ $result.djoin.stderr = $djoin_result.stderr
+
+ if ($djoin_result.rc) {
+ Fail-Json -obj $result -message "Problem running djoin.exe. See returned values."
+ }
+
+ if ($output) {
+ $bytes = [System.IO.File]::ReadAllBytes($BlobPath.FullName)
+ $data = [Convert]::ToBase64String($bytes)
+ $result.odj_blob = $data
+ }
+ }
+ finally {
+ if ($output -and $BlobPath.Exists) {
+ $BlobPath.Delete()
+ }
+ }
+ }
+ }
+}
+
+# ------------------------------------------------------------------------------
+Function Remove-ConstructedState($initial_state) {
+ Try {
+ Get-ADComputer -Identity $initial_state.sam_account_name @extra_args |
+ Remove-ADObject `
+ -Recursive `
+ -Confirm:$False `
+ -WhatIf:$check_mode `
+ @extra_args
+ }
+ Catch {
+ Fail-Json -obj $result -message "Failed to remove the AD object $($desired_state.name): $($_.Exception.Message)"
+ }
+
+ $result.changed = $true
+}
+
+# ------------------------------------------------------------------------------
+Function Test-HashtableEquality($x, $y) {
+ # Compare not nested HashTables
+ Foreach ($key in $x.Keys) {
+ If (($y.Keys -notcontains $key) -or ($x[$key] -cne $y[$key])) {
+ Return $false
+ }
+ }
+ foreach ($key in $y.Keys) {
+ if (($x.Keys -notcontains $key) -or ($x[$key] -cne $y[$key])) {
+ Return $false
+ }
+ }
+ Return $true
+}
+
+# ------------------------------------------------------------------------------
+$initial_state = Get-InitialState($desired_state)
+
+If ($desired_state.state -eq "present") {
+ If ($initial_state.state -eq "present") {
+ $in_desired_state = Test-HashtableEquality -X $initial_state -Y $desired_state
+
+ If (-not $in_desired_state) {
+ Set-ConstructedState -initial_state $initial_state -desired_state $desired_state
+ }
+ }
+ Else {
+ # $desired_state.state = "Present" & $initial_state.state = "Absent"
+ Add-ConstructedState -desired_state $desired_state
+ Invoke-OfflineDomainJoin -desired_state $desired_state -Action $odj_action -BlobPath $odj_blob_path -WhatIf:$check_mode
+ }
+}
+Else {
+ # $desired_state.state = "Absent"
+ If ($initial_state.state -eq "present") {
+ Remove-ConstructedState -initial_state $initial_state
+ }
+}
+
+If ($diff_support) {
+ $diff = @{
+ before = $initial_state
+ after = $desired_state
+ }
+ $result.diff = $diff
+}
+
+Exit-Json -obj $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_domain_computer.py b/ansible_collections/community/windows/plugins/modules/win_domain_computer.py
new file mode 100644
index 00000000..8e759c3d
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_domain_computer.py
@@ -0,0 +1,210 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2020, Brian Scholer (@briantist)
+# Copyright: (c) 2017, AMTEGA - Xunta de Galicia
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_domain_computer
+short_description: Manage computers in Active Directory
+description:
+ - Create, read, update and delete computers in Active Directory using a
+ windows bridge computer to launch New-ADComputer, Get-ADComputer,
+ Set-ADComputer, Remove-ADComputer and Move-ADObject powershell commands.
+options:
+ name:
+ description:
+ - Specifies the name of the object.
+ - This parameter sets the Name property of the Active Directory object.
+ - The LDAP display name (ldapDisplayName) of this property is name.
+ type: str
+ required: true
+ sam_account_name:
+ description:
+ - Specifies the Security Account Manager (SAM) account name of the
+ computer.
+ - It maximum is 256 characters, 15 is advised for older
+ operating systems compatibility.
+ - The LDAP display name (ldapDisplayName) for this property is sAMAccountName.
+ - If ommitted the value is the same as C(name).
+ - Note that all computer SAMAccountNames need to end with a C($).
+ - If C($) is omitted, it will be added to the end.
+ type: str
+ enabled:
+ description:
+ - Specifies if an account is enabled.
+ - An enabled account requires a password.
+ - This parameter sets the Enabled property for an account object.
+ - This parameter also sets the ADS_UF_ACCOUNTDISABLE flag of the
+ Active Directory User Account Control (UAC) attribute.
+ type: bool
+ default: yes
+ ou:
+ description:
+ - Specifies the X.500 path of the Organizational Unit (OU) or container
+ where the new object is created. Required when I(state=present).
+ - "Special characters must be escaped,
+ see L(Distinguished Names,https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ldap/distinguished-names) for details."
+ type: str
+ description:
+ description:
+ - Specifies a description of the object.
+ - This parameter sets the value of the Description property for the object.
+ - The LDAP display name (ldapDisplayName) for this property is description.
+ type: str
+ default: ''
+ dns_hostname:
+ description:
+ - Specifies the fully qualified domain name (FQDN) of the computer.
+ - This parameter sets the DNSHostName property for a computer object.
+ - The LDAP display name for this property is dNSHostName.
+ - Required when I(state=present).
+ type: str
+ domain_username:
+ description:
+ - The username to use when interacting with AD.
+ - If this is not set then the user Ansible used to log in with will be
+ used instead when using CredSSP or Kerberos with credential delegation.
+ type: str
+ domain_password:
+ description:
+ - The password for I(username).
+ type: str
+ domain_server:
+ description:
+ - Specifies the Active Directory Domain Services instance to connect to.
+ - Can be in the form of an FQDN or NetBIOS name.
+ - If not specified then the value is based on the domain of the computer
+ running PowerShell.
+ type: str
+ state:
+ description:
+ - Specified whether the computer should be C(present) or C(absent) in
+ Active Directory.
+ type: str
+ choices: [ absent, present ]
+ default: present
+ managed_by:
+ description:
+ - The value to be assigned to the LDAP C(managedBy) attribute.
+ - This value can be in the forms C(Distinguished Name), C(objectGUID),
+ C(objectSid) or C(sAMAccountName), see examples for more details.
+ type: str
+ version_added: '1.3.0'
+ offline_domain_join:
+ description:
+ - Provisions a computer in the directory and provides a BLOB file that can be used on the target computer/image to join it to the domain while offline.
+ - The C(none) value doesn't do any offline join operations.
+ - C(output) returns the BLOB in output. The BLOB should be treated as secret (it contains the machine password) so use C(no_log) when using this option.
+ - C(path) preserves the offline domain join BLOB file on the target machine for later use. The path will be returned.
+ - If the computer already exists, no BLOB will be created/returned, and the module will operate as it would have without offline domain join.
+ type: str
+ choices:
+ - none
+ - output
+ - path
+ default: none
+ odj_blob_path:
+ description:
+ - The path to the file where the BLOB will be saved. If omitted, a temporary file will be used.
+ - If I(offline_domain_join=output) the file will be deleted after its contents are returned.
+ - The parent directory for the BLOB file must exist; intermediate directories will not be created.
+notes:
+ - "For more information on Offline Domain Join
+ see L(the step-by-step guide,https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd392267%28v=ws.10%29)."
+ - When using the ODJ BLOB to join a computer to the domain, it must be written out to a file.
+ - The file must be UTF-16 encoded (in PowerShell this encoding is called C(Unicode)), and it must end in a null character. See examples.
+ - The C(djoin.exe) part of the offline domain join process will not use I(domain_server), I(domain_username), or I(domain_password).
+seealso:
+- module: ansible.windows.win_domain
+- module: ansible.windows.win_domain_controller
+- module: community.windows.win_domain_group
+- module: ansible.windows.win_domain_membership
+- module: community.windows.win_domain_user
+author:
+- Daniel Sánchez Fábregas (@Daniel-Sanchez-Fabregas)
+- Brian Scholer (@briantist)
+'''
+
+EXAMPLES = r'''
+ - name: Add linux computer to Active Directory OU using a windows machine
+ community.windows.win_domain_computer:
+ name: one_linux_server
+ sam_account_name: linux_server$
+ dns_hostname: one_linux_server.my_org.local
+ ou: "OU=servers,DC=my_org,DC=local"
+ description: Example of linux server
+ enabled: yes
+ state: present
+ delegate_to: my_windows_bridge.my_org.local
+
+ - name: Remove linux computer from Active Directory using a windows machine
+ community.windows.win_domain_computer:
+ name: one_linux_server
+ state: absent
+ delegate_to: my_windows_bridge.my_org.local
+
+ - name: Provision a computer for offline domain join
+ community.windows.win_domain_computer:
+ name: newhost
+ dns_hostname: newhost.ansible.local
+ ou: 'OU=A great\, big organizational unit name,DC=ansible,DC=local'
+ state: present
+ offline_domain_join: yes
+ odj_return_blob: yes
+ register: computer_status
+ delegate_to: windc.ansible.local
+
+ - name: Join a workgroup computer to the domain
+ vars:
+ target_blob_file: 'C:\ODJ\blob.txt'
+ ansible.windows.win_shell: |
+ $blob = [Convert]::FromBase64String('{{ computer_status.odj_blob }}')
+ [IO.File]::WriteAllBytes('{{ target_blob_file }}', $blob)
+ & djoin.exe --% /RequestODJ /LoadFile '{{ target_blob_file }}' /LocalOS /WindowsPath "%SystemRoot%"
+
+ - name: Restart to complete domain join
+ ansible.windows.win_restart:
+'''
+
+RETURN = r'''
+odj_blob:
+ description:
+ - The offline domain join BLOB. This is an empty string when in check mode or when offline_domain_join is 'path'.
+ - This field contains the base64 encoded raw bytes of the offline domain join BLOB file.
+ returned: when offline_domain_join is not 'none' and the computer didn't exist
+ type: str
+ sample: <a long base64 string>
+odj_blob_file:
+ description: The path to the offline domain join BLOB file on the target host. If odj_blob_path was specified, this will match that path.
+ returned: when offline_domain_join is 'path' and the computer didn't exist
+ type: str
+ sample: 'C:\Users\admin\AppData\Local\Temp\e4vxonty.rkb'
+djoin:
+ description: Information about the invocation of djoin.exe.
+ returned: when offline_domain_join is True and the computer didn't exist
+ type: dict
+ contains:
+ invocation:
+ description: The full command line used to call djoin.exe
+ type: str
+ returned: always
+ sample: djoin.exe /PROVISION /MACHINE compname /MACHINEOU OU=Hosts,DC=ansible,DC=local /DOMAIN ansible.local /SAVEFILE blobfile.txt
+ rc:
+ description: The return code from djoin.exe
+ type: int
+ returned: when not check mode
+ sample: 87
+ stdout:
+ description: The stdout from djoin.exe
+ type: str
+ returned: when not check mode
+ sample: Computer provisioning completed successfully.
+ stderr:
+ description: The stderr from djoin.exe
+ type: str
+ returned: when not check mode
+ sample: Invalid input parameter combination.
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_domain_group.ps1 b/ansible_collections/community/windows/plugins/modules/win_domain_group.ps1
new file mode 100644
index 00000000..d9373766
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_domain_group.ps1
@@ -0,0 +1,371 @@
+#!powershell
+
+# Copyright: (c) 2017, Jordan Borean <jborean93@gmail.com>, and others
+# 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 -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
+
+$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
+$display_name = Get-AnsibleParam -obj $params -name "display_name" -type "str"
+$domain_username = Get-AnsibleParam -obj $params -name "domain_username" -type "str"
+$domain_password = Get-AnsibleParam -obj $params -name "domain_password" -type "str" -failifempty ($null -ne $domain_username)
+$description = Get-AnsibleParam -obj $params -name "description" -type "str"
+$category = Get-AnsibleParam -obj $params -name "category" -type "str" -validateset "distribution", "security"
+$scope = Get-AnsibleParam -obj $params -name "scope" -type "str" -validateset "domainlocal", "global", "universal"
+$managed_by = Get-AnsibleParam -obj $params -name "managed_by" -type "str"
+$attributes = Get-AnsibleParam -obj $params -name "attributes"
+$organizational_unit = Get-AnsibleParam -obj $params -name "organizational_unit" -type "str" -aliases "ou", "path"
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present", "absent"
+$protect = Get-AnsibleParam -obj $params -name "protect" -type "bool"
+$ignore_protection = Get-AnsibleParam -obj $params -name "ignore_protection" -type "bool" -default $false
+$domain_server = Get-AnsibleParam -obj $params -name "domain_server" -type "str"
+
+$result = @{
+ changed = $false
+ created = $false
+}
+
+if ($diff_mode) {
+ $result.diff = @{}
+}
+
+if (-not (Get-Module -Name ActiveDirectory -ListAvailable)) {
+ Fail-Json $result "win_domain_group requires the ActiveDirectory PS module to be installed"
+}
+Import-Module ActiveDirectory
+
+$extra_args = @{}
+if ($null -ne $domain_username) {
+ $domain_password = ConvertTo-SecureString $domain_password -AsPlainText -Force
+ $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $domain_username, $domain_password
+ $extra_args.Credential = $credential
+}
+if ($null -ne $domain_server) {
+ $extra_args.Server = $domain_server
+}
+
+try {
+ $group = Get-ADGroup -Identity $name -Properties * @extra_args
+}
+catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
+ $group = $null
+}
+catch {
+ Fail-Json $result "failed to retrieve initial details for group $($name): $($_.Exception.Message)"
+}
+if ($state -eq "absent") {
+ if ($null -ne $group) {
+ if ($group.ProtectedFromAccidentalDeletion -eq $true -and $ignore_protection -eq $true) {
+ $group = $group | Set-ADObject -ProtectedFromAccidentalDeletion $false -WhatIf:$check_mode -PassThru @extra_args
+ }
+ elseif ($group.ProtectedFromAccidentalDeletion -eq $true -and $ignore_protection -eq $false) {
+ $msg = -join @(
+ "cannot delete group $name when ProtectedFromAccidentalDeletion is turned on, "
+ "run this module with ignore_protection=true to override this"
+ )
+ Fail-Json $result $msg
+ }
+
+ try {
+ $group | Remove-ADGroup -Confirm:$false -WhatIf:$check_mode @extra_args
+ }
+ catch {
+ Fail-Json $result "failed to remove group $($name): $($_.Exception.Message)"
+ }
+
+ $result.changed = $true
+ if ($diff_mode) {
+ $result.diff.prepared = "-[$name]"
+ }
+ }
+}
+else {
+ # validate that path is an actual path
+ if ($null -ne $organizational_unit) {
+ try {
+ Get-ADObject -Identity $organizational_unit @extra_args | Out-Null
+ }
+ catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
+ Fail-Json $result "the group path $organizational_unit does not exist, please specify a valid LDAP path"
+ }
+ }
+
+ $diff_text = $null
+ if ($null -ne $group) {
+ # will be overridden later if no change actually occurs
+ $diff_text += "[$name]`n"
+
+ # change the path of the group
+ if ($null -ne $organizational_unit) {
+ $group_cn = $group.CN
+ $existing_path = $group.DistinguishedName -replace "^CN=$group_cn,", ''
+ if ($existing_path -ne $organizational_unit) {
+ $protection_disabled = $false
+ if ($group.ProtectedFromAccidentalDeletion -eq $true -and $ignore_protection -eq $true) {
+ $group | Set-ADObject -ProtectedFromAccidentalDeletion $false -WhatIf:$check_mode -PassThru @extra_args | Out-Null
+ $protection_disabled = $true
+ }
+ elseif ($group.ProtectedFromAccidentalDeletion -eq $true -and $ignore_protection -eq $false) {
+ $msg = -join @(
+ "cannot move group $name when ProtectedFromAccidentalDeletion is turned on, "
+ "run this module with ignore_protection=true to override this"
+ )
+ Fail-Json $result $msg
+ }
+
+ try {
+ $group = $group | Move-ADObject -Targetpath $organizational_unit -WhatIf:$check_mode -PassThru @extra_args
+ }
+ catch {
+ Fail-Json $result "failed to move group from $existing_path to $($organizational_unit): $($_.Exception.Message)"
+ }
+ finally {
+ if ($protection_disabled -eq $true) {
+ $group | Set-ADObject -ProtectedFromAccidentalDeletion $true -WhatIf:$check_mode -PassThru @extra_args | Out-Null
+ }
+ }
+
+ $result.changed = $true
+ $diff_text += "-DistinguishedName = CN=$group_cn,$existing_path`n+DistinguishedName = CN=$group_cn,$organizational_unit`n"
+
+ if ($protection_disabled -eq $true) {
+ $group | Set-ADObject -ProtectedFromAccidentalDeletion $true -WhatIf:$check_mode @extra_args | Out-Null
+ }
+ # get the group again once we have moved it
+ $group = Get-ADGroup -Identity $name -Properties * @extra_args
+ }
+ }
+
+ # change attributes of group
+ $extra_scope_change = $null
+ $run_change = $false
+ $set_args = $extra_args.Clone()
+
+ if ($null -ne $scope) {
+ if ($group.GroupScope -ne $scope) {
+ # you cannot from from Global to DomainLocal and vice-versa, we
+ # need to change it to Universal and then finally to the target
+ # scope
+ if ($group.GroupScope -eq "global" -and $scope -eq "domainlocal") {
+ $set_args.GroupScope = "Universal"
+ $extra_scope_change = $scope
+ }
+ elseif ($group.GroupScope -eq "domainlocal" -and $scope -eq "global") {
+ $set_args.GroupScope = "Universal"
+ $extra_scope_change = $scope
+ }
+ else {
+ $set_args.GroupScope = $scope
+ }
+ $run_change = $true
+ $diff_text += "-GroupScope = $($group.GroupScope)`n+GroupScope = $scope`n"
+ }
+ }
+
+ if ($null -ne $description -and $group.Description -cne $description) {
+ $set_args.Description = $description
+ $run_change = $true
+ $diff_text += "-Description = $($group.Description)`n+Description = $description`n"
+ }
+
+ if ($null -ne $display_name -and $group.DisplayName -cne $display_name) {
+ $set_args.DisplayName = $display_name
+ $run_change = $true
+ $diff_text += "-DisplayName = $($group.DisplayName)`n+DisplayName = $display_name`n"
+ }
+
+ if ($null -ne $category -and $group.GroupCategory -ne $category) {
+ $set_args.GroupCategory = $category
+ $run_change = $true
+ $diff_text += "-GroupCategory = $($group.GroupCategory)`n+GroupCategory = $category`n"
+ }
+
+ if ($null -ne $managed_by) {
+ if ($null -eq $group.ManagedBy) {
+ $set_args.ManagedBy = $managed_by
+ $run_change = $true
+ $diff_text += "+ManagedBy = $managed_by`n"
+ }
+ else {
+ try {
+ $managed_by_object = Get-ADGroup -Identity $managed_by @extra_args
+ }
+ catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
+ try {
+ $managed_by_object = Get-ADUser -Identity $managed_by @extra_args
+ }
+ catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
+ Fail-Json $result "failed to find managed_by user or group $managed_by to be used for comparison"
+ }
+ }
+
+ if ($group.ManagedBy -ne $managed_by_object.DistinguishedName) {
+ $set_args.ManagedBy = $managed_by
+ $run_change = $true
+ $diff_text += "-ManagedBy = $($group.ManagedBy)`n+ManagedBy = $($managed_by_object.DistinguishedName)`n"
+ }
+ }
+ }
+
+ if ($null -ne $attributes) {
+ $add_attributes = @{}
+ $replace_attributes = @{}
+ foreach ($attribute in $attributes.GetEnumerator()) {
+ $attribute_name = $attribute.Name
+ $attribute_value = $attribute.Value
+
+ $valid_property = [bool]($group.PSobject.Properties.name -eq $attribute_name)
+ if ($valid_property) {
+ $existing_value = $group.$attribute_name
+ if ($existing_value -cne $attribute_value) {
+ $replace_attributes.$attribute_name = $attribute_value
+ $diff_text += "-$attribute_name = $existing_value`n+$attribute_name = $attribute_value`n"
+ }
+ }
+ else {
+ $add_attributes.$attribute_name = $attribute_value
+ $diff_text += "+$attribute_name = $attribute_value`n"
+ }
+ }
+ if ($add_attributes.Count -gt 0) {
+ $set_args.Add = $add_attributes
+ $run_change = $true
+ }
+ if ($replace_attributes.Count -gt 0) {
+ $set_args.Replace = $replace_attributes
+ $run_change = $true
+ }
+ }
+
+ if ($run_change) {
+ try {
+ $group = $group | Set-ADGroup -WhatIf:$check_mode -PassThru @set_args
+ }
+ catch {
+ Fail-Json $result "failed to change group $($name): $($_.Exception.Message)"
+ }
+ $result.changed = $true
+
+ if ($null -ne $extra_scope_change) {
+ try {
+ $group = $group | Set-ADGroup -GroupScope $extra_scope_change -WhatIf:$check_mode -PassThru @extra_args
+ }
+ catch {
+ Fail-Json $result "failed to change scope of group $name to $($scope): $($_.Exception.Message)"
+ }
+ }
+ }
+
+ # make sure our diff text is null if no change occurred
+ if ($result.changed -eq $false) {
+ $diff_text = $null
+ }
+ }
+ else {
+ # validate if scope is set
+ if ($null -eq $scope) {
+ Fail-Json $result "scope must be set when state=present and the group doesn't exist"
+ }
+
+ $diff_text += "+[$name]`n+Scope = $scope`n"
+ $add_args = $extra_args.Clone()
+ $add_args.Name = $name
+ $add_args.GroupScope = $scope
+
+ if ($null -ne $description) {
+ $add_args.Description = $description
+ $diff_text += "+Description = $description`n"
+ }
+
+ if ($null -ne $display_name) {
+ $add_args.DisplayName = $display_name
+ $diff_text += "+DisplayName = $display_name`n"
+ }
+
+ if ($null -ne $category) {
+ $add_args.GroupCategory = $category
+ $diff_text += "+GroupCategory = $category`n"
+ }
+
+ if ($null -ne $managed_by) {
+ $add_args.ManagedBy = $managed_by
+ $diff_text += "+ManagedBy = $managed_by`n"
+ }
+
+ if ($null -ne $attributes) {
+ $add_args.OtherAttributes = $attributes
+ foreach ($attribute in $attributes.GetEnumerator()) {
+ $diff_text += "+$($attribute.Name) = $($attribute.Value)`n"
+ }
+ }
+
+ if ($null -ne $organizational_unit) {
+ $add_args.Path = $organizational_unit
+ $diff_text += "+Path = $organizational_unit`n"
+ }
+
+ try {
+ $group = New-AdGroup -WhatIf:$check_mode -PassThru @add_args
+ }
+ catch {
+ Fail-Json $result "failed to create group $($name): $($_.Exception.Message)"
+ }
+ $result.changed = $true
+ $result.created = $true
+ }
+
+ # set the protection value
+ if ($null -ne $protect) {
+ if (-not $check_mode) {
+ $group = Get-ADGroup -Identity $name -Properties * @extra_args
+ }
+ $existing_protection_value = $group.ProtectedFromAccidentalDeletion
+ if ($null -eq $existing_protection_value) {
+ $existing_protection_value = $false
+ }
+ if ($existing_protection_value -ne $protect) {
+ $diff_text += @"
+-ProtectedFromAccidentalDeletion = $existing_protection_value
++ProtectedFromAccidentalDeletion = $protect
+"@
+
+ $group | Set-ADObject -ProtectedFromAccidentalDeletion $protect -WhatIf:$check_mode -PassThru @extra_args
+ $result.changed = $true
+ }
+ }
+
+ if ($diff_mode -and $null -ne $diff_text) {
+ $result.diff.prepared = $diff_text
+ }
+
+ if (-not $check_mode) {
+ $group = Get-ADGroup -Identity $name -Properties * @extra_args
+ $result.sid = $group.SID.Value
+ $result.description = $group.Description
+ $result.distinguished_name = $group.DistinguishedName
+ $result.display_name = $group.DisplayName
+ $result.name = $group.Name
+ $result.canonical_name = $group.CanonicalName
+ $result.guid = $group.ObjectGUID
+ $result.protected_from_accidental_deletion = $group.ProtectedFromAccidentalDeletion
+ $result.managed_by = $group.ManagedBy
+ $result.group_scope = ($group.GroupScope).ToString()
+ $result.category = ($group.GroupCategory).ToString()
+
+ if ($null -ne $attributes) {
+ $result.attributes = @{}
+ foreach ($attribute in $attributes.GetEnumerator()) {
+ $attribute_name = $attribute.Name
+ $result.attributes.$attribute_name = $group.$attribute_name
+ }
+ }
+ }
+}
+
+Exit-Json $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_domain_group.py b/ansible_collections/community/windows/plugins/modules/win_domain_group.py
new file mode 100644
index 00000000..b761055e
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_domain_group.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)
+
+DOCUMENTATION = r'''
+---
+module: win_domain_group
+short_description: Creates, modifies or removes domain groups
+description:
+- Creates, modifies or removes groups in Active Directory.
+- For local groups, use the M(ansible.windows.win_group) module instead.
+options:
+ attributes:
+ description:
+ - A dict of custom LDAP attributes to set on the group.
+ - This can be used to set custom attributes that are not exposed as module
+ parameters, e.g. C(mail).
+ - See the examples on how to format this parameter.
+ type: dict
+ category:
+ description:
+ - The category of the group, this is the value to assign to the LDAP
+ C(groupType) attribute.
+ - If a new group is created then C(security) will be used by default.
+ type: str
+ choices: [ distribution, security ]
+ description:
+ description:
+ - The value to be assigned to the LDAP C(description) attribute.
+ type: str
+ display_name:
+ description:
+ - The value to assign to the LDAP C(displayName) attribute.
+ type: str
+ domain_username:
+ description:
+ - The username to use when interacting with AD.
+ - If this is not set then the user Ansible used to log in with will be
+ used instead.
+ type: str
+ domain_password:
+ description:
+ - The password for C(username).
+ type: str
+ domain_server:
+ description:
+ - Specifies the Active Directory Domain Services instance to connect to.
+ - Can be in the form of an FQDN or NetBIOS name.
+ - If not specified then the value is based on the domain of the computer
+ running PowerShell.
+ type: str
+ ignore_protection:
+ description:
+ - Will ignore the C(ProtectedFromAccidentalDeletion) flag when deleting or
+ moving a group.
+ - The module will fail if one of these actions need to occur and this value
+ is set to C(no).
+ type: bool
+ default: no
+ managed_by:
+ description:
+ - The value to be assigned to the LDAP C(managedBy) attribute.
+ - This value can be in the forms C(Distinguished Name), C(objectGUID),
+ C(objectSid) or C(sAMAccountName), see examples for more details.
+ type: str
+ name:
+ description:
+ - The name of the group to create, modify or remove.
+ - This value can be in the forms C(Distinguished Name), C(objectGUID),
+ C(objectSid) or C(sAMAccountName), see examples for more details.
+ type: str
+ required: yes
+ organizational_unit:
+ description:
+ - The full LDAP path to create or move the group to.
+ - This should be the path to the parent object to create or move the group to.
+ - See examples for details of how this path is formed.
+ type: str
+ aliases: [ ou, path ]
+ protect:
+ description:
+ - Will set the C(ProtectedFromAccidentalDeletion) flag based on this value.
+ - This flag stops a user from deleting or moving a group to a different
+ path.
+ type: bool
+ scope:
+ description:
+ - The scope of the group.
+ - If C(state=present) and the group doesn't exist then this must be set.
+ type: str
+ choices: [domainlocal, global, universal]
+ state:
+ description:
+ - If C(state=present) this module will ensure the group is created and is
+ configured accordingly.
+ - If C(state=absent) this module will delete the group if it exists
+ type: str
+ choices: [ absent, present ]
+ default: present
+notes:
+- This must be run on a host that has the ActiveDirectory powershell module installed.
+seealso:
+- module: ansible.windows.win_domain
+- module: ansible.windows.win_domain_controller
+- module: community.windows.win_domain_computer
+- module: ansible.windows.win_domain_membership
+- module: community.windows.win_domain_user
+- module: ansible.windows.win_group
+- module: ansible.windows.win_group_membership
+author:
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = r'''
+- name: Ensure the group Cow exists using sAMAccountName
+ community.windows.win_domain_group:
+ name: Cow
+ scope: global
+ path: OU=groups,DC=ansible,DC=local
+
+- name: Ensure the group Cow doesn't exist using the Distinguished Name
+ community.windows.win_domain_group:
+ name: CN=Cow,OU=groups,DC=ansible,DC=local
+ state: absent
+
+- name: Delete group ignoring the protection flag
+ community.windows.win_domain_group:
+ name: Cow
+ state: absent
+ ignore_protection: yes
+
+- name: Create group with delete protection enabled and custom attributes
+ community.windows.win_domain_group:
+ name: Ansible Users
+ scope: domainlocal
+ category: security
+ attributes:
+ mail: helpdesk@ansible.com
+ wWWHomePage: www.ansible.com
+ ignore_protection: yes
+
+- name: Change the OU of a group using the SID and ignore the protection flag
+ community.windows.win_domain_group:
+ name: S-1-5-21-2171456218-3732823212-122182344-1189
+ scope: global
+ organizational_unit: OU=groups,DC=ansible,DC=local
+ ignore_protection: yes
+
+- name: Add managed_by user
+ community.windows.win_domain_group:
+ name: Group Name Here
+ managed_by: Domain Admins
+
+- name: Add group and specify the AD domain services to use for the create
+ community.windows.win_domain_group:
+ name: Test Group
+ domain_username: user@CORP.ANSIBLE.COM
+ domain_password: Password01!
+ domain_server: corp-DC12.corp.ansible.com
+ scope: domainlocal
+'''
+
+RETURN = r'''
+attributes:
+ description: Custom attributes that were set by the module. This does not
+ show all the custom attributes rather just the ones that were set by the
+ module.
+ returned: group exists and attributes are set on the module invocation
+ type: dict
+ sample:
+ mail: 'helpdesk@ansible.com'
+ wWWHomePage: 'www.ansible.com'
+canonical_name:
+ description: The canonical name of the group.
+ returned: group exists
+ type: str
+ sample: ansible.local/groups/Cow
+category:
+ description: The Group type value of the group, i.e. Security or Distribution.
+ returned: group exists
+ type: str
+ sample: Security
+description:
+ description: The Description of the group.
+ returned: group exists
+ type: str
+ sample: Group Description
+display_name:
+ description: The Display name of the group.
+ returned: group exists
+ type: str
+ sample: Users who connect through RDP
+distinguished_name:
+ description: The full Distinguished Name of the group.
+ returned: group exists
+ type: str
+ sample: CN=Cow,OU=groups,DC=ansible,DC=local
+group_scope:
+ description: The Group scope value of the group.
+ returned: group exists
+ type: str
+ sample: Universal
+guid:
+ description: The guid of the group.
+ returned: group exists
+ type: str
+ sample: 512a9adb-3fc0-4a26-9df0-e6ea1740cf45
+managed_by:
+ description: The full Distinguished Name of the AD object that is set on the
+ managedBy attribute.
+ returned: group exists
+ type: str
+ sample: CN=Domain Admins,CN=Users,DC=ansible,DC=local
+name:
+ description: The name of the group.
+ returned: group exists
+ type: str
+ sample: Cow
+protected_from_accidental_deletion:
+ description: Whether the group is protected from accidental deletion.
+ returned: group exists
+ type: bool
+ sample: true
+sid:
+ description: The Security ID of the group.
+ returned: group exists
+ type: str
+ sample: S-1-5-21-2171456218-3732823212-122182344-1189
+created:
+ description: Whether a group was created
+ returned: always
+ type: bool
+ sample: true
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_domain_group_membership.ps1 b/ansible_collections/community/windows/plugins/modules/win_domain_group_membership.ps1
new file mode 100644
index 00000000..6393e3c5
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_domain_group_membership.ps1
@@ -0,0 +1,136 @@
+#!powershell
+
+# Copyright: (c) 2019, Marius Rieder <marius.rieder@scs.ch>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+try {
+ Import-Module ActiveDirectory
+}
+catch {
+ Fail-Json -obj @{} -message "win_domain_group_membership requires the ActiveDirectory PS module to be installed"
+}
+
+$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
+
+# Module control parameters
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present", "absent", "pure"
+$domain_username = Get-AnsibleParam -obj $params -name "domain_username" -type "str"
+$domain_password = Get-AnsibleParam -obj $params -name "domain_password" -type "str" -failifempty ($null -ne $domain_username)
+$domain_server = Get-AnsibleParam -obj $params -name "domain_server" -type "str"
+
+# Group Membership parameters
+$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
+$members = Get-AnsibleParam -obj $params -name "members" -type "list" -failifempty $true
+
+# Filter ADObjects by ObjectClass
+$ad_object_class_filter = "(ObjectClass -eq 'user' -or ObjectClass -eq 'group' -or ObjectClass -eq 'computer' -or ObjectClass -eq 'msDS-ManagedServiceAccount')"
+
+$extra_args = @{}
+if ($null -ne $domain_username) {
+ $domain_password = ConvertTo-SecureString $domain_password -AsPlainText -Force
+ $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $domain_username, $domain_password
+ $extra_args.Credential = $credential
+}
+if ($null -ne $domain_server) {
+ $extra_args.Server = $domain_server
+}
+
+$ADGroup = Get-ADGroup -Identity $name @extra_args
+
+$result = @{
+ changed = $false
+ added = [System.Collections.Generic.List`1[String]]@()
+ removed = [System.Collections.Generic.List`1[String]]@()
+}
+if ($diff_mode) {
+ $result.diff = @{}
+}
+
+$filter = "(memberOf=$($ADGroup.DistinguishedName))"
+
+$members_before = Get-ADObject -LDAPFilter $filter -Properties sAMAccountName, objectSID @extra_args
+$pure_members = [System.Collections.Generic.List`1[String]]@()
+
+foreach ($member in $members) {
+ $extra_member_args = $extra_args.Clone()
+ if ($member -match "\\") {
+ $extra_member_args.Server = $member.Split("\")[0]
+ $member = $member.Split("\")[1]
+ }
+ $group_member = Get-ADObject -Filter "SamAccountName -eq '$member' -and $ad_object_class_filter" -Properties objectSid, sAMAccountName @extra_member_args
+ if (!$group_member) {
+ Fail-Json -obj $result "Could not find domain user, group, service account or computer named $member"
+ }
+
+ if ($state -eq "pure") {
+ $pure_members.Add($group_member.objectSid)
+ }
+
+ $user_in_group = $false
+ foreach ($current_member in $members_before) {
+ if ($current_member.objectSid -eq $group_member.objectSid) {
+ $user_in_group = $true
+ break
+ }
+ }
+
+ if ($state -in @("present", "pure") -and !$user_in_group) {
+ Add-ADPrincipalGroupMembership -Identity $group_member -MemberOf $ADGroup -WhatIf:$check_mode @extra_member_args
+ $result.added.Add($group_member.SamAccountName)
+ $result.changed = $true
+ }
+ elseif ($state -eq "absent" -and $user_in_group) {
+ Remove-ADPrincipalGroupMembership -Identity $group_member -MemberOf $ADGroup -WhatIf:$check_mode -Confirm:$False @extra_member_args
+ $result.removed.Add($group_member.SamAccountName)
+ $result.changed = $true
+ }
+}
+
+if ($state -eq "pure") {
+ # Perform removals for existing group members not defined in $members
+ $current_members = Get-ADObject -LDAPFilter $filter -Properties sAMAccountName, objectSID @extra_args
+
+ foreach ($current_member in $current_members) {
+ $user_to_remove = $true
+ foreach ($pure_member in $pure_members) {
+ if ($pure_member -eq $current_member.objectSid) {
+ $user_to_remove = $false
+ break
+ }
+ }
+
+ if ($user_to_remove) {
+ Remove-ADPrincipalGroupMembership -Identity $current_member -MemberOf $ADGroup -WhatIf:$check_mode -Confirm:$False @extra_member_args
+ $result.removed.Add($current_member.SamAccountName)
+ $result.changed = $true
+ }
+ }
+}
+
+$final_members = Get-ADObject -LDAPFilter $filter -Properties sAMAccountName, objectSID @extra_args
+
+if ($final_members) {
+ $result.members = [Array]$final_members.SamAccountName
+}
+else {
+ $result.members = @()
+}
+
+if ($diff_mode -and $result.changed) {
+ $result.diff.before = $members_before.SamAccountName | Out-String
+ if (!$check_mode) {
+ $result.diff.after = [Array]$final_members.SamAccountName | Out-String
+ }
+ else {
+ $after = [System.Collections.Generic.List`1[String]]$result.members
+ $result.removed | ForEach-Object { $after.Remove($_) > $null }
+ $after.AddRange($result.added)
+ $result.diff.after = $after | Out-String
+ }
+}
+
+Exit-Json -obj $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_domain_group_membership.py b/ansible_collections/community/windows/plugins/modules/win_domain_group_membership.py
new file mode 100644
index 00000000..5e10ac3b
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_domain_group_membership.py
@@ -0,0 +1,126 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2017, Andrew Saraceni <andrew.saraceni@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_domain_group_membership
+short_description: Manage Windows domain group membership
+description:
+ - Allows the addition and removal of domain users
+ and domain groups from/to a domain group.
+options:
+ name:
+ description:
+ - Name of the domain group to manage membership on.
+ type: str
+ required: yes
+ members:
+ description:
+ - A list of members to ensure are present/absent from the group.
+ - The given names must be a SamAccountName of a user, group, service account, or computer.
+ - For computers, you must add "$" after the name; for example, to add "Mycomputer" to a group, use "Mycomputer$" as the member.
+ - If the member object is part of another domain in a multi-domain forest, you must add the domain and "\" in front of the name.
+ type: list
+ elements: str
+ required: yes
+ state:
+ description:
+ - Desired state of the members in the group.
+ - When C(state) is C(pure), only the members specified will exist,
+ and all other existing members not specified are removed.
+ type: str
+ choices: [ absent, present, pure ]
+ default: present
+ domain_username:
+ description:
+ - The username to use when interacting with AD.
+ - If this is not set then the user Ansible used to log in with will be
+ used instead when using CredSSP or Kerberos with credential delegation.
+ type: str
+ domain_password:
+ description:
+ - The password for I(username).
+ type: str
+ domain_server:
+ description:
+ - Specifies the Active Directory Domain Services instance to connect to.
+ - Can be in the form of an FQDN or NetBIOS name.
+ - If not specified then the value is based on the domain of the computer
+ running PowerShell.
+ type: str
+notes:
+- This must be run on a host that has the ActiveDirectory powershell module installed.
+seealso:
+- module: community.windows.win_domain_user
+- module: community.windows.win_domain_group
+author:
+ - Marius Rieder (@jiuka)
+'''
+
+EXAMPLES = r'''
+- name: Add a domain user/group to a domain group
+ community.windows.win_domain_group_membership:
+ name: Foo
+ members:
+ - Bar
+ state: present
+
+- name: Remove a domain user/group from a domain group
+ community.windows.win_domain_group_membership:
+ name: Foo
+ members:
+ - Bar
+ state: absent
+
+- name: Ensure only a domain user/group exists in a domain group
+ community.windows.win_domain_group_membership:
+ name: Foo
+ members:
+ - Bar
+ state: pure
+
+- name: Add a computer to a domain group
+ community.windows.win_domain_group_membership:
+ name: Foo
+ members:
+ - DESKTOP$
+ state: present
+
+- name: Add a domain user/group from another Domain in the multi-domain forest to a domain group
+ community.windows.win_domain_group_membership:
+ domain_server: DomainAAA.cloud
+ name: GroupinDomainAAA
+ members:
+ - DomainBBB.cloud\UserInDomainBBB
+ state: Present
+
+'''
+
+RETURN = r'''
+name:
+ description: The name of the target domain group.
+ returned: always
+ type: str
+ sample: Domain-Admins
+added:
+ description: A list of members added when C(state) is C(present) or
+ C(pure); this is empty if no members are added.
+ returned: success and C(state) is C(present) or C(pure)
+ type: list
+ sample: ["UserName", "GroupName"]
+removed:
+ description: A list of members removed when C(state) is C(absent) or
+ C(pure); this is empty if no members are removed.
+ returned: success and C(state) is C(absent) or C(pure)
+ type: list
+ sample: ["UserName", "GroupName"]
+members:
+ description: A list of all domain group members at completion; this is empty
+ if the group contains no members.
+ returned: success
+ type: list
+ sample: ["UserName", "GroupName"]
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_domain_object_info.ps1 b/ansible_collections/community/windows/plugins/modules/win_domain_object_info.ps1
new file mode 100644
index 00000000..12347946
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_domain_object_info.ps1
@@ -0,0 +1,284 @@
+#!powershell
+
+# Copyright: (c) 2020, 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.AddType
+
+$spec = @{
+ options = @{
+ domain_password = @{ type = 'str'; no_log = $true }
+ domain_server = @{ type = 'str' }
+ domain_username = @{ type = 'str' }
+ filter = @{ type = 'str' }
+ identity = @{ type = 'str' }
+ include_deleted = @{ type = 'bool'; default = $false }
+ ldap_filter = @{ type = 'str' }
+ properties = @{ type = 'list'; elements = 'str' }
+ search_base = @{ type = 'str' }
+ search_scope = @{ type = 'str'; choices = @('base', 'one_level', 'subtree') }
+ }
+ supports_check_mode = $true
+ mutually_exclusive = @(
+ @('filter', 'identity', 'ldap_filter'),
+ @('identity', 'search_base'),
+ @('identity', 'search_scope')
+ )
+ required_one_of = @(
+ , @('filter', 'identity', 'ldap_filter')
+ )
+ required_together = @(, @('domain_username', 'domain_password'))
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$module.Result.objects = @() # Always ensure this is returned even in a failure.
+
+$domainServer = $module.Params.domain_server
+$domainPassword = $module.Params.domain_password
+$domainUsername = $module.Params.domain_username
+$filter = $module.Params.filter
+$identity = $module.Params.identity
+$includeDeleted = $module.Params.include_deleted
+$ldapFilter = $module.Params.ldap_filter
+$properties = $module.Params.properties
+$searchBase = $module.Params.search_base
+$searchScope = $module.Params.search_scope
+
+$credential = $null
+if ($domainUsername) {
+ $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @(
+ $domainUsername,
+ (ConvertTo-SecureString -AsPlainText -Force -String $domainPassword)
+ )
+}
+
+Add-CSharpType -References @'
+using System;
+
+namespace Ansible.WinDomainObjectInfo
+{
+ [Flags]
+ public enum UserAccountControl : int
+ {
+ ADS_UF_SCRIPT = 0x00000001,
+ ADS_UF_ACCOUNTDISABLE = 0x00000002,
+ ADS_UF_HOMEDIR_REQUIRED = 0x00000008,
+ ADS_UF_LOCKOUT = 0x00000010,
+ ADS_UF_PASSWD_NOTREQD = 0x00000020,
+ ADS_UF_PASSWD_CANT_CHANGE = 0x00000040,
+ ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED = 0x00000080,
+ ADS_UF_TEMP_DUPLICATE_ACCOUNT = 0x00000100,
+ ADS_UF_NORMAL_ACCOUNT = 0x00000200,
+ ADS_UF_INTERDOMAIN_TRUST_ACCOUNT = 0x00000800,
+ ADS_UF_WORKSTATION_TRUST_ACCOUNT = 0x00001000,
+ ADS_UF_SERVER_TRUST_ACCOUNT = 0x00002000,
+ ADS_UF_DONT_EXPIRE_PASSWD = 0x00010000,
+ ADS_UF_MNS_LOGON_ACCOUNT = 0x00020000,
+ ADS_UF_SMARTCARD_REQUIRED = 0x00040000,
+ ADS_UF_TRUSTED_FOR_DELEGATION = 0x00080000,
+ ADS_UF_NOT_DELEGATED = 0x00100000,
+ ADS_UF_USE_DES_KEY_ONLY = 0x00200000,
+ ADS_UF_DONT_REQUIRE_PREAUTH = 0x00400000,
+ ADS_UF_PASSWORD_EXPIRED = 0x00800000,
+ ADS_UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION = 0x01000000,
+ }
+
+ public enum sAMAccountType : int
+ {
+ SAM_DOMAIN_OBJECT = 0x00000000,
+ SAM_GROUP_OBJECT = 0x10000000,
+ SAM_NON_SECURITY_GROUP_OBJECT = 0x10000001,
+ SAM_ALIAS_OBJECT = 0x20000000,
+ SAM_NON_SECURITY_ALIAS_OBJECT = 0x20000001,
+ SAM_USER_OBJECT = 0x30000000,
+ SAM_NORMAL_USER_ACCOUNT = 0x30000000,
+ SAM_MACHINE_ACCOUNT = 0x30000001,
+ SAM_TRUST_ACCOUNT = 0x30000002,
+ SAM_APP_BASIC_GROUP = 0x40000000,
+ SAM_APP_QUERY_GROUP = 0x40000001,
+ SAM_ACCOUNT_TYPE_MAX = 0x7fffffff,
+ }
+}
+'@
+
+Function ConvertTo-OutputValue {
+ [CmdletBinding()]
+ Param (
+ [Parameter(Mandatory = $true)]
+ [AllowNull()]
+ [Object]
+ $InputObject
+ )
+
+ if ($InputObject -is [System.Security.Principal.SecurityIdentifier]) {
+ # Syntax: SID - Only serialize the SID as a string and not the other metadata properties.
+ $sidInfo = @{
+ Sid = $InputObject.Value
+ }
+
+ # Try and map the SID to the account name, this may fail if the SID is invalid or not mappable.
+ try {
+ $sidInfo.Name = $InputObject.Translate([System.Security.Principal.NTAccount]).Value
+ }
+ catch [System.Security.Principal.IdentityNotMappedException] {
+ $sidInfo.Name = $null
+ }
+
+ $sidInfo
+ }
+ elseif ($InputObject -is [Byte[]]) {
+ # Syntax: Octet String - By default will serialize as a list of decimal values per byte, instead return a
+ # Base64 string as Ansible can easily parse that.
+ [System.Convert]::ToBase64String($InputObject)
+ }
+ elseif ($InputObject -is [DateTime]) {
+ # Syntax: UTC Coded Time - .NET DateTimes serialized as in the form "Date(FILETIME)" which isn't easily
+ # parsable by Ansible, instead return as an ISO 8601 string in the UTC timezone.
+ [TimeZoneInfo]::ConvertTimeToUtc($InputObject).ToString("o")
+ }
+ elseif ($InputObject -is [System.Security.AccessControl.ObjectSecurity]) {
+ # Complex object which isn't easily serializable. Instead we should just return the SDDL string. If a user
+ # needs to parse this then they really need to reprocess the SDDL string and process their results on another
+ # win_shell task.
+ $InputObject.GetSecurityDescriptorSddlForm(([System.Security.AccessControl.AccessControlSections]::All))
+ }
+ else {
+ # Syntax: (All Others) - The default serialization handling of other syntaxes are fine, don't do anything.
+ $InputObject
+ }
+}
+
+<#
+Calling Get-ADObject that returns multiple objects with -Properties * will only return the properties that were set on
+the first found object. To counter this problem we will first call Get-ADObject to list all the objects that match the
+filter specified then get the properties on each object.
+#>
+
+$commonParams = @{
+ IncludeDeletedObjects = $includeDeleted
+}
+
+if ($credential) {
+ $commonParams.Credential = $credential
+}
+
+if ($domainServer) {
+ $commonParams.Server = $domainServer
+}
+
+# First get the IDs for all the AD objects that match the filter specified.
+$getParams = @{
+ Properties = @('DistinguishedName', 'ObjectGUID')
+}
+
+if ($filter) {
+ $getParams.Filter = $filter
+}
+elseif ($identity) {
+ $getParams.Identity = $identity
+}
+elseif ($ldapFilter) {
+ $getParams.LDAPFilter = $ldapFilter
+}
+
+# Explicit check on $null as an empty string is different from not being set.
+if ($null -ne $searchBase) {
+ $getParams.SearchBase = $searchbase
+}
+
+if ($searchScope) {
+ $getParams.SearchScope = switch ($searchScope) {
+ base { 'Base' }
+ one_level { 'OneLevel' }
+ subtree { 'Subtree' }
+ }
+}
+
+try {
+ # We run this in a custom PowerShell pipeline so that users of this module can't use any of the variables defined
+ # above in their filter. While the cmdlet won't execute sub expressions we don't want anyone implicitly relying on
+ # a defined variable in this module in case we ever change the name or remove it.
+ $ps = [PowerShell]::Create()
+ $null = $ps.AddCommand('Get-ADObject').AddParameters($commonParams).AddParameters($getParams)
+ $null = $ps.AddCommand('Select-Object').AddParameter('Property', @('DistinguishedName', 'ObjectGUID'))
+
+ $foundGuids = @($ps.Invoke())
+}
+catch {
+ # Because we ran in a pipeline we can't catch ADIdentityNotFoundException. Instead just get the base exception and
+ # do the error checking on that.
+ if ($_.Exception.GetBaseException() -is [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException]) {
+ $foundGuids = @()
+ }
+ else {
+ # The exception is from the .Invoke() call, compare on the InnerException which was what was actually raised by
+ # the pipeline.
+ $innerException = $_.Exception.InnerException.InnerException
+ if ($innerException -is [Microsoft.ActiveDirectory.Management.ADServerDownException]) {
+ # Point users in the direction of the double hop problem as that is what is typically the cause of this.
+ $msg = "Failed to contact the AD server, this could be caused by the double hop problem over WinRM. "
+ $msg += "Try using the module with auth as Kerberos with credential delegation or CredSSP, become, or "
+ $msg += "defining the domain_username and domain_password module parameters."
+ $module.FailJson($msg, $innerException)
+ }
+ else {
+ throw $innerException
+ }
+ }
+}
+
+$getParams = @{}
+if ($properties) {
+ $getParams.Properties = $properties
+}
+$module.Result.objects = @(foreach ($adId in $foundGuids) {
+ try {
+ $adObject = Get-ADObject @commonParams @getParams -Identity $adId.ObjectGUID
+ }
+ catch {
+ $msg = "Failed to retrieve properties for AD Object '$($adId.DistinguishedName)': $($_.Exception.Message)"
+ $module.Warn($msg)
+ continue
+ }
+
+ $propertyNames = $adObject.PropertyNames
+ $propertyNames += ($properties | Where-Object { $_ -ne '*' })
+
+ # Now process each property to an easy to represent string
+ $filteredObject = [Ordered]@{}
+ foreach ($name in ($propertyNames | Sort-Object)) {
+ # In the case of explicit properties that were asked for but weren't set, Get-ADObject won't actually return
+ # the property so this is a defensive check against that scenario.
+ if (-not $adObject.PSObject.Properties.Name.Contains($name)) {
+ $filteredObject.$name = $null
+ continue
+ }
+
+ $value = $adObject.$name
+ if ($value -is [Microsoft.ActiveDirectory.Management.ADPropertyValueCollection]) {
+ $value = foreach ($v in $value) {
+ ConvertTo-OutputValue -InputObject $v
+ }
+ }
+ else {
+ $value = ConvertTo-OutputValue -InputObject $value
+ }
+ $filteredObject.$name = $value
+
+ # For these 2 properties, add an _AnsibleFlags attribute which contains the enum strings that are set.
+ if ($name -eq 'sAMAccountType') {
+ $enumValue = [Ansible.WinDomainObjectInfo.sAMAccountType]$value
+ $filteredObject.'sAMAccountType_AnsibleFlags' = $enumValue.ToString() -split ', '
+ }
+ elseif ($name -eq 'userAccountControl') {
+ $enumValue = [Ansible.WinDomainObjectInfo.UserAccountControl]$value
+ $filteredObject.'userAccountControl_AnsibleFlags' = $enumValue.ToString() -split ', '
+ }
+ }
+
+ $filteredObject
+ })
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_domain_object_info.py b/ansible_collections/community/windows/plugins/modules/win_domain_object_info.py
new file mode 100644
index 00000000..200df533
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_domain_object_info.py
@@ -0,0 +1,157 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2020, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_domain_object_info
+short_description: Gather information an Active Directory object
+description:
+- Gather information about multiple Active Directory object(s).
+options:
+ domain_password:
+ description:
+ - The password for C(domain_username).
+ type: str
+ domain_server:
+ description:
+ - Specified the Active Directory Domain Services instance to connect to.
+ - Can be in the form of an FQDN or NetBIOS name.
+ - If not specified then the value is based on the default domain of the computer running PowerShell.
+ type: str
+ domain_username:
+ description:
+ - The username to use when interacting with AD.
+ - If this is not set then the user that is used for authentication will be the connection user.
+ - Ansible will be unable to use the connection user unless auth is Kerberos with credential delegation or CredSSP,
+ or become is used on the task.
+ type: str
+ filter:
+ description:
+ - Specifies a query string using the PowerShell Expression Language syntax.
+ - This follows the same rules and formatting as the C(-Filter) parameter for the PowerShell AD cmdlets exception
+ there is no variable substitutions.
+ - This is mutually exclusive with I(identity) and I(ldap_filter).
+ type: str
+ identity:
+ description:
+ - Specifies a single Active Directory object by its distinguished name or its object GUID.
+ - This is mutually exclusive with I(filter) and I(ldap_filter).
+ - This cannot be used with either the I(search_base) or I(search_scope) options.
+ type: str
+ include_deleted:
+ description:
+ - Also search for deleted Active Directory objects.
+ default: no
+ type: bool
+ ldap_filter:
+ description:
+ - Like I(filter) but this is a tradiitional LDAP query string to filter the objects to return.
+ - This is mutually exclusive with I(filter) and I(identity).
+ type: str
+ properties:
+ description:
+ - A list of properties to return.
+ - If a property is C(*), all properties that have a set value on the AD object will be returned.
+ - If a property is valid on the object but not set, it is only returned if defined explicitly in this option list.
+ - The properties C(DistinguishedName), C(Name), C(ObjectClass), and C(ObjectGUID) are always returned.
+ - Specifying multiple properties can have a performance impact, it is best to only return what is needed.
+ - If an invalid property is specified then the module will display a warning for each object it is invalid on.
+ type: list
+ elements: str
+ search_base:
+ description:
+ - Specify the Active Directory path to search for objects in.
+ - This cannot be set with I(identity).
+ - By default the search base is the default naming context of the target AD instance which is the DN returned by
+ "(Get-ADRootDSE).defaultNamingContext".
+ type: str
+ search_scope:
+ description:
+ - Specify the scope of when searching for an object in the C(search_base).
+ - C(base) will limit the search to the base object so the maximum number of objects returned is always one. This
+ will not search any objects inside a container..
+ - C(one_level) will search the current path and any immediate objects in that path.
+ - C(subtree) will search the current path and all objects of that path recursively.
+ - This cannot be set with I(identity).
+ choices:
+ - base
+ - one_level
+ - subtree
+ type: str
+notes:
+- The C(sAMAccountType_AnsibleFlags) and C(userAccountControl_AnsibleFlags) return property is something set by the
+ module itself as an easy way to view what those flags represent. These properties cannot be used as part of the
+ I(filter) or I(ldap_filter) and are automatically added if those properties were requested.
+author:
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = r'''
+- name: Get all properties for the specified account using its DistinguishedName
+ community.windows.win_domain_object_info:
+ identity: CN=Username,CN=Users,DC=domain,DC=com
+ properties: '*'
+
+- name: Get the SID for all user accounts as a filter
+ community.windows.win_domain_object_info:
+ filter: ObjectClass -eq 'user' -and objectCategory -eq 'Person'
+ properties:
+ - objectSid
+
+- name: Get the SID for all user accounts as a LDAP filter
+ community.windows.win_domain_object_info:
+ ldap_filter: (&(objectClass=user)(objectCategory=Person))
+ properties:
+ - objectSid
+
+- name: Search all computer accounts in a specific path that were added after February 1st
+ community.windows.win_domain_object_info:
+ filter: objectClass -eq 'computer' -and whenCreated -gt '20200201000000.0Z'
+ properties: '*'
+ search_scope: one_level
+ search_base: CN=Computers,DC=domain,DC=com
+'''
+
+RETURN = r'''
+objects:
+ description:
+ - A list of dictionaries that are the Active Directory objects found and the properties requested.
+ - The dict's keys are the property name and the value is the value for the property.
+ - All date properties are return in the ISO 8601 format in the UTC timezone.
+ - All SID properties are returned as a dict with the keys C(Sid) as the SID string and C(Name) as the translated SID
+ account name.
+ - All byte properties are returned as a base64 string.
+ - All security descriptor properties are returned as the SDDL string of that descriptor.
+ - The properties C(DistinguishedName), C(Name), C(ObjectClass), and C(ObjectGUID) are always returned.
+ returned: always
+ type: list
+ elements: dict
+ sample: |
+ [{
+ "accountExpires": 0,
+ "adminCount": 1,
+ "CanonicalName": "domain.com/Users/Administrator",
+ "CN": "Administrator",
+ "Created": "2020-01-13T09:03:22.0000000Z",
+ "Description": "Built-in account for administering computer/domain",
+ "DisplayName": null,
+ "DistinguishedName": "CN=Administrator,CN=Users,DC=domain,DC=com",
+ "memberOf": [
+ "CN=Group Policy Creator Owners,CN=Users,DC=domain,DC=com",
+ "CN=Domain Admins",CN=Users,DC=domain,DC=com"
+ ],
+ "Name": "Administrator",
+ "nTSecurityDescriptor": "O:DAG:DAD:PAI(A;;LCRPLORC;;;AU)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;SY)(A;;CCDCLCSWRPWPLOCRSDRCWDWO;;;BA)",
+ "ObjectCategory": "CN=Person,CN=Schema,CN=Configuration,DC=domain,DC=com",
+ "ObjectClass": "user",
+ "ObjectGUID": "c8c6569e-4688-4f3c-8462-afc4ff60817b",
+ "objectSid": {
+ "Sid": "S-1-5-21-2959096244-3298113601-420842770-500",
+ "Name": "DOMAIN\Administrator"
+ },
+ "sAMAccountName": "Administrator",
+ }]
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_domain_ou.ps1 b/ansible_collections/community/windows/plugins/modules/win_domain_ou.ps1
new file mode 100644
index 00000000..ee6cdc45
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_domain_ou.ps1
@@ -0,0 +1,253 @@
+#!powershell
+
+# Copyright: (c) 2020 VMware, Inc. All Rights Reserved.
+# SPDX-License-Identifier: GPL-3.0-only
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#Requires -Module ActiveDirectory
+
+$spec = @{
+ options = @{
+ state = @{ type = "str"; choices = @("absent", "present"); default = "present" }
+ name = @{ type = "str"; required = $true }
+ protected = @{ type = "bool"; default = $false }
+ path = @{ type = "str"; required = $false }
+ filter = @{type = "str"; default = '*' }
+ recursive = @{ type = "bool"; default = $false }
+ domain_username = @{ type = "str"; }
+ domain_password = @{ type = "str"; no_log = $true }
+ domain_server = @{ type = "str" }
+ properties = @{ type = "dict" }
+ }
+ required_together = @(
+ , @('domain_password', 'domain_username')
+ )
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$extra_args = @{}
+$onboard_extra_args = @{}
+if ($null -ne $module.Params.domain_username) {
+ $domain_password = ConvertTo-SecureString $module.Params.domain_password -AsPlainText -Force
+ $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $module.Params.domain_username, $domain_password
+ $extra_args.Credential = $credential
+ $onboard_extra_args.Credential = $credential
+}
+if ($null -ne $module.Params.domain_server) {
+ $extra_args.Server = $module.Params.domain_server
+ $onboard_extra_args.Server = $module.Params.domain_server
+}
+if ($module.Params.properties.count -ne 0) {
+ $Properties = New-Object Collections.Generic.List[string]
+ $module.Params.properties.Keys | Foreach-Object {
+ $Properties.Add($_)
+ }
+ $extra_args.Properties = $Properties
+}
+else {
+ $extra_args.Properties = '*'
+ $Properties = '*'
+}
+
+$extra_args.Filter = $module.Params.filter
+$check_mode = $module.CheckMode
+$name = $module.Params.name
+$protected = $module.Params.protected
+$path = $module.Params.path
+$state = $module.Params.state
+$recursive = $module.Params.recursive
+
+# setup Dynamic Params
+$params = @{}
+if ($module.Params.properties.count -ne 0) {
+ $module.Params.properties.Keys | ForEach-Object {
+ $params.Add($_, $module.Params.properties.Item($_))
+ }
+}
+
+Function Get-SimulatedOu {
+ Param($Object)
+ $ou = @{
+ Name = $Object.name
+ DistinguishedName = "OU=$($Object.name),$($Object.path)"
+ ProtectedFromAccidentalDeletion = $Object.protected
+ Properties = New-Object Collections.Generic.List[string]
+ }
+ $ou.Properties.Add("Name")
+ $ou.Properties.Add("DistinguishedName")
+ $ou.Properties.Add("ProtectedFromAccidentalDeletion")
+ if ($Object.Params.properties.Count -ne 0) {
+ $Object.Params.properties.Keys | ForEach-Object {
+ $property = $_
+ $module.Result.simulate_property = $property
+ $ou.Add($property, $Object.Params.properties.Item($property))
+ $ou.Properties.Add($property)
+ }
+ }
+ # convert to psobject & return
+ [PSCustomObject]$ou
+}
+
+Function Get-OuObject {
+ Param([PSObject]$Object)
+ $obj = $Object | Select-Object -Property * -ExcludeProperty nTSecurityDescriptor | ConvertTo-Json -Depth 1 | ConvertFrom-Json
+ return $obj
+}
+
+# attempt import of module
+Try { Import-Module ActiveDirectory }
+Catch { $module.FailJson("The ActiveDirectory module failed to load properly: $($_.Exception.Message)", $_) }
+Try {
+ $all_ous = Get-ADOrganizationalUnit @extra_args
+}
+Catch { $module.FailJson("Get-ADOrganizationalUnit failed: $($_.Exception.Message)", $_) }
+
+# set path if not defined to base domain
+if ($null -eq $path) {
+ if ($($all_ous | Measure-Object | Select-Object -ExpandProperty Count) -eq 1) {
+ $matched = $all_ous.DistinguishedName -match "DC=.+"
+ }
+ elseif ($($all_ous | Measure-Object | Select-Object -ExpandProperty Count) -gt 1) {
+ $matched = $all_ous[0].DistinguishedName -match "DC=.+"
+ }
+ else {
+ $module.FailJson("Path was null and unable to determine default domain $($_.Exception.Message)")
+ }
+ if ($matched) {
+ $path = $matches.Values[0]
+ }
+ else {
+ $module.FailJson("Unable to find default domain $($_.Exception.Message)")
+ }
+}
+$module.Result.path = $path
+
+# determine if requested OU exist
+$current_ou = $false
+Try {
+ $current_ou = $all_ous | Where-Object {
+ $_.DistinguishedName -eq "OU=$name,$path" }
+ $module.Diff.before = Get-OuObject -Object $current_ou
+ $module.Result.ou = Get-OuObject $module.Diff.before
+}
+Catch {
+ $module.Diff.before = ""
+ $current_ou = $false
+}
+
+# determine if ou needs created
+if (($state -eq "present") -and (-not $current_ou)) {
+ $create_ou = $true
+}
+else {
+ $create_ou = $false
+}
+
+# determine if ou needs change
+$update_ou = $false
+if (($state -eq "present") -and ($create_ou -eq $false)) {
+ if ($module.Params.properties.Count -ne 0) {
+ $changed_properties = New-Object Collections.Generic.List[hashtable]
+ $module.Params.properties.Keys | ForEach-Object {
+ $property = $_
+ $current_value = $current_ou.Item($property)
+ $requested_value = $module.Params.properties.Item($property)
+ if (-not ($current_value -eq $requested_value) ) {
+ $changed_properties.Add(
+ @{
+ "Actual_$property" = $current_value
+ "Requested_$property" = $requested_value
+ }
+ )
+ }
+ }
+ if ($changed_properties.Count -ge 1) {
+ $update_ou = $true
+ }
+ }
+}
+
+if ($state -eq "present") {
+ # ou does not exist, create object
+ if ($create_ou) {
+ $params.Name = $name
+ $params.Path = $path
+ Try {
+ New-ADOrganizationalUnit @params @onboard_extra_args -ProtectedFromAccidentalDeletion $protected -WhatIf:$check_mode
+ }
+ Catch {
+ $module.FailJson("Failed to create organizational unit: $($_.Exception.Message)", $_)
+ }
+ $module.Result.changed = $true
+ if ($check_mode) {
+ $module.Diff.after = Get-SimulatedOu -Object $module
+ }
+ else {
+ $new_ou = Get-ADOrganizationalUnit @extra_args | Where-Object {
+ $_.DistinguishedName -eq "OU=$name,$path"
+ }
+ $module.Diff.after = Get-OuObject -Object $new_ou
+ }
+ }
+ # ou exists, update object if needed
+ if ($update_ou) {
+ Try {
+ Set-ADOrganizationalUnit -Identity "OU=$name,$path" @params @onboard_extra_args -WhatIf:$check_mode
+ $module.Result.changed = $true
+ }
+ Catch {
+ $module.FailJson("Failed to update organizational unit: $($_.Exception.Message)", $_)
+ }
+ if ($check_mode) {
+ $module.Diff.after = Get-SimulatedOu -Object $module
+ }
+ else {
+ $new_ou = Get-ADOrganizationalUnit @extra_args | Where-Object {
+ $_.DistinguishedName -eq "OU=$name,$path"
+ }
+ $module.Diff.after = Get-OuObject -Object $new_ou
+ }
+ }
+}
+
+if ($state -eq "absent") {
+ # ou exists, delete object
+ if ($current_ou) {
+ Try {
+ # override protected from accidental deletion
+ Set-ADOrganizationalUnit -Identity "OU=$name,$path" -ProtectedFromAccidentalDeletion $false @onboard_extra_args -Confirm:$False -WhatIf:$check_mode
+ $module.Result.changed = $true
+ }
+ Catch {
+ $module.FailJson("Failed to remove ProtectedFromAccidentalDeletion Lock: $($_.Exception.Message)", $_)
+ }
+ # check recursive deletion
+ if ($recursive) {
+ try {
+ Remove-ADOrganizationalUnit -Identity "OU=$name,$path" -Confirm:$False -WhatIf:$check_mode -Recursive @onboard_extra_args
+ $module.Result.changed = $true
+ $module.Diff.after = ""
+ $module.Result.ou = ""
+ }
+ catch {
+ $module.FailJson("Failed to recursively Remove-ADOrganizationalUnit $($_.Exception.Message)", $_)
+ }
+ }
+ else {
+ try {
+ Remove-ADOrganizationalUnit -Identity "OU=$name,$path" -Confirm:$False -WhatIf:$check_mode @onboard_extra_args
+ $module.Result.changed = $true
+ $module.Diff.after = ""
+ $module.Result.ou = ""
+ }
+ Catch {
+ $module.FailJson("Failed to Remove-ADOrganizationalUnit: $($_.Exception.Message)", $_)
+ }
+ }
+ }
+}
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_domain_ou.py b/ansible_collections/community/windows/plugins/modules/win_domain_ou.py
new file mode 100644
index 00000000..e144b837
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_domain_ou.py
@@ -0,0 +1,174 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2020 VMware, Inc. All Rights Reserved.
+# SPDX-License-Identifier: GPL-3.0-only
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_domain_ou
+short_description: Manage Active Directory Organizational Units
+author: ['Joe Zollo (@joezollo)', 'Larry Lane (@gamethis)']
+version_added: 1.8.0
+requirements:
+ - This module requires Windows Server 2012 or Newer
+ - Powershell ActiveDirectory Module
+description:
+ - Manage Active Directory Organizational Units
+ - Adds, Removes and Modifies Active Directory Organizational Units
+ - Task should be delegated to a Windows Active Directory Domain Controller
+options:
+ name:
+ description:
+ - The name of the Organizational Unit
+ type: str
+ required: true
+ protected:
+ description:
+ - Indicates whether to prevent the object from being deleted. When this
+ I(protected=true), you cannot delete the corresponding object without
+ changing the value of the property.
+ type: bool
+ default: false
+ path:
+ description:
+ - Specifies the X.500 path of the OU or container where the new object is
+ created.
+ - defaults to adding ou at base of domain connected to.
+ type: str
+ required: false
+ state:
+ description:
+ - Specifies the desired state of the OU.
+ - When I(state=present) the module will attempt to create the specified
+ OU if it does not already exist.
+ - When I(state=absent), the module will remove the specified OU.
+ - When I(state=absent) and I(recursive=true), the module will remove all
+ the OU and all child OU's.
+ type: str
+ default: present
+ choices: [ present, absent ]
+ recursive:
+ description:
+ - Removes the OU and any child items it contains.
+ - You must specify this parameter to remove an OU that is not empty.
+ type: bool
+ default: false
+ domain_server:
+ description:
+ - Specifies the Active Directory Domain Services instance to connect to.
+ - Can be in the form of an FQDN or NetBIOS name.
+ - If not specified then the value is based on the domain of the computer
+ running PowerShell.
+ type: str
+ domain_username:
+ description:
+ - The username to use when interacting with AD.
+ - If this is not set then the user Ansible used to log in with will be
+ used instead when using CredSSP or Kerberos with credential delegation.
+ type: str
+ domain_password:
+ type: str
+ description:
+ - The password for the domain you are accessing
+ filter:
+ type: str
+ description: filter for lookup of ou.
+ default: '*'
+ properties:
+ type: dict
+ description:
+ - Free form dict of properties for the organizational unit. Follows LDAP property names, like C(StreetAddress) or C(PostalCode).
+'''
+
+EXAMPLES = r'''
+---
+- name: Ensure OU is present & protected
+ community.windows.win_domain_ou:
+ name: AnsibleFest
+ state: present
+
+- name: Ensure OU is present & protected
+ community.windows.win_domain_ou:
+ name: EUC Users
+ path: "DC=euc,DC=vmware,DC=lan"
+ state: present
+ protected: true
+ delegate_to: win-ad1.euc.vmware.lab
+
+- name: Ensure OU is absent
+ community.windows.win_domain_ou:
+ name: EUC Users
+ path: "DC=euc,DC=vmware,DC=lan"
+ state: absent
+ delegate_to: win-ad1.euc.vmware.lab
+
+- name: Ensure OU is present with specific properties
+ community.windows.win_domain_ou:
+ name: WS1Users
+ path: "CN=EUC Users,DC=euc,DC=vmware,DC=lan"
+ protected: true
+ properties:
+ city: Sandy Springs
+ state: Georgia
+ StreetAddress: 1155 Perimeter Center West
+ country: US
+ description: EUC Business Unit
+ PostalCode: 30189
+ delegate_to: win-ad1.euc.vmware.lab
+
+- name: Ensure OU updated with new properties
+ community.windows.win_domain_ou:
+ name: WS1Users
+ path: DC=euc,DC=vmware,DC=lan
+ protected: false
+ properties:
+ city: Atlanta
+ state: Georgia
+ managedBy: jzollo@vmware.com
+ delegate_to: win-ad1.euc.vmware.lab
+'''
+
+RETURN = r'''
+path:
+ description:
+ - Base ou path used by module either when provided I(path=DC=Ansible,DC=Test) or derived by module.
+ returned: always
+ type: str
+ sample:
+ path: "DC=ansible,DC=test"
+ou:
+ description:
+ - New/Updated organizational unit parameters
+ returned: When I(state=present)
+ type: dict
+ sample:
+ AddedProperties: []
+ City: "Sandy Springs"
+ Country: null
+ DistinguishedName: "OU=VMW Atlanta,DC=ansible,DC=test"
+ LinkedGroupPolicyObjects: []
+ ManagedBy: null
+ ModifiedProperties: []
+ Name: "VMW Atlanta"
+ ObjectClass: "organizationalUnit"
+ ObjectGUID: "3e987e30-93ad-4229-8cd0-cff6a91275e4"
+ PostalCode: null
+ PropertyCount: 11
+ PropertyNames:
+ City
+ Country
+ DistinguishedName
+ LinkedGroupPolicyObjects
+ ManagedBy
+ Name
+ ObjectClass
+ ObjectGUID
+ PostalCode
+ State
+ StreetAddress
+ RemovedProperties: []
+ State: "Georgia"
+ StreetAddress: "1155 Perimeter Center West"
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_domain_user.ps1 b/ansible_collections/community/windows/plugins/modules/win_domain_user.ps1
new file mode 100644
index 00000000..e87c606c
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_domain_user.ps1
@@ -0,0 +1,564 @@
+#!powershell
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.AccessToken
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+Function Test-Credential {
+ param(
+ [String]$Username,
+ [String]$Password,
+ [String]$Domain = $null
+ )
+ if (($Username.ToCharArray()) -contains [char]'@') {
+ # UserPrincipalName
+ $Domain = $null # force $Domain to be null, to prevent undefined behaviour, as a domain name is already included in the username
+ }
+ elseif (($Username.ToCharArray()) -contains [char]'\') {
+ # Pre Win2k Account Name
+ $Domain = ($Username -split '\\')[0]
+ $Username = ($Username -split '\\', 2)[-1]
+ } # If no domain provided, so maybe local user, or domain specified separately.
+
+ try {
+ $handle = [Ansible.AccessToken.TokenUtil]::LogonUser($Username, $Domain, $Password, "Network", "Default")
+ $handle.Dispose()
+ return $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
+ )
+ $failed_codes = @(
+ 0x0000052E, # ERROR_LOGON_FAILURE
+ 0x00000532, # ERROR_PASSWORD_EXPIRED
+ 0x00000773, # ERROR_PASSWORD_MUST_CHANGE
+ 0x00000533 # ERROR_ACCOUNT_DISABLED
+ )
+
+ if ($_.Exception.NativeErrorCode -in $failed_codes) {
+ return $false
+ }
+ elseif ($_.Exception.NativeErrorCode -in $success_codes) {
+ return $true
+ }
+ else {
+ # an unknown failure, reraise exception
+ throw $_
+ }
+ }
+}
+
+$spec = @{
+ options = @{
+ name = @{ type = 'str'; required = $true }
+ state = @{
+ type = "str"
+ choices = @('present', 'absent', 'query')
+ default = "present"
+ }
+ domain_username = @{ type = 'str' }
+ domain_password = @{ type = 'str'; no_log = $true }
+ domain_server = @{ type = 'str' }
+ groups_action = @{
+ type = 'str'
+ choices = @('add', 'remove', 'replace')
+ default = 'replace'
+ }
+ spn_action = @{
+ type = 'str'
+ choices = @('add', 'remove', 'replace')
+ default = 'replace'
+ }
+ spn = @{
+ type = 'list'
+ elements = 'str'
+ aliases = @('spns')
+ }
+ description = @{ type = 'str' }
+ password = @{ type = 'str'; no_log = $true }
+ password_expired = @{ type = 'bool' }
+ password_never_expires = @{ type = 'bool' }
+ user_cannot_change_password = @{ type = 'bool' }
+ account_locked = @{ type = 'bool' }
+ groups = @{ type = 'list'; elements = 'str' }
+ groups_missing_behaviour = @{ type = 'str'; choices = "fail", "ignore", "warn"; default = "fail" }
+ enabled = @{ type = 'bool'; default = $true }
+ path = @{ type = 'str' }
+ upn = @{ type = 'str' }
+ sam_account_name = @{ type = 'str' }
+ identity = @{ type = 'str' }
+ firstname = @{ type = 'str' }
+ surname = @{ type = 'str'; aliases = @('lastname') }
+ display_name = @{ type = 'str' }
+ company = @{ type = 'str' }
+ email = @{ type = 'str' }
+ street = @{ type = 'str' }
+ city = @{ type = 'str' }
+ state_province = @{ type = 'str' }
+ postal_code = @{ type = 'str' }
+ country = @{ type = 'str' }
+ attributes = @{ type = 'dict' }
+ delegates = @{
+ type = 'list'
+ elements = 'str'
+ aliases = @('principals_allowed_to_delegate')
+ }
+ update_password = @{
+ type = 'str'
+ choices = @('always', 'on_create', 'when_changed')
+ default = 'always'
+ }
+ }
+ required_together = @(
+ , @("domain_username", "domain_password")
+ )
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+$check_mode = $module.CheckMode
+
+$module.Result.created = $false
+$module.Result.password_updated = $false
+
+try {
+ Import-Module ActiveDirectory
+}
+catch {
+ $msg = "Failed to import ActiveDirectory PowerShell module."
+ $module.FailJson($msg, $_)
+}
+
+# Module control parameters
+$state = $module.Params.state
+$update_password = $module.Params.update_password
+$groups_action = $module.Params.groups_action
+$domain_username = $module.Params.domain_username
+$domain_password = $module.Params.domain_password
+$domain_server = $module.Params.domain_server
+
+# User account parameters
+$name = $module.Params.name
+$description = $module.Params.description
+$password = $module.Params.password
+$password_expired = $module.Params.password_expired
+$password_never_expires = $module.Params.password_never_expires
+$user_cannot_change_password = $module.Params.user_cannot_change_password
+$account_locked = $module.Params.account_locked
+$groups = $module.Params.groups
+$groups_missing_behaviour = $module.Params.groups_missing_behaviour
+$enabled = $module.Params.enabled
+$path = $module.Params.path
+$upn = $module.Params.upn
+$spn = $module.Params.spn
+$spn_action = $module.Params.spn_action
+$sam_account_name = $module.Params.sam_account_name
+$delegates = $module.Params.delegates
+$identity = $module.Params.identity
+
+if ($null -eq $identity) {
+ $identity = $name
+}
+
+# User informational parameters
+$user_info = @{
+ GivenName = $module.Params.firstname
+ Surname = $module.Params.surname
+ DisplayName = $module.Params.display_name
+ Company = $module.Params.company
+ EmailAddress = $module.Params.email
+ StreetAddress = $module.Params.street
+ City = $module.Params.city
+ State = $module.Params.state_province
+ PostalCode = $module.Params.postal_code
+ Country = $module.Params.country
+}
+
+# Additional attributes
+$attributes = $module.Params.attributes
+
+# Parameter validation
+If ($null -ne $account_locked -and $account_locked) {
+ $module.FailJson("account_locked must be set to 'no' if provided")
+}
+
+If (($null -ne $password_expired) -and ($null -ne $password_never_expires)) {
+ $module.FailJson("password_expired and password_never_expires are mutually exclusive but have both been set")
+}
+
+$extra_args = @{}
+if ($null -ne $domain_username) {
+ $domain_password = ConvertTo-SecureString $domain_password -AsPlainText -Force
+ $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $domain_username, $domain_password
+ $extra_args.Credential = $credential
+}
+
+if ($null -ne $domain_server) {
+ $extra_args.Server = $domain_server
+}
+
+Function Get-PrincipalGroup {
+ Param ($identity, $args_extra)
+ try {
+ $groups = Get-ADPrincipalGroupMembership `
+ -Identity $identity @args_extra `
+ -ErrorAction Stop
+ }
+ catch {
+ $module.Warn("Failed to enumerate user groups but continuing on: $($_.Exception.Message)")
+ return @()
+ }
+
+ $result_groups = foreach ($group in $groups) {
+ $group.DistinguishedName
+ }
+ return $result_groups
+}
+
+try {
+ $user_obj = Get-ADUser `
+ -Identity $identity `
+ -Properties ('*', 'msDS-PrincipalName') @extra_args
+ $user_guid = $user_obj.ObjectGUID
+}
+catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
+ $user_obj = $null
+ $user_guid = $null
+}
+
+If ($state -eq 'present') {
+ # If the account does not exist, create it
+ If (-not $user_obj) {
+ $create_args = @{}
+ $create_args.Name = $name
+ If ($null -ne $path) {
+ $create_args.Path = $path
+ }
+ If ($null -ne $upn) {
+ $create_args.UserPrincipalName = $upn
+ $create_args.SamAccountName = $upn.Split('@')[0]
+ }
+ If ($null -ne $sam_account_name) {
+ $create_args.SamAccountName = $sam_account_name
+ }
+ if ($null -ne $password) {
+ $create_args.AccountPassword = ConvertTo-SecureString $password -AsPlainText -Force
+ }
+ $user_obj = New-ADUser @create_args -WhatIf:$check_mode -PassThru @extra_args
+ $user_guid = $user_obj.ObjectGUID
+ $module.Result.created = $true
+ $module.Result.changed = $true
+ If ($check_mode) {
+ $module.ExitJson()
+ }
+ $user_obj = Get-ADUser -Identity $user_guid -Properties ('*', 'msDS-PrincipalName') @extra_args
+ }
+ ElseIf ($password) {
+ # Don't unnecessary check for working credentials.
+ # Set the password if we need to.
+ If ($update_password -eq "always") {
+ $set_new_credentials = $true
+ }
+ elseif ($update_password -eq "when_changed") {
+ $user_identifier = If ($user_obj.UserPrincipalName) {
+ $user_obj.UserPrincipalName
+ }
+ else {
+ $user_obj.'msDS-PrincipalName'
+ }
+
+ $set_new_credentials = -not (Test-Credential -Username $user_identifier -Password $password)
+ }
+ else {
+ $set_new_credentials = $false
+ }
+ If ($set_new_credentials) {
+ $secure_password = ConvertTo-SecureString $password -AsPlainText -Force
+ try {
+ Set-ADAccountPassword -Identity $user_guid `
+ -Reset:$true `
+ -Confirm:$false `
+ -NewPassword $secure_password `
+ -WhatIf:$check_mode @extra_args
+ }
+ catch {
+ $module.FailJson("Failed to set password on account: $($_.Exception.Message)", $_)
+ }
+ $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args
+ $module.Result.password_updated = $true
+ $module.Result.changed = $true
+ }
+ }
+
+ # Configure password policies
+ If (($null -ne $password_never_expires) -and ($password_never_expires -ne $user_obj.PasswordNeverExpires)) {
+ Set-ADUser -Identity $user_guid -PasswordNeverExpires $password_never_expires -WhatIf:$check_mode @extra_args
+ $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args
+ $module.Result.changed = $true
+ }
+ If (($null -ne $password_expired) -and ($password_expired -ne $user_obj.PasswordExpired)) {
+ Set-ADUser -Identity $user_guid -ChangePasswordAtLogon $password_expired -WhatIf:$check_mode @extra_args
+ $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args
+ $module.Result.changed = $true
+ }
+ If (($null -ne $user_cannot_change_password) -and ($user_cannot_change_password -ne $user_obj.CannotChangePassword)) {
+ Set-ADUser -Identity $user_guid -CannotChangePassword $user_cannot_change_password -WhatIf:$check_mode @extra_args
+ $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args
+ $module.Result.changed = $true
+ }
+
+ # Assign other account settings
+ If (($null -ne $upn) -and ($upn -ne $user_obj.UserPrincipalName)) {
+ Set-ADUser -Identity $user_guid -UserPrincipalName $upn -WhatIf:$check_mode @extra_args
+ $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args
+ $module.Result.changed = $true
+ }
+ If (($null -ne $sam_account_name) -and ($sam_account_name -ne $user_obj.SamAccountName)) {
+ Set-ADUser -Identity $user_guid -SamAccountName $sam_account_name -WhatIf:$check_mode @extra_args
+ $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args
+ $module.Result.changed = $true
+ }
+ If (($null -ne $description) -and ($description -ne $user_obj.Description)) {
+ Set-ADUser -Identity $user_guid -description $description -WhatIf:$check_mode @extra_args
+ $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args
+ $module.Result.changed = $true
+ }
+ If ($enabled -ne $user_obj.Enabled) {
+ Set-ADUser -Identity $user_guid -Enabled $enabled -WhatIf:$check_mode @extra_args
+ $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args
+ $module.Result.changed = $true
+ }
+ If ((-not $account_locked) -and ($user_obj.LockedOut -eq $true)) {
+ Unlock-ADAccount -Identity $user_guid -WhatIf:$check_mode @extra_args
+ $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args
+ $module.Result.changed = $true
+ }
+ If ($delegates) {
+ if (Compare-Object $delegates $user_obj.PrincipalsAllowedToDelegateToAccount) {
+ Set-ADUser -Identity $user_guid -PrincipalsAllowedToDelegateToAccount $delegates
+ $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args
+ $module.Result.changed = $true
+ }
+ }
+
+ # configure service principal names
+ if ($null -ne $spn) {
+ $current_spn = [Array]$user_obj.ServicePrincipalNames
+ $desired_spn = [Array]$spn
+ $spn_diff = @()
+
+ # generate a diff
+ $desired_spn | ForEach-Object {
+ if ($current_spn -contains $_) {
+ $spn_diff += $_
+ }
+ }
+
+ try {
+ switch ($spn_action) {
+ "add" {
+ # the current spn list does not have any spn's in the desired list
+ if (-not $spn_diff) {
+ Set-ADUser `
+ -Identity $user_guid `
+ -ServicePrincipalNames @{ Add = $(($spn | ForEach-Object { "$($_)" } )) } `
+ -WhatIf:$check_mode @extra_args
+ $module.Result.changed = $true
+ }
+ }
+ "remove" {
+ # the current spn list does not have any differences
+ # that means we can remove the desired list
+ if ($spn_diff) {
+ Set-ADUser `
+ -Identity $user_guid `
+ -ServicePrincipalNames @{ Remove = $(($spn | ForEach-Object { "$($_)" } )) } `
+ -WhatIf:$check_mode @extra_args
+ $module.Result.changed = $true
+ }
+ }
+ "replace" {
+ # the current and desired spn lists do not match
+ if (Compare-Object $current_spn $desired_spn) {
+ Set-ADUser `
+ -Identity $user_guid `
+ -ServicePrincipalNames @{ Replace = $(($spn | ForEach-Object { "$($_)" } )) } `
+ -WhatIf:$check_mode @extra_args
+ $module.Result.changed = $true
+ }
+ }
+ }
+ }
+ catch {
+ $module.FailJson("Failed to $spn_action SPN(s)", $_)
+ }
+ }
+
+ # Set user information
+ Foreach ($key in $user_info.Keys) {
+ If ($null -eq $user_info[$key]) {
+ continue
+ }
+ $value = $user_info[$key]
+ If ($value -ne $user_obj.$key) {
+ $set_args = $extra_args.Clone()
+ $set_args.$key = $value
+ Set-ADUser -Identity $user_guid -WhatIf:$check_mode @set_args
+ $module.Result.changed = $true
+ $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args
+ }
+ }
+
+ # Set additional attributes
+ $set_args = $extra_args.Clone()
+ $run_change = $false
+
+ if ($null -ne $attributes) {
+ $add_attributes = @{}
+ $replace_attributes = @{}
+ foreach ($attribute in $attributes.GetEnumerator()) {
+ $attribute_name = $attribute.Key
+ $attribute_value = $attribute.Value
+
+ $valid_property = [bool]($user_obj.PSobject.Properties.name -eq $attribute_name)
+ if ($valid_property) {
+ $existing_value = $user_obj.$attribute_name
+ if ($existing_value -cne $attribute_value) {
+ $replace_attributes.$attribute_name = $attribute_value
+ }
+ }
+ else {
+ $add_attributes.$attribute_name = $attribute_value
+ }
+ }
+ if ($add_attributes.Count -gt 0) {
+ $set_args.Add = $add_attributes
+ $run_change = $true
+ }
+ if ($replace_attributes.Count -gt 0) {
+ $set_args.Replace = $replace_attributes
+ $run_change = $true
+ }
+ }
+
+ if ($run_change) {
+ Set-ADUser -Identity $user_guid -WhatIf:$check_mode @set_args
+ $module.Result.changed = $true
+ $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args
+ }
+
+ # Configure group assignment
+ if ($null -ne $groups) {
+ $group_list = $groups
+
+ $groups = @(
+ Foreach ($group in $group_list) {
+ try {
+ (Get-ADGroup -Identity $group @extra_args).DistinguishedName
+ }
+ catch {
+ if ($groups_missing_behaviour -eq "fail") {
+ $module.FailJson("Failed to locate group $($group): $($_.Exception.Message)", $_)
+ }
+ elseif ($groups_missing_behaviour -eq "warn") {
+ $module.Warn("Failed to locate group $($group) but continuing on: $($_.Exception.Message)")
+ }
+ }
+ }
+ )
+
+ $assigned_groups = Get-PrincipalGroup $user_guid $extra_args
+
+ switch ($groups_action) {
+ "add" {
+ Foreach ($group in $groups) {
+ If (-not ($assigned_groups -Contains $group)) {
+ Add-ADGroupMember -Identity $group -Members $user_guid -WhatIf:$check_mode @extra_args
+ $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args
+ $module.Result.changed = $true
+ }
+ }
+ }
+ "remove" {
+ Foreach ($group in $groups) {
+ If ($assigned_groups -Contains $group) {
+ Remove-ADGroupMember -Identity $group -Members $user_guid -Confirm:$false -WhatIf:$check_mode @extra_args
+ $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args
+ $module.Result.changed = $true
+ }
+ }
+ }
+ "replace" {
+ Foreach ($group in $assigned_groups) {
+ If (($group -ne $user_obj.PrimaryGroup) -and -not ($groups -Contains $group)) {
+ Remove-ADGroupMember -Identity $group -Members $user_guid -Confirm:$false -WhatIf:$check_mode @extra_args
+ $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args
+ $module.Result.changed = $true
+ }
+ }
+ Foreach ($group in $groups) {
+ If (-not ($assigned_groups -Contains $group)) {
+ Add-ADGroupMember -Identity $group -Members $user_guid -WhatIf:$check_mode @extra_args
+ $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args
+ $module.Result.changed = $true
+ }
+ }
+ }
+ }
+ }
+}
+elseif ($state -eq 'absent') {
+ # Ensure user does not exist
+ If ($user_obj) {
+ Remove-ADUser $user_obj -Confirm:$false -WhatIf:$check_mode @extra_args
+ $module.Result.changed = $true
+ if ($check_mode) {
+ $module.ExitJson()
+ }
+ $user_obj = $null
+ }
+}
+
+If ($user_obj) {
+ $user_obj = Get-ADUser -Identity $user_guid -Properties * @extra_args
+ $module.Result.name = $user_obj.Name
+ $module.Result.firstname = $user_obj.GivenName
+ $module.Result.surname = $user_obj.Surname
+ $module.Result.display_name = $user_obj.DisplayName
+ $module.Result.enabled = $user_obj.Enabled
+ $module.Result.company = $user_obj.Company
+ $module.Result.street = $user_obj.StreetAddress
+ $module.Result.email = $user_obj.EmailAddress
+ $module.Result.city = $user_obj.City
+ $module.Result.state_province = $user_obj.State
+ $module.Result.country = $user_obj.Country
+ $module.Result.postal_code = $user_obj.PostalCode
+ $module.Result.distinguished_name = $user_obj.DistinguishedName
+ $module.Result.description = $user_obj.Description
+ $module.Result.password_expired = $user_obj.PasswordExpired
+ $module.Result.password_never_expires = $user_obj.PasswordNeverExpires
+ $module.Result.user_cannot_change_password = $user_obj.CannotChangePassword
+ $module.Result.account_locked = $user_obj.LockedOut
+ $module.Result.delegates = $user_obj.PrincipalsAllowedToDelegateToAccount
+ $module.Result.sid = [string]$user_obj.SID
+ $module.Result.spn = [Array]$user_obj.ServicePrincipalNames
+ $module.Result.upn = $user_obj.UserPrincipalName
+ $module.Result.sam_account_name = $user_obj.SamAccountName
+ $module.Result.groups = Get-PrincipalGroup $user_guid $extra_args
+ $module.Result.msg = "User '$name' is present"
+ $module.Result.state = "present"
+}
+else {
+ $module.Result.name = $name
+ $module.Result.msg = "User '$name' is absent"
+ $module.Result.state = "absent"
+}
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_domain_user.py b/ansible_collections/community/windows/plugins/modules/win_domain_user.py
new file mode 100644
index 00000000..aee5efd0
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_domain_user.py
@@ -0,0 +1,477 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_domain_user
+short_description: Manages Windows Active Directory user accounts
+description:
+ - Manages Windows Active Directory user accounts.
+options:
+ name:
+ description:
+ - Name of the user to create, remove or modify.
+ type: str
+ required: true
+ identity:
+ description:
+ - Identity parameter used to find the User in the Active Directory.
+ - This value can be in the forms C(Distinguished Name), C(objectGUID),
+ C(objectSid) or C(sAMAccountName).
+ - Default to C(name) if not set.
+ type: str
+ state:
+ description:
+ - When C(present), creates or updates the user account.
+ - When C(absent), removes the user account if it exists.
+ - When C(query), retrieves the user account details without making any changes.
+ type: str
+ choices: [ absent, present, query ]
+ default: present
+ enabled:
+ description:
+ - C(yes) will enable the user account.
+ - C(no) will disable the account.
+ type: bool
+ default: yes
+ account_locked:
+ description:
+ - C(no) will unlock the user account if locked.
+ - Note that there is not a way to lock an account as an administrator.
+ - Accounts are locked due to user actions; as an admin, you may only unlock a locked account.
+ - If you wish to administratively disable an account, set I(enabled) to C(no).
+ type: bool
+ description:
+ description:
+ - Description of the user
+ type: str
+ groups:
+ description:
+ - Adds or removes the user from this list of groups,
+ depending on the value of I(groups_action).
+ - To remove all but the Principal Group, set C(groups=<principal group name>) and
+ I(groups_action=replace).
+ - Note that users cannot be removed from their principal group (for example, "Domain Users").
+ type: list
+ elements: str
+ groups_action:
+ description:
+ - If C(add), the user is added to each group in I(groups) where not already a member.
+ - If C(remove), the user is removed from each group in I(groups).
+ - If C(replace), the user is added as a member of each group in
+ I(groups) and removed from any other groups.
+ type: str
+ choices: [ add, remove, replace ]
+ default: replace
+ groups_missing_behaviour:
+ description:
+ - Controls what happens when a group specified by C(groups) is an invalid group name.
+ - C(fail) is the default and will return an error any groups do not exist.
+ - C(ignore) will ignore any groups that does not exist.
+ - C(warn) will display a warning for any groups that do not exist but will continue without failing.
+ type: str
+ choices:
+ - fail
+ - ignore
+ - warn
+ default: fail
+ version_added: 1.10.0
+ spn:
+ description:
+ - Specifies the service principal name(s) for the account. This parameter sets the
+ ServicePrincipalNames property of the account. The LDAP display name (ldapDisplayName)
+ for this property is servicePrincipalName.
+ type: list
+ elements: str
+ aliases: [ spns ]
+ version_added: 1.10.0
+ spn_action:
+ description:
+ - If C(add), the SPNs are added to the user.
+ - If C(remove), the SPNs are removed from the user.
+ - If C(replace), the defined set of SPN's overwrite the current set of SPNs.
+ type: str
+ choices: [ add, remove, replace ]
+ default: replace
+ version_added: 1.10.0
+ password:
+ description:
+ - Optionally set the user's password to this (plain text) value.
+ - To enable an account - I(enabled) - a password must already be
+ configured on the account, or you must provide a password here.
+ type: str
+ update_password:
+ description:
+ - C(always) will always update passwords.
+ - C(on_create) will only set the password for newly created users.
+ - C(when_changed) will only set the password when changed.
+ type: str
+ choices: [ always, on_create, when_changed ]
+ default: always
+ password_expired:
+ description:
+ - C(yes) will require the user to change their password at next login.
+ - C(no) will clear the expired password flag.
+ - This is mutually exclusive with I(password_never_expires).
+ type: bool
+ password_never_expires:
+ description:
+ - C(yes) will set the password to never expire.
+ - C(no) will allow the password to expire.
+ - This is mutually exclusive with I(password_expired).
+ type: bool
+ 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
+ firstname:
+ description:
+ - Configures the user's first name (given name).
+ type: str
+ surname:
+ description:
+ - Configures the user's last name (surname).
+ type: str
+ aliases: [ lastname ]
+ display_name:
+ description:
+ - Configures the user's display name.
+ type: str
+ version_added: 1.12.0
+ company:
+ description:
+ - Configures the user's company name.
+ type: str
+ upn:
+ description:
+ - Configures the User Principal Name (UPN) for the account.
+ - This is not required, but is best practice to configure for modern
+ versions of Active Directory.
+ - The format is C(<username>@<domain>).
+ type: str
+ sam_account_name:
+ description:
+ - Configures the SAM Account Name (C(sAMAccountName)) for the account.
+ - This is allowed to a maximum of 20 characters due to pre-Windows 2000 restrictions.
+ - Default to the C(<username>) specified in C(upn) or C(name) if not set.
+ type: str
+ version_added: 1.7.0
+ email:
+ description:
+ - Configures the user's email address.
+ - This is a record in AD and does not do anything to configure any email
+ servers or systems.
+ type: str
+ street:
+ description:
+ - Configures the user's street address.
+ type: str
+ city:
+ description:
+ - Configures the user's city.
+ type: str
+ state_province:
+ description:
+ - Configures the user's state or province.
+ type: str
+ postal_code:
+ description:
+ - Configures the user's postal code / zip code.
+ type: str
+ country:
+ description:
+ - Configures the user's country code.
+ - Note that this is a two-character ISO 3166 code.
+ type: str
+ path:
+ description:
+ - Container or OU for the new user; if you do not specify this, the
+ user will be placed in the default container for users in the domain.
+ - Setting the path is only available when a new user is created;
+ if you specify a path on an existing user, the user's path will not
+ be updated - you must delete (e.g., C(state=absent)) the user and
+ then re-add the user with the appropriate path.
+ type: str
+ delegates:
+ description:
+ - Specifies an array of principal objects. This parameter sets the
+ msDS-AllowedToActOnBehalfOfOtherIdentity attribute of a computer account
+ object.
+ - Must be specified as a distinguished name C(CN=shenetworks,CN=Users,DC=ansible,DC=test)
+ type: list
+ elements: str
+ aliases: [ principals_allowed_to_delegate ]
+ version_added: 1.10.0
+ attributes:
+ description:
+ - A dict of custom LDAP attributes to set on the user.
+ - This can be used to set custom attributes that are not exposed as module
+ parameters, e.g. C(telephoneNumber).
+ - See the examples on how to format this parameter.
+ type: dict
+ domain_username:
+ description:
+ - The username to use when interacting with AD.
+ - If this is not set then the user Ansible used to log in with will be
+ used instead when using CredSSP or Kerberos with credential delegation.
+ type: str
+ domain_password:
+ description:
+ - The password for I(username).
+ type: str
+ domain_server:
+ description:
+ - Specifies the Active Directory Domain Services instance to connect to.
+ - Can be in the form of an FQDN or NetBIOS name.
+ - If not specified then the value is based on the domain of the computer
+ running PowerShell.
+ type: str
+notes:
+ - Works with Windows 2012R2 and newer.
+ - If running on a server that is not a Domain Controller, credential
+ delegation through CredSSP or Kerberos with delegation must be used or the
+ I(domain_username), I(domain_password) must be set.
+ - Note that some individuals have confirmed successful operation on Windows
+ 2008R2 servers with AD and AD Web Services enabled, but this has not
+ received the same degree of testing as Windows 2012R2.
+seealso:
+- module: ansible.windows.win_domain
+- module: ansible.windows.win_domain_controller
+- module: community.windows.win_domain_computer
+- module: community.windows.win_domain_group
+- module: ansible.windows.win_domain_membership
+- module: ansible.windows.win_user
+- module: community.windows.win_user_profile
+author:
+ - Nick Chandler (@nwchandler)
+ - Joe Zollo (@zollo)
+'''
+
+EXAMPLES = r'''
+- name: Ensure user bob is present with address information
+ community.windows.win_domain_user:
+ name: bob
+ firstname: Bob
+ surname: Smith
+ display_name: Mr. Bob Smith
+ company: BobCo
+ password: B0bP4ssw0rd
+ state: present
+ groups:
+ - Domain Admins
+ street: 123 4th St.
+ city: Sometown
+ state_province: IN
+ postal_code: 12345
+ country: US
+ attributes:
+ telephoneNumber: 555-123456
+
+- name: Ensure user bob is created and use custom credentials to create the user
+ community.windows.win_domain_user:
+ name: bob
+ firstname: Bob
+ surname: Smith
+ password: B0bP4ssw0rd
+ state: present
+ domain_username: DOMAIN\admin-account
+ domain_password: SomePas2w0rd
+ domain_server: domain@DOMAIN.COM
+
+- name: Ensure user bob is present in OU ou=test,dc=domain,dc=local
+ community.windows.win_domain_user:
+ name: bob
+ password: B0bP4ssw0rd
+ state: present
+ path: ou=test,dc=domain,dc=local
+ groups:
+ - Domain Admins
+
+- name: Ensure user bob is absent
+ community.windows.win_domain_user:
+ name: bob
+ state: absent
+
+- name: Ensure user has spn's defined
+ community.windows.win_domain_user:
+ name: liz.kenyon
+ spn:
+ - MSSQLSvc/us99db-svr95:1433
+ - MSSQLSvc/us99db-svr95.vmware.com:1433
+
+- name: Ensure user has spn added
+ community.windows.win_domain_user:
+ name: liz.kenyon
+ spn_action: add
+ spn:
+ - MSSQLSvc/us99db-svr95:2433
+
+- name: Ensure user is created with delegates and spn's defined
+ community.windows.win_domain_user:
+ name: shmemmmy
+ password: The3rubberducki33!
+ state: present
+ groups:
+ - Domain Admins
+ - Enterprise Admins
+ delegates:
+ - CN=shenetworks,CN=Users,DC=ansible,DC=test
+ - CN=mk.ai,CN=Users,DC=ansible,DC=test
+ - CN=jessiedotjs,CN=Users,DC=ansible,DC=test
+ spn:
+ - MSSQLSvc/us99db-svr95:2433
+'''
+
+RETURN = r'''
+account_locked:
+ description: true if the account is locked
+ returned: always
+ type: bool
+ sample: false
+changed:
+ description: true if the account changed during execution
+ returned: always
+ type: bool
+ sample: false
+city:
+ description: The user city
+ returned: always
+ type: str
+ sample: Indianapolis
+company:
+ description: The user company
+ returned: always
+ type: str
+ sample: RedHat
+country:
+ description: The user country
+ returned: always
+ type: str
+ sample: US
+delegates:
+ description: Principals allowed to delegate
+ returned: always
+ type: list
+ elements: str
+ sample:
+ - CN=svc.tech.unicorn,CN=Users,DC=ansible,DC=test
+ - CN=geoff,CN=Users,DC=ansible,DC=test
+ version_added: 1.10.0
+description:
+ description: A description of the account
+ returned: always
+ type: str
+ sample: Server Administrator
+display_name:
+ description: The user display name
+ returned: always
+ type: str
+ sample: Nick Doe
+distinguished_name:
+ description: DN of the user account
+ returned: always
+ type: str
+ sample: CN=nick,OU=test,DC=domain,DC=local
+email:
+ description: The user email address
+ returned: always
+ type: str
+ sample: nick@domain.local
+enabled:
+ description: true if the account is enabled and false if disabled
+ returned: always
+ type: str
+ sample: true
+firstname:
+ description: The user first name
+ returned: always
+ type: str
+ sample: Nick
+groups:
+ description: AD Groups to which the account belongs
+ returned: always
+ type: list
+ sample: [ "Domain Admins", "Domain Users" ]
+msg:
+ description: Summary message of whether the user is present or absent
+ returned: always
+ type: str
+ sample: User nick is present
+name:
+ description: The username on the account
+ returned: always
+ type: str
+ sample: nick
+password_expired:
+ description: true if the account password has expired
+ returned: always
+ type: bool
+ sample: false
+password_updated:
+ description: true if the password changed during this execution
+ returned: always
+ type: bool
+ sample: true
+postal_code:
+ description: The user postal code
+ returned: always
+ type: str
+ sample: 46033
+sid:
+ description: The SID of the account
+ returned: always
+ type: str
+ sample: S-1-5-21-2752426336-228313920-2202711348-1175
+spn:
+ description: The service principal names
+ returned: always
+ type: list
+ sample:
+ - HTTPSvc/ws1intel-svc1
+ - HTTPSvc/ws1intel-svc1.vmware.com
+ version_added: 1.10.0
+state:
+ description: The state of the user account
+ returned: always
+ type: str
+ sample: present
+state_province:
+ description: The user state or province
+ returned: always
+ type: str
+ sample: IN
+street:
+ description: The user street address
+ returned: always
+ type: str
+ sample: 123 4th St.
+surname:
+ description: The user last name
+ returned: always
+ type: str
+ sample: Doe
+upn:
+ description: The User Principal Name of the account
+ returned: always
+ type: str
+ sample: nick@domain.local
+sam_account_name:
+ description: The SAM Account Name of the account
+ returned: always
+ type: str
+ sample: nick
+ version_added: 1.7.0
+user_cannot_change_password:
+ description: true if the user is not allowed to change password
+ returned: always
+ type: str
+ sample: false
+created:
+ description: Whether a user was created
+ returned: always
+ type: bool
+ sample: true
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_dotnet_ngen.ps1 b/ansible_collections/community/windows/plugins/modules/win_dotnet_ngen.ps1
new file mode 100644
index 00000000..50405925
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_dotnet_ngen.ps1
@@ -0,0 +1,65 @@
+#!powershell
+
+# Copyright: (c) 2015, Peter Mounce <public@neverrunwithscissors.com>
+# 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
+
+$ErrorActionPreference = 'Stop'
+
+$params = Parse-Args $args -supports_check_mode $true
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
+
+$result = @{
+ changed = $false
+}
+
+Function Invoke-Ngen($architecture = "") {
+ $cmd = "$($env:windir)\Microsoft.NET\Framework$($architecture)\v4.0.30319\ngen.exe"
+
+ if (Test-Path -LiteralPath $cmd) {
+ $arguments = "update /force"
+ if ($check_mode) {
+ $ngen_result = @{
+ rc = 0
+ stdout = "check mode output for $cmd $arguments"
+ }
+ }
+ else {
+ try {
+ $ngen_result = Run-Command -command "$cmd $arguments"
+ }
+ catch {
+ Fail-Json -obj $result -message "failed to execute '$cmd $arguments': $($_.Exception.Message)"
+ }
+ }
+ $result."dotnet_ngen$($architecture)_update_exit_code" = $ngen_result.rc
+ $result."dotnet_ngen$($architecture)_update_output" = $ngen_result.stdout
+
+ $arguments = "executeQueuedItems"
+ if ($check_mode) {
+ $executed_queued_items = @{
+ rc = 0
+ stdout = "check mode output for $cmd $arguments"
+ }
+ }
+ else {
+ try {
+ $executed_queued_items = Run-Command -command "$cmd $arguments"
+ }
+ catch {
+ Fail-Json -obj $result -message "failed to execute '$cmd $arguments': $($_.Exception.Message)"
+ }
+ }
+ $result."dotnet_ngen$($architecture)_eqi_exit_code" = $executed_queued_items.rc
+ $result."dotnet_ngen$($architecture)_eqi_output" = $executed_queued_items.stdout
+ $result.changed = $true
+ }
+}
+
+Invoke-Ngen
+Invoke-Ngen -architecture "64"
+
+Exit-Json -obj $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_dotnet_ngen.py b/ansible_collections/community/windows/plugins/modules/win_dotnet_ngen.py
new file mode 100644
index 00000000..d207bafd
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_dotnet_ngen.py
@@ -0,0 +1,80 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2015, Peter Mounce <public@neverrunwithscissors.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_dotnet_ngen
+short_description: Runs ngen to recompile DLLs after .NET updates
+description:
+ - After .NET framework is installed/updated, Windows will probably want to recompile things to optimise for the host.
+ - This happens via scheduled task, usually at some inopportune time.
+ - This module allows you to run this task on your own schedule, so you incur the CPU hit at some more convenient and controlled time.
+ - U(https://docs.microsoft.com/en-us/dotnet/framework/tools/ngen-exe-native-image-generator#native-image-service)
+ - U(http://blogs.msdn.com/b/dotnet/archive/2013/08/06/wondering-why-mscorsvw-exe-has-high-cpu-usage-you-can-speed-it-up.aspx)
+options: {}
+notes:
+ - There are in fact two scheduled tasks for ngen but they have no triggers so aren't a problem.
+ - There's no way to test if they've been completed.
+ - The stdout is quite likely to be several megabytes.
+author:
+- Peter Mounce (@petemounce)
+'''
+
+EXAMPLES = r'''
+- name: Run ngen tasks
+ community.windows.win_dotnet_ngen:
+'''
+
+RETURN = r'''
+dotnet_ngen_update_exit_code:
+ description: The exit code after running the 32-bit ngen.exe update /force
+ command.
+ returned: 32-bit ngen executable exists
+ type: int
+ sample: 0
+dotnet_ngen_update_output:
+ description: The stdout after running the 32-bit ngen.exe update /force
+ command.
+ returned: 32-bit ngen executable exists
+ type: str
+ sample: sample output
+dotnet_ngen_eqi_exit_code:
+ description: The exit code after running the 32-bit ngen.exe
+ executeQueuedItems command.
+ returned: 32-bit ngen executable exists
+ type: int
+ sample: 0
+dotnet_ngen_eqi_output:
+ description: The stdout after running the 32-bit ngen.exe executeQueuedItems
+ command.
+ returned: 32-bit ngen executable exists
+ type: str
+ sample: sample output
+dotnet_ngen64_update_exit_code:
+ description: The exit code after running the 64-bit ngen.exe update /force
+ command.
+ returned: 64-bit ngen executable exists
+ type: int
+ sample: 0
+dotnet_ngen64_update_output:
+ description: The stdout after running the 64-bit ngen.exe update /force
+ command.
+ returned: 64-bit ngen executable exists
+ type: str
+ sample: sample output
+dotnet_ngen64_eqi_exit_code:
+ description: The exit code after running the 64-bit ngen.exe
+ executeQueuedItems command.
+ returned: 64-bit ngen executable exists
+ type: int
+ sample: 0
+dotnet_ngen64_eqi_output:
+ description: The stdout after running the 64-bit ngen.exe executeQueuedItems
+ command.
+ returned: 64-bit ngen executable exists
+ type: str
+ sample: sample output
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_eventlog.ps1 b/ansible_collections/community/windows/plugins/modules/win_eventlog.ps1
new file mode 100644
index 00000000..a2388d9b
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_eventlog.ps1
@@ -0,0 +1,287 @@
+#!powershell
+
+# Copyright: (c) 2017, Andrew Saraceni <andrew.saraceni@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+$ErrorActionPreference = "Stop"
+
+function Get-EventLogDetail {
+ <#
+ .SYNOPSIS
+ Get details of an event log, sources, and associated attributes.
+ Used for comparison against passed-in option values to ensure idempotency.
+ #>
+ param(
+ [String]$LogName
+ )
+
+ $log_details = @{}
+ $log_details.name = $LogName
+ $log_details.exists = $false
+ $log = Get-EventLog -List | Where-Object { $_.Log -eq $LogName }
+
+ if ($log) {
+ $log_details.exists = $true
+ $log_details.maximum_size_kb = $log.MaximumKilobytes
+ $log_details.overflow_action = $log.OverflowAction.ToString()
+ $log_details.retention_days = $log.MinimumRetentionDays
+ $log_details.entries = $log.Entries.Count
+ $log_details.sources = [Ordered]@{}
+
+ # Retrieve existing sources and category/message/parameter file locations
+ # Associating file locations and sources with logs can only be done from the registry
+
+ $root_key = "HKLM:\SYSTEM\CurrentControlSet\Services\EventLog\{0}" -f $LogName
+ $log_root = Get-ChildItem -LiteralPath $root_key
+
+ foreach ($child in $log_root) {
+ $source_name = $child.PSChildName
+ $log_details.sources.$source_name = @{}
+ $hash_cursor = $log_details.sources.$source_name
+
+ $source_root = "{0}\{1}" -f $root_key, $source_name
+ $resource_files = Get-ItemProperty -LiteralPath $source_root
+
+ $hash_cursor.category_file = $resource_files.CategoryMessageFile
+ $hash_cursor.message_file = $resource_files.EventMessageFile
+ $hash_cursor.parameter_file = $resource_files.ParameterMessageFile
+ }
+ }
+
+ return $log_details
+}
+
+function Test-SourceExistence {
+ <#
+ .SYNOPSIS
+ Get information on a source's existence.
+ Examine existence regarding the parent log it belongs to and its expected state.
+ #>
+ param(
+ [String]$LogName,
+ [String]$SourceName,
+ [Switch]$NoLogShouldExist
+ )
+
+ $source_exists = [System.Diagnostics.EventLog]::SourceExists($SourceName)
+
+ if ($source_exists -and $NoLogShouldExist) {
+ Fail-Json -obj $result -message "Source $SourceName already exists and cannot be created"
+ }
+ elseif ($source_exists) {
+ $source_log = [System.Diagnostics.EventLog]::LogNameFromSourceName($SourceName, ".")
+ if ($source_log -ne $LogName) {
+ Fail-Json -obj $result -message "Source $SourceName does not belong to log $LogName and cannot be modified"
+ }
+ }
+
+ return $source_exists
+}
+
+function ConvertTo-MaximumSize {
+ <#
+ .SYNOPSIS
+ Convert a string KB/MB/GB value to common bytes and KB representations.
+ .NOTES
+ Size must be between 64KB and 4GB and divisible by 64KB, as per the MaximumSize parameter of Limit-EventLog.
+ #>
+ param(
+ [String]$Size
+ )
+
+ $parsed_size = @{
+ bytes = $null
+ KB = $null
+ }
+
+ $size_regex = "^\d+(\.\d+)?(KB|MB|GB)$"
+ if ($Size -notmatch $size_regex) {
+ Fail-Json -obj $result -message "Maximum size $Size is not properly specified"
+ }
+
+ $size_upper = $Size.ToUpper()
+ $size_numeric = [Double]$Size.Substring(0, $Size.Length - 2)
+
+ if ($size_upper.EndsWith("GB")) {
+ $size_bytes = $size_numeric * 1GB
+ }
+ elseif ($size_upper.EndsWith("MB")) {
+ $size_bytes = $size_numeric * 1MB
+ }
+ elseif ($size_upper.EndsWith("KB")) {
+ $size_bytes = $size_numeric * 1KB
+ }
+
+ if (($size_bytes -lt 64KB) -or ($size_bytes -ge 4GB)) {
+ Fail-Json -obj $result -message "Maximum size must be between 64KB and 4GB"
+ }
+ elseif (($size_bytes % 64KB) -ne 0) {
+ Fail-Json -obj $result -message "Maximum size must be divisible by 64KB"
+ }
+
+ $parsed_size.bytes = $size_bytes
+ $parsed_size.KB = $size_bytes / 1KB
+ return $parsed_size
+}
+
+$params = Parse-Args $args -supports_check_mode $true
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
+
+$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present", "clear", "absent"
+$sources = Get-AnsibleParam -obj $params -name "sources" -type "list"
+$category_file = Get-AnsibleParam -obj $params -name "category_file" -type "path"
+$message_file = Get-AnsibleParam -obj $params -name "message_file" -type "path"
+$parameter_file = Get-AnsibleParam -obj $params -name "parameter_file" -type "path"
+$maximum_size = Get-AnsibleParam -obj $params -name "maximum_size" -type "str"
+$overflow_action = Get-AnsibleParam -obj $params -name "overflow_action" -type "str" -validateset "OverwriteOlder", "OverwriteAsNeeded", "DoNotOverwrite"
+$retention_days = Get-AnsibleParam -obj $params -name "retention_days" -type "int"
+
+$result = @{
+ changed = $false
+ name = $name
+ sources_changed = @()
+}
+
+$log_details = Get-EventLogDetail -LogName $name
+
+# Handle common error cases up front
+if ($state -eq "present" -and !$log_details.exists -and !$sources) {
+ # When creating a log, one or more sources must be passed
+ Fail-Json -obj $result -message "You must specify one or more sources when creating a log for the first time"
+}
+elseif ($state -eq "present" -and $log_details.exists -and $name -in $sources -and ($category_file -or $message_file -or $parameter_file)) {
+ # After a default source of the same name is created, it cannot be modified without removing the log
+ Fail-Json -obj $result -message "Cannot modify default source $name of log $name - you must remove the log"
+}
+elseif ($state -eq "clear" -and !$log_details.exists) {
+ Fail-Json -obj $result -message "Cannot clear log $name as it does not exist"
+}
+elseif ($state -eq "absent" -and $name -in $sources) {
+ # You also cannot remove a default source for the log - you must remove the log itself
+ Fail-Json -obj $result -message "Cannot remove default source $name from log $name - you must remove the log"
+}
+
+try {
+ switch ($state) {
+ "present" {
+ foreach ($source in $sources) {
+ if ($log_details.exists) {
+ $source_exists = Test-SourceExistence -LogName $name -SourceName $source
+ }
+ else {
+ $source_exists = Test-SourceExistence -LogName $name -SourceName $source -NoLogShouldExist
+ }
+
+ if ($source_exists) {
+ $category_change = $category_file -and $log_details.sources.$source.category_file -ne $category_file
+ $message_change = $message_file -and $log_details.sources.$source.message_file -ne $message_file
+ $parameter_change = $parameter_file -and $log_details.sources.$source.parameter_file -ne $parameter_file
+ # Remove source and recreate later if any of the above are true
+ if ($category_change -or $message_change -or $parameter_change) {
+ Remove-EventLog -Source $source -WhatIf:$check_mode
+ }
+ else {
+ continue
+ }
+ }
+
+ $new_params = @{
+ LogName = $name
+ Source = $source
+ }
+ if ($category_file) {
+ $new_params.CategoryResourceFile = $category_file
+ }
+ if ($message_file) {
+ $new_params.MessageResourceFile = $message_file
+ }
+ if ($parameter_file) {
+ $new_params.ParameterResourceFile = $parameter_file
+ }
+
+ if (!$check_mode) {
+ New-EventLog @new_params
+ $result.sources_changed += $source
+ }
+ $result.changed = $true
+ }
+
+ if ($maximum_size) {
+ $converted_size = ConvertTo-MaximumSize -Size $maximum_size
+ }
+
+ $size_change = $maximum_size -and $log_details.maximum_size_kb -ne $converted_size.KB
+ $overflow_change = $overflow_action -and $log_details.overflow_action -ne $overflow_action
+ $retention_change = $retention_days -and $log_details.retention_days -ne $retention_days
+
+ if ($size_change -or $overflow_change -or $retention_change) {
+ $limit_params = @{
+ LogName = $name
+ WhatIf = $check_mode
+ }
+ if ($maximum_size) {
+ $limit_params.MaximumSize = $converted_size.bytes
+ }
+ if ($overflow_action) {
+ $limit_params.OverflowAction = $overflow_action
+ }
+ if ($retention_days) {
+ $limit_params.RetentionDays = $retention_days
+ }
+
+ Limit-EventLog @limit_params
+ $result.changed = $true
+ }
+
+ }
+ "clear" {
+ if ($log_details.entries -gt 0) {
+ Clear-EventLog -LogName $name -WhatIf:$check_mode
+ $result.changed = $true
+ }
+ }
+ "absent" {
+ if ($sources -and $log_details.exists) {
+ # Since sources were passed, remove sources tied to event log
+ foreach ($source in $sources) {
+ $source_exists = Test-SourceExistence -LogName $name -SourceName $source
+ if ($source_exists) {
+ Remove-EventLog -Source $source -WhatIf:$check_mode
+ if (!$check_mode) {
+ $result.sources_changed += $source
+ }
+ $result.changed = $true
+ }
+ }
+ }
+ elseif ($log_details.exists) {
+ # Only name passed, so remove event log itself (which also removes contained sources)
+ Remove-EventLog -LogName $name -WhatIf:$check_mode
+ if (!$check_mode) {
+ $log_details.sources.GetEnumerator() | ForEach-Object { $result.sources_changed += $_.Name }
+ }
+ $result.changed = $true
+ }
+ }
+ }
+}
+catch {
+ Fail-Json -obj $result -message $_.Exception.Message
+}
+
+$final_log_details = Get-EventLogDetail -LogName $name
+foreach ($final_log_detail in $final_log_details.GetEnumerator()) {
+ if ($final_log_detail.Name -eq "sources") {
+ $sources = @()
+ $final_log_detail.Value.GetEnumerator() | ForEach-Object { $sources += $_.Name }
+ $result.$($final_log_detail.Name) = [Array]$sources
+ }
+ else {
+ $result.$($final_log_detail.Name) = $final_log_detail.Value
+ }
+}
+
+Exit-Json -obj $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_eventlog.py b/ansible_collections/community/windows/plugins/modules/win_eventlog.py
new file mode 100644
index 00000000..8cc9b354
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_eventlog.py
@@ -0,0 +1,159 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2017, Andrew Saraceni <andrew.saraceni@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_eventlog
+short_description: Manage Windows event logs
+description:
+ - Allows the addition, clearing and removal of local Windows event logs,
+ and the creation and removal of sources from a given event log. Also
+ allows the specification of settings per log and source.
+options:
+ name:
+ description:
+ - Name of the event log to manage.
+ type: str
+ required: yes
+ state:
+ description:
+ - Desired state of the log and/or sources.
+ - When C(sources) is populated, state is checked for sources.
+ - When C(sources) is not populated, state is checked for the specified log itself.
+ - If C(state) is C(clear), event log entries are cleared for the target log.
+ type: str
+ choices: [ absent, clear, present ]
+ default: present
+ sources:
+ description:
+ - A list of one or more sources to ensure are present/absent in the log.
+ - When C(category_file), C(message_file) and/or C(parameter_file) are specified,
+ these values are applied across all sources.
+ type: list
+ elements: str
+ category_file:
+ description:
+ - For one or more sources specified, the path to a custom category resource file.
+ type: path
+ message_file:
+ description:
+ - For one or more sources specified, the path to a custom event message resource file.
+ type: path
+ parameter_file:
+ description:
+ - For one or more sources specified, the path to a custom parameter resource file.
+ type: path
+ maximum_size:
+ description:
+ - The maximum size of the event log.
+ - Value must be between 64KB and 4GB, and divisible by 64KB.
+ - Size can be specified in KB, MB or GB (e.g. 128KB, 16MB, 2.5GB).
+ type: str
+ overflow_action:
+ description:
+ - The action for the log to take once it reaches its maximum size.
+ - For C(DoNotOverwrite), all existing entries are kept and new entries are not retained.
+ - For C(OverwriteAsNeeded), each new entry overwrites the oldest entry.
+ - For C(OverwriteOlder), new log entries overwrite those older than the C(retention_days) value.
+ type: str
+ choices: [ DoNotOverwrite, OverwriteAsNeeded, OverwriteOlder ]
+ retention_days:
+ description:
+ - The minimum number of days event entries must remain in the log.
+ - This option is only used when C(overflow_action) is C(OverwriteOlder).
+ type: int
+seealso:
+- module: community.windows.win_eventlog_entry
+author:
+ - Andrew Saraceni (@andrewsaraceni)
+'''
+
+EXAMPLES = r'''
+- name: Add a new event log with two custom sources
+ community.windows.win_eventlog:
+ name: MyNewLog
+ sources:
+ - NewLogSource1
+ - NewLogSource2
+ state: present
+
+- name: Change the category and message resource files used for NewLogSource1
+ community.windows.win_eventlog:
+ name: MyNewLog
+ sources:
+ - NewLogSource1
+ category_file: C:\NewApp\CustomCategories.dll
+ message_file: C:\NewApp\CustomMessages.dll
+ state: present
+
+- name: Change the maximum size and overflow action for MyNewLog
+ community.windows.win_eventlog:
+ name: MyNewLog
+ maximum_size: 16MB
+ overflow_action: DoNotOverwrite
+ state: present
+
+- name: Clear event entries for MyNewLog
+ community.windows.win_eventlog:
+ name: MyNewLog
+ state: clear
+
+- name: Remove NewLogSource2 from MyNewLog
+ community.windows.win_eventlog:
+ name: MyNewLog
+ sources:
+ - NewLogSource2
+ state: absent
+
+- name: Remove MyNewLog and all remaining sources
+ community.windows.win_eventlog:
+ name: MyNewLog
+ state: absent
+'''
+
+RETURN = r'''
+name:
+ description: The name of the event log.
+ returned: always
+ type: str
+ sample: MyNewLog
+exists:
+ description: Whether the event log exists or not.
+ returned: success
+ type: bool
+ sample: true
+entries:
+ description: The count of entries present in the event log.
+ returned: success
+ type: int
+ sample: 50
+maximum_size_kb:
+ description: Maximum size of the log in KB.
+ returned: success
+ type: int
+ sample: 512
+overflow_action:
+ description: The action the log takes once it reaches its maximum size.
+ returned: success
+ type: str
+ sample: OverwriteOlder
+retention_days:
+ description: The minimum number of days entries are retained in the log.
+ returned: success
+ type: int
+ sample: 7
+sources:
+ description: A list of the current sources for the log.
+ returned: success
+ type: list
+ sample: ["MyNewLog", "NewLogSource1", "NewLogSource2"]
+sources_changed:
+ description: A list of sources changed (e.g. re/created, removed) for the log;
+ this is empty if no sources are changed.
+ returned: always
+ type: list
+ sample: ["NewLogSource2"]
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_eventlog_entry.ps1 b/ansible_collections/community/windows/plugins/modules/win_eventlog_entry.ps1
new file mode 100644
index 00000000..3934ecf3
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_eventlog_entry.ps1
@@ -0,0 +1,106 @@
+#!powershell
+
+# Copyright: (c) 2017, Andrew Saraceni <andrew.saraceni@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+$ErrorActionPreference = "Stop"
+
+function Test-LogExistence {
+ <#
+ .SYNOPSIS
+ Get information on a log's existence.
+ #>
+ param(
+ [String]$LogName
+ )
+
+ $log_exists = $false
+ $log = Get-EventLog -List | Where-Object { $_.Log -eq $LogName }
+ if ($log) {
+ $log_exists = $true
+ }
+ return $log_exists
+}
+
+function Test-SourceExistence {
+ <#
+ .SYNOPSIS
+ Get information on a source's existence.
+ #>
+ param(
+ [String]$LogName,
+ [String]$SourceName
+ )
+
+ $source_exists = [System.Diagnostics.EventLog]::SourceExists($SourceName)
+
+ if ($source_exists) {
+ $source_log = [System.Diagnostics.EventLog]::LogNameFromSourceName($SourceName, ".")
+ if ($source_log -ne $LogName) {
+ Fail-Json -obj $result -message "Source $SourceName does not belong to log $LogName and cannot be written to"
+ }
+ }
+
+ return $source_exists
+}
+
+$params = Parse-Args $args -supports_check_mode $true
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
+
+$log = Get-AnsibleParam -obj $params -name "log" -type "str" -failifempty $true
+$source = Get-AnsibleParam -obj $params -name "source" -type "str" -failifempty $true
+$event_id = Get-AnsibleParam -obj $params -name "event_id" -type "int" -failifempty $true
+$message = Get-AnsibleParam -obj $params -name "message" -type "str" -failifempty $true
+$entry_type = Get-AnsibleParam -obj $params -name "entry_type" -type "str" -validateset "Error", "FailureAudit", "Information", "SuccessAudit", "Warning"
+$category = Get-AnsibleParam -obj $params -name "category" -type "int"
+$raw_data = Get-AnsibleParam -obj $params -name "raw_data" -type "str"
+
+$result = @{
+ changed = $false
+}
+
+$log_exists = Test-LogExistence -LogName $log
+if (!$log_exists) {
+ Fail-Json -obj $result -message "Log $log does not exist and cannot be written to"
+}
+
+$source_exists = Test-SourceExistence -LogName $log -SourceName $source
+if (!$source_exists) {
+ Fail-Json -obj $result -message "Source $source does not exist"
+}
+
+if ($event_id -lt 0 -or $event_id -gt 65535) {
+ Fail-Json -obj $result -message "Event ID must be between 0 and 65535"
+}
+
+$write_params = @{
+ LogName = $log
+ Source = $source
+ EventId = $event_id
+ Message = $message
+}
+
+try {
+ if ($entry_type) {
+ $write_params.EntryType = $entry_type
+ }
+ if ($category) {
+ $write_params.Category = $category
+ }
+ if ($raw_data) {
+ $write_params.RawData = [Byte[]]($raw_data -split ",")
+ }
+
+ if (!$check_mode) {
+ Write-EventLog @write_params
+ }
+ $result.changed = $true
+ $result.msg = "Entry added to log $log from source $source"
+}
+catch {
+ Fail-Json -obj $result -message $_.Exception.Message
+}
+
+Exit-Json -obj $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_eventlog_entry.py b/ansible_collections/community/windows/plugins/modules/win_eventlog_entry.py
new file mode 100644
index 00000000..2d474d58
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_eventlog_entry.py
@@ -0,0 +1,78 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2017, Andrew Saraceni <andrew.saraceni@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_eventlog_entry
+short_description: Write entries to Windows event logs
+description:
+ - Write log entries to a given event log from a specified source.
+options:
+ log:
+ description:
+ - Name of the event log to write an entry to.
+ type: str
+ required: yes
+ source:
+ description:
+ - Name of the log source to indicate where the entry is from.
+ type: str
+ required: yes
+ event_id:
+ description:
+ - The numeric event identifier for the entry.
+ - Value must be between 0 and 65535.
+ type: int
+ required: yes
+ message:
+ description:
+ - The message for the given log entry.
+ type: str
+ required: yes
+ entry_type:
+ description:
+ - Indicates the entry being written to the log is of a specific type.
+ type: str
+ choices: [ Error, FailureAudit, Information, SuccessAudit, Warning ]
+ category:
+ description:
+ - A numeric task category associated with the category message file for the log source.
+ type: int
+ raw_data:
+ description:
+ - Binary data associated with the log entry.
+ - Value must be a comma-separated array of 8-bit unsigned integers (0 to 255).
+ type: str
+notes:
+ - This module will always report a change when writing an event entry.
+seealso:
+- module: community.windows.win_eventlog
+author:
+ - Andrew Saraceni (@andrewsaraceni)
+'''
+
+EXAMPLES = r'''
+- name: Write an entry to a Windows event log
+ community.windows.win_eventlog_entry:
+ log: MyNewLog
+ source: NewLogSource1
+ event_id: 1234
+ message: This is a test log entry.
+
+- name: Write another entry to a different Windows event log
+ community.windows.win_eventlog_entry:
+ log: AnotherLog
+ source: MyAppSource
+ event_id: 5000
+ message: An error has occurred.
+ entry_type: Error
+ category: 5
+ raw_data: 10,20
+'''
+
+RETURN = r'''
+# Default return values
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_feature_info.ps1 b/ansible_collections/community/windows/plugins/modules/win_feature_info.ps1
new file mode 100644
index 00000000..4942a575
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_feature_info.ps1
@@ -0,0 +1,45 @@
+#!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 = @{
+ name = @{ type = "str"; default = '*' }
+ }
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$name = $module.Params.name
+
+$module.Result.exists = $false
+
+$features = Get-WindowsFeature -Name $name
+
+$module.Result.features = @(foreach ($feature in ($features)) {
+ # These should closely reflect the options for win_feature
+ @{
+ name = $feature.Name
+ display_name = $feature.DisplayName
+ description = $feature.Description
+ installed = $feature.Installed
+ install_state = $feature.InstallState.ToString()
+ feature_type = $feature.FeatureType
+ path = $feature.Path
+ depth = $feature.Depth
+ depends_on = $feature.DependsOn
+ parent = $feature.Parent
+ server_component_descriptor = $feature.ServerComponentDescriptor
+ sub_features = $feature.SubFeatures
+ system_service = $feature.SystemService
+ best_practices_model_id = $feature.BestPracticesModelId
+ event_query = $feature.EventQuery
+ post_configuration_needed = $feature.PostConfigurationNeeded
+ additional_info = $feature.AdditionalInfo
+ }
+ $module.Result.exists = $true
+ })
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_feature_info.py b/ansible_collections/community/windows/plugins/modules/win_feature_info.py
new file mode 100644
index 00000000..c35d8c36
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_feature_info.py
@@ -0,0 +1,166 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2020, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_feature_info
+version_added: '1.4.0'
+short_description: Gather information about Windows features
+description:
+- Gather information about all or a specific installed Windows feature(s).
+options:
+ name:
+ description:
+ - If specified, this is used to match the C(name) of the Windows feature to get the info for.
+ - Can be a wildcard to match multiple features but the wildcard will only be matched on the C(name) of the feature.
+ - If omitted then all features will returned.
+ type: str
+ default: '*'
+seealso:
+- module: ansible.windows.win_feature
+author:
+- Larry Lane (@gamethis)
+'''
+
+EXAMPLES = r'''
+- name: Get info for all installed features
+ community.windows.win_feature_info:
+ register: feature_info
+- name: Get info for a single feature
+ community.windows.win_feature_info:
+ name: DNS
+ register: feature_info
+- name: Find all features that start with 'FS'
+ ansible.windows.win_feature_info:
+ name: FS*
+'''
+
+RETURN = r'''
+exists:
+ description: Whether any features were found based on the criteria specified.
+ returned: always
+ type: bool
+ sample: true
+features:
+ description:
+ - A list of feature(s) that were found based on the criteria.
+ - Will be an empty list if no features were found.
+ returned: always
+ type: list
+ elements: dict
+ contains:
+ name:
+ description:
+ - Name of feature found.
+ type: str
+ sample: AD-Certificate
+ display_name:
+ description:
+ - The Display name of feature found.
+ type: str
+ sample: Active Directory Certificate Services
+ description:
+ description:
+ - The description of the feature.
+ type: str
+ sample: Example description of the Windows feature.
+ installed:
+ description:
+ - Whether the feature by C(name) is installed.
+ type: bool
+ sample: false
+ install_state:
+ description:
+ - The Install State of C(name).
+ - Values will be one of C(Available), C(Removed), C(Installed).
+ type: str
+ sample: Installed
+ feature_type:
+ description:
+ - The Feature Type of C(name).
+ - Values will be one of C(Role), C(Role Service), C(Feature).
+ type: str
+ sample: Feature
+ path:
+ description:
+ - The Path of C(name) feature.
+ type: str
+ sample: WoW64 Support
+ depth:
+ description:
+ - Depth of C(name) feature.
+ type: int
+ sample: 1
+ depends_on:
+ description:
+ - The command line that will be run when a C(run_command) failure action is fired.
+ type: list
+ elements: str
+ sample: ['Web-Static-Content', 'Web-Default-Doc']
+ parent:
+ description:
+ - The parent of feature C(name) if present.
+ type: str
+ sample: PowerShellRoot
+ server_component_descriptor:
+ description:
+ - Descriptor of C(name) feature.
+ type: str
+ sample: ServerComponent_AD_Certificate
+ sub_features:
+ description:
+ - List of sub features names of feature C(name).
+ type: list
+ elements: str
+ sample: ['WAS-Process-Model', 'WAS-NET-Environment', 'WAS-Config-APIs']
+ system_service:
+ description:
+ - The name of the service installed by feature C(name).
+ type: list
+ elements: str
+ sample: ['iisadmin', 'w3svc']
+ best_practices_model_id:
+ description:
+ - BestPracticesModelId for feature C(name).
+ type: str
+ sample: Microsoft/Windows/UpdateServices
+ event_query:
+ description:
+ - The EventQuery for feature C(name).
+ - This will be C(null) if None Present
+ type: str
+ sample: IPAMServer.Events.xml
+ post_configuration_needed:
+ description:
+ - Tells if Post Configuration is needed for feature C(name).
+ type: bool
+ sample: False
+ additional_info:
+ description:
+ - A list of privileges that the feature requires and will run with
+ type: dict
+ contains:
+ major_version:
+ description:
+ - Major Version of feature C(name).
+ type: int
+ sample: 8
+ minor_version:
+ description:
+ - Minor Version of feature C(name).
+ type: int
+ sample: 0
+ number_id_version:
+ description:
+ - Numberic Id of feature C(name).
+ type: int
+ sample: 16
+ install_name:
+ description:
+ - The action to perform once triggered, can be C(start_feature) or C(stop_feature).
+ type: str
+ sample: ADCertificateServicesRole
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_file_compression.ps1 b/ansible_collections/community/windows/plugins/modules/win_file_compression.ps1
new file mode 100644
index 00000000..de905922
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_file_compression.ps1
@@ -0,0 +1,120 @@
+#!powershell
+
+# Copyright: (c) 2019, Micah Hunsberger
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+Set-StrictMode -Version 2
+
+$spec = @{
+ options = @{
+ path = @{ type = 'path'; required = $true }
+ state = @{ type = 'str'; default = 'present'; choices = 'absent', 'present' }
+ recurse = @{ type = 'bool'; default = $false }
+ force = @{ type = 'bool'; default = $true }
+ }
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$path = $module.Params.path
+$state = $module.Params.state
+$recurse = $module.Params.recurse
+$force = $module.Params.force
+
+$module.Result.rc = 0
+
+if (-not (Test-Path -LiteralPath $path)) {
+ $module.FailJson("Path to item, $path, does not exist.")
+}
+
+$item = Get-Item -LiteralPath $path -Force # Use -Force for hidden files
+if (-not $item.PSIsContainer -and $recurse) {
+ $module.Warn("The recurse option has no effect when path is not a folder.")
+}
+
+$cim_params = @{
+ ClassName = 'Win32_LogicalDisk'
+ Filter = "DeviceId='$($item.PSDrive.Name):'"
+ Property = @('FileSystem', 'SupportsFileBasedCompression')
+}
+$drive_info = Get-CimInstance @cim_params
+if ($drive_info.SupportsFileBasedCompression -eq $false) {
+ $module.FailJson("Path, $path, is not on a filesystemi '$($drive_info.FileSystem)' that supports file based compression.")
+}
+
+function Get-ReturnCodeMessage {
+ param(
+ [int]$code
+ )
+ switch ($code) {
+ 0 { return "The request was successful." }
+ 2 { return "Access was denied." }
+ 8 { return "An unspecified failure occurred." }
+ 9 { return "The name specified was not valid." }
+ 10 { return "The object specified already exists." }
+ 11 { return "The file system is not NTFS." }
+ 12 { return "The platform is not Windows." }
+ 13 { return "The drive is not the same." }
+ 14 { return "The directory is not empty." }
+ 15 { return "There has been a sharing violation." }
+ 16 { return "The start file specified was not valid." }
+ 17 { return "A privilege required for the operation is not held." }
+ 21 { return "A parameter specified is not valid." }
+ }
+}
+
+function Get-EscapedFileName {
+ param(
+ [string]$FullName
+ )
+ return $FullName.Replace("\", "\\").Replace("'", "\'")
+}
+
+$is_compressed = ($item.Attributes -band [System.IO.FileAttributes]::Compressed) -eq [System.IO.FileAttributes]::Compressed
+$needs_changed = $is_compressed -ne ($state -eq 'present')
+
+if ($force -and $recurse -and $item.PSIsContainer) {
+ if (-not $needs_changed) {
+ # Check the subfolders and files
+ $entries_to_check = $item.EnumerateFileSystemInfos("*", [System.IO.SearchOption]::AllDirectories)
+ foreach ($entry in $entries_to_check) {
+ $is_compressed = ($entry.Attributes -band [System.IO.FileAttributes]::Compressed) -eq [System.IO.FileAttributes]::Compressed
+ if ($is_compressed -ne ($state -eq 'present')) {
+ $needs_changed = $true
+ break
+ }
+ }
+ }
+}
+
+if ($needs_changed) {
+ $module.Result.changed = $true
+ if ($item.PSIsContainer) {
+ $cim_obj = Get-CimInstance -ClassName 'Win32_Directory' -Filter "Name='$(Get-EscapedFileName -FullName $item.FullName)'"
+ }
+ else {
+ $cim_obj = Get-CimInstance -ClassName 'CIM_LogicalFile' -Filter "Name='$(Get-EscapedFileName -FullName $item.FullName)'"
+ }
+ if ($state -eq 'present') {
+ if (-not $module.CheckMode) {
+ $ret = Invoke-CimMethod -InputObject $cim_obj -MethodName 'CompressEx' -Arguments @{ Recursive = $recurse }
+ $module.Result.rc = $ret.ReturnValue
+ }
+ }
+ else {
+ if (-not $module.CheckMode) {
+ $ret = $ret = Invoke-CimMethod -InputObject $cim_obj -MethodName 'UnCompressEx' -Arguments @{ Recursive = $recurse }
+ $module.Result.rc = $ret.ReturnValue
+ }
+ }
+}
+
+$module.Result.msg = Get-ReturnCodeMessage -code $module.Result.rc
+if ($module.Result.rc -ne 0) {
+ $module.FailJson($module.Result.msg)
+}
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_file_compression.py b/ansible_collections/community/windows/plugins/modules/win_file_compression.py
new file mode 100644
index 00000000..265d4bd8
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_file_compression.py
@@ -0,0 +1,93 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2019, Micah Hunsberger (@mhunsber)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_file_compression
+short_description: Alters the compression of files and directories on NTFS partitions.
+description:
+ - This module sets the compressed attribute for files and directories on a filesystem that supports it like NTFS.
+ - NTFS compression can be used to save disk space.
+options:
+ path:
+ description:
+ - The full path of the file or directory to modify.
+ - The path must exist on file system that supports compression like NTFS.
+ required: yes
+ type: path
+ state:
+ description:
+ - Set to C(present) to ensure the I(path) is compressed.
+ - Set to C(absent) to ensure the I(path) is not compressed.
+ type: str
+ choices:
+ - absent
+ - present
+ default: present
+ recurse:
+ description:
+ - Whether to recursively apply changes to all subdirectories and files.
+ - This option only has an effect when I(path) is a directory.
+ - When set to C(false), only applies changes to I(path).
+ - When set to C(true), applies changes to I(path) and all subdirectories and files.
+ type: bool
+ default: false
+ force:
+ description:
+ - This option only has an effect when I(recurse) is C(true)
+ - If C(true), will check the compressed state of all subdirectories and files
+ and make a change if any are different from I(compressed).
+ - If C(false), will only make a change if the compressed state of I(path) is different from I(compressed).
+ - If the folder structure is complex or contains a lot of files, it is recommended to set this
+ option to C(false) so that not every file has to be checked.
+ type: bool
+ default: true
+author:
+ - Micah Hunsberger (@mhunsber)
+notes:
+ - M(community.windows.win_file_compression) sets the file system's compression state, it does not create a zip
+ archive file.
+ - For more about NTFS Compression, see U(http://www.ntfs.com/ntfs-compressed.htm)
+'''
+
+EXAMPLES = r'''
+- name: Compress log files directory
+ community.windows.win_file_compression:
+ path: C:\Logs
+ state: present
+
+- name: Decompress log files directory
+ community.windows.win_file_compression:
+ path: C:\Logs
+ state: absent
+
+- name: Compress reports directory and all subdirectories
+ community.windows.win_file_compression:
+ path: C:\business\reports
+ state: present
+ recurse: yes
+
+# This will only check C:\business\reports for the compressed state
+# If C:\business\reports is compressed, it will not make a change
+# even if one of the child items is uncompressed
+
+- name: Compress reports directory and all subdirectories (quick)
+ community.windows.win_file_compression:
+ path: C:\business\reports
+ compressed: yes
+ recurse: yes
+ force: no
+'''
+
+RETURN = r'''
+rc:
+ description:
+ - The return code of the compress/uncompress operation.
+ - If no changes are made or the operation is successful, rc is 0.
+ returned: always
+ sample: 0
+ type: int
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_file_version.ps1 b/ansible_collections/community/windows/plugins/modules/win_file_version.ps1
new file mode 100644
index 00000000..8ca94e81
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_file_version.ps1
@@ -0,0 +1,63 @@
+#!powershell
+
+# Copyright: (c) 2015, Sam Liu <sam.liu@activenetwork.com>
+# 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
+
+$result = @{
+ win_file_version = @{}
+ changed = $false
+}
+
+$path = Get-AnsibleParam -obj $params -name "path" -type "path" -failifempty $true -resultobj $result
+
+If (-Not (Test-Path -LiteralPath $path -PathType Leaf)) {
+ Fail-Json $result "Specified path $path does not exist or is not a file."
+}
+$ext = [System.IO.Path]::GetExtension($path)
+If ( $ext -notin '.exe', '.dll') {
+ Fail-Json $result "Specified path $path is not a valid file type; must be DLL or EXE."
+}
+
+Try {
+ $_version_fields = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($path)
+ $file_version = $_version_fields.FileVersion
+ If ($null -eq $file_version) {
+ $file_version = ''
+ }
+ $product_version = $_version_fields.ProductVersion
+ If ($null -eq $product_version) {
+ $product_version = ''
+ }
+ $file_major_part = $_version_fields.FileMajorPart
+ If ($null -eq $file_major_part) {
+ $file_major_part = ''
+ }
+ $file_minor_part = $_version_fields.FileMinorPart
+ If ($null -eq $file_minor_part) {
+ $file_minor_part = ''
+ }
+ $file_build_part = $_version_fields.FileBuildPart
+ If ($null -eq $file_build_part) {
+ $file_build_part = ''
+ }
+ $file_private_part = $_version_fields.FilePrivatePart
+ If ($null -eq $file_private_part) {
+ $file_private_part = ''
+ }
+}
+Catch {
+ Fail-Json $result "Error: $_.Exception.Message"
+}
+
+$result.win_file_version.path = $path.toString()
+$result.win_file_version.file_version = $file_version.toString()
+$result.win_file_version.product_version = $product_version.toString()
+$result.win_file_version.file_major_part = $file_major_part.toString()
+$result.win_file_version.file_minor_part = $file_minor_part.toString()
+$result.win_file_version.file_build_part = $file_build_part.toString()
+$result.win_file_version.file_private_part = $file_private_part.toString()
+Exit-Json $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_file_version.py b/ansible_collections/community/windows/plugins/modules/win_file_version.py
new file mode 100644
index 00000000..15fec88d
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_file_version.py
@@ -0,0 +1,73 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2015, Sam Liu <sam.liu@activenetwork.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_file_version
+short_description: Get DLL or EXE file build version
+description:
+ - Get DLL or EXE file build version.
+notes:
+ - This module will always return no change.
+options:
+ path:
+ description:
+ - File to get version.
+ - Always provide absolute path.
+ type: path
+ required: yes
+seealso:
+- module: ansible.windows.win_file
+author:
+- Sam Liu (@SamLiu79)
+'''
+
+EXAMPLES = r'''
+- name: Get acm instance version
+ community.windows.win_file_version:
+ path: C:\Windows\System32\cmd.exe
+ register: exe_file_version
+
+- debug:
+ msg: '{{ exe_file_version }}'
+'''
+
+RETURN = r'''
+path:
+ description: file path
+ returned: always
+ type: str
+
+file_version:
+ description: File version number..
+ returned: no error
+ type: str
+
+product_version:
+ description: The version of the product this file is distributed with.
+ returned: no error
+ type: str
+
+file_major_part:
+ description: the major part of the version number.
+ returned: no error
+ type: str
+
+file_minor_part:
+ description: the minor part of the version number of the file.
+ returned: no error
+ type: str
+
+file_build_part:
+ description: build number of the file.
+ returned: no error
+ type: str
+
+file_private_part:
+ description: file private part number.
+ returned: no error
+ type: str
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_firewall.ps1 b/ansible_collections/community/windows/plugins/modules/win_firewall.ps1
new file mode 100644
index 00000000..9a203986
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_firewall.ps1
@@ -0,0 +1,90 @@
+#!powershell
+
+# Copyright: (c) 2017, Michael Eaton <meaton@iforium.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+$ErrorActionPreference = "Stop"
+$firewall_profiles = @('Domain', 'Private', 'Public')
+
+$params = Parse-Args $args -supports_check_mode $true
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
+
+$profiles = Get-AnsibleParam -obj $params -name "profiles" -type "list" -default @("Domain", "Private", "Public")
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -failifempty $true -validateset 'disabled', 'enabled'
+$inbound_action = Get-AnsibleParam -obj $params -name "inbound_action" -type "str" -validateset 'allow', 'block', 'not_configured'
+$outbound_action = Get-AnsibleParam -obj $params -name "outbound_action" -type "str" -validateset 'allow', 'block', 'not_configured'
+
+$result = @{
+ changed = $false
+ profiles = $profiles
+ state = $state
+}
+
+try {
+ get-command Get-NetFirewallProfile > $null
+ get-command Set-NetFirewallProfile > $null
+}
+catch {
+ Fail-Json $result "win_firewall requires Get-NetFirewallProfile and Set-NetFirewallProfile Cmdlets."
+}
+
+$FIREWALL_ENABLED = [Microsoft.PowerShell.Cmdletization.GeneratedTypes.NetSecurity.GpoBoolean]::True
+$FIREWALL_DISABLED = [Microsoft.PowerShell.Cmdletization.GeneratedTypes.NetSecurity.GpoBoolean]::False
+
+Try {
+
+ ForEach ($profile in $firewall_profiles) {
+ $current_profile = Get-NetFirewallProfile -Name $profile
+ $currentstate = $current_profile.Enabled
+ $current_inboundaction = $current_profile.DefaultInboundAction
+ $current_outboundaction = $current_profile.DefaultOutboundAction
+ $result.$profile = @{
+ enabled = ($currentstate -eq $FIREWALL_ENABLED)
+ considered = ($profiles -contains $profile)
+ currentstate = $currentstate
+ }
+
+ if ($profiles -notcontains $profile) {
+ continue
+ }
+
+ if ($state -eq 'enabled') {
+
+ if ($currentstate -eq $FIREWALL_DISABLED) {
+ Set-NetFirewallProfile -name $profile -Enabled true -WhatIf:$check_mode
+ $result.changed = $true
+ $result.$profile.enabled = $true
+ }
+ if ($null -ne $inbound_action) {
+ $inbound_action = [Globalization.CultureInfo]::InvariantCulture.TextInfo.ToTitleCase($inbound_action.ToLower()) -replace '_', ''
+ if ($inbound_action -ne $current_inboundaction) {
+ Set-NetFirewallProfile -name $profile -DefaultInboundAction $inbound_action -WhatIf:$check_mode
+ $result.changed = $true
+ }
+ }
+ if ($null -ne $outbound_action) {
+ $outbound_action = [Globalization.CultureInfo]::InvariantCulture.TextInfo.ToTitleCase($outbound_action.ToLower()) -replace '_', ''
+ if ($outbound_action -ne $current_outboundaction) {
+ Set-NetFirewallProfile -name $profile -DefaultOutboundAction $outbound_action -WhatIf:$check_mode
+ $result.changed = $true
+ }
+ }
+ }
+ else {
+
+ if ($currentstate -eq $FIREWALL_ENABLED) {
+ Set-NetFirewallProfile -name $profile -Enabled false -WhatIf:$check_mode
+ $result.changed = $true
+ $result.$profile.enabled = $false
+ }
+
+ }
+ }
+}
+Catch {
+ Fail-Json $result "an error occurred when attempting to change firewall status for profile $profile $($_.Exception.Message)"
+}
+
+Exit-Json $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_firewall.py b/ansible_collections/community/windows/plugins/modules/win_firewall.py
new file mode 100644
index 00000000..ba9f2864
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_firewall.py
@@ -0,0 +1,89 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2017, Michael Eaton <meaton@iforium.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_firewall
+short_description: Enable or disable the Windows Firewall
+description:
+- Enable or Disable Windows Firewall profiles.
+requirements:
+ - This module requires Windows Management Framework 5 or later.
+options:
+ profiles:
+ description:
+ - Specify one or more profiles to change.
+ type: list
+ elements: str
+ choices: [ Domain, Private, Public ]
+ default: [ Domain, Private, Public ]
+ state:
+ description:
+ - Set state of firewall for given profile.
+ type: str
+ choices: [ disabled, enabled ]
+ inbound_action:
+ description:
+ - Set to C(allow) or C(block) inbound network traffic in the profile.
+ - C(not_configured) is valid when configuring a GPO.
+ type: str
+ choices: [ allow, block, not_configured ]
+ version_added: 1.1.0
+ outbound_action:
+ description:
+ - Set to C(allow) or C(block) inbound network traffic in the profile.
+ - C(not_configured) is valid when configuring a GPO.
+ type: str
+ choices: [ allow, block, not_configured ]
+ version_added: 1.1.0
+seealso:
+- module: community.windows.win_firewall_rule
+author:
+- Michael Eaton (@michaeldeaton)
+'''
+
+EXAMPLES = r'''
+- name: Enable firewall for Domain, Public and Private profiles
+ community.windows.win_firewall:
+ state: enabled
+ profiles:
+ - Domain
+ - Private
+ - Public
+ tags: enable_firewall
+
+- name: Disable Domain firewall
+ community.windows.win_firewall:
+ state: disabled
+ profiles:
+ - Domain
+ tags: disable_firewall
+
+- name: Enable firewall for Domain profile and block outbound connections
+ community.windows.win_firewall:
+ profiles: Domain
+ state: enabled
+ outbound_action: block
+ tags: block_connection
+'''
+
+RETURN = r'''
+enabled:
+ description: Current firewall status for chosen profile (after any potential change).
+ returned: always
+ type: bool
+ sample: true
+profiles:
+ description: Chosen profile.
+ returned: always
+ type: str
+ sample: Domain
+state:
+ description: Desired state of the given firewall profile(s).
+ returned: always
+ type: list
+ sample: enabled
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_firewall_rule.ps1 b/ansible_collections/community/windows/plugins/modules/win_firewall_rule.ps1
new file mode 100644
index 00000000..bdbf8894
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_firewall_rule.ps1
@@ -0,0 +1,315 @@
+#!powershell
+
+# Copyright: (c) 2014, Timothy Vandenbrande <timothy.vandenbrande@gmail.com>
+# Copyright: (c) 2017, Artem Zinenko <zinenkoartem@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
+
+function ConvertTo-ProtocolType {
+ param($protocol)
+
+ $protocolNumber = $protocol -as [int]
+ if ($protocolNumber -is [int]) {
+ return $protocolNumber
+ }
+
+ switch -wildcard ($protocol) {
+ "tcp" { return [System.Net.Sockets.ProtocolType]::Tcp -as [int] }
+ "udp" { return [System.Net.Sockets.ProtocolType]::Udp -as [int] }
+ "icmpv4*" { return [System.Net.Sockets.ProtocolType]::Icmp -as [int] }
+ "icmpv6*" { return [System.Net.Sockets.ProtocolType]::IcmpV6 -as [int] }
+ default { throw "Unknown protocol '$protocol'." }
+ }
+}
+
+# See 'Direction' constants here: https://msdn.microsoft.com/en-us/library/windows/desktop/aa364724(v=vs.85).aspx
+function ConvertTo-Direction {
+ param($directionStr)
+
+ switch ($directionStr) {
+ "in" { return 1 }
+ "out" { return 2 }
+ default { throw "Unknown direction '$directionStr'." }
+ }
+}
+
+# See 'Action' constants here: https://msdn.microsoft.com/en-us/library/windows/desktop/aa364724(v=vs.85).aspx
+function ConvertTo-Action {
+ param($actionStr)
+
+ switch ($actionStr) {
+ "block" { return 0 }
+ "allow" { return 1 }
+ default { throw "Unknown action '$actionStr'." }
+ }
+}
+
+# Profile enum values: https://msdn.microsoft.com/en-us/library/windows/desktop/aa366303(v=vs.85).aspx
+function ConvertTo-Profile {
+ param($profilesList)
+
+ $profiles = ($profilesList | Select-Object -Unique | ForEach-Object {
+ switch ($_) {
+ "domain" { return 1 }
+ "private" { return 2 }
+ "public" { return 4 }
+ default { throw "Unknown profile '$_'." }
+ }
+ } | Measure-Object -Sum).Sum
+
+ if ($profiles -eq 7) { return 0x7fffffff }
+ return $profiles
+}
+
+function ConvertTo-InterfaceType {
+ param($interfaceTypes)
+
+ return ($interfaceTypes | Select-Object -Unique | ForEach-Object {
+ switch ($_) {
+ "wireless" { return "Wireless" }
+ "lan" { return "Lan" }
+ "ras" { return "RemoteAccess" }
+ default { throw "Unknown interface type '$_'." }
+ }
+ }) -Join ","
+}
+
+function ConvertTo-EdgeTraversalOption {
+ param($edgeTraversalOptionsStr)
+
+ switch ($edgeTraversalOptionsStr) {
+ "yes" { return 1 }
+ "deferapp" { return 2 }
+ "deferuser" { return 3 }
+ default { throw "Unknown edge traversal options '$edgeTraversalOptionsStr'." }
+ }
+}
+
+function ConvertTo-SecureFlag {
+ param($secureFlagsStr)
+
+ switch ($secureFlagsStr) {
+ "authnoencap" { return 1 }
+ "authenticate" { return 2 }
+ "authdynenc" { return 3 }
+ "authenc" { return 4 }
+ default { throw "Unknown secure flags '$secureFlagsStr'." }
+ }
+}
+
+$ErrorActionPreference = "Stop"
+
+$result = @{
+ changed = $false
+}
+
+$params = Parse-Args $args -supports_check_mode $true
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
+$diff_support = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false
+
+$name = Get-AnsibleParam -obj $params -name "name"
+$description = Get-AnsibleParam -obj $params -name "description" -type "str"
+$direction = Get-AnsibleParam -obj $params -name "direction" -type "str" -validateset "in", "out"
+$action = Get-AnsibleParam -obj $params -name "action" -type "str" -validateset "allow", "block"
+$program = Get-AnsibleParam -obj $params -name "program" -type "str"
+$group = Get-AnsibleParam -obj $params -name "group" -type "str"
+$service = Get-AnsibleParam -obj $params -name "service" -type "str"
+$enabled = Get-AnsibleParam -obj $params -name "enabled" -type "bool" -aliases "enable"
+$profiles = Get-AnsibleParam -obj $params -name "profiles" -type "list" -aliases "profile"
+$localip = Get-AnsibleParam -obj $params -name "localip" -type "str"
+$remoteip = Get-AnsibleParam -obj $params -name "remoteip" -type "str"
+$localport = Get-AnsibleParam -obj $params -name "localport" -type "str"
+$remoteport = Get-AnsibleParam -obj $params -name "remoteport" -type "str"
+$protocol = Get-AnsibleParam -obj $params -name "protocol" -type "str"
+$interfacetypes = Get-AnsibleParam -obj $params -name "interfacetypes" -type "list"
+$edge = Get-AnsibleParam -obj $params -name "edge" -type "str" -validateset "no", "yes", "deferapp", "deferuser"
+$security_options = "notrequired", "authnoencap", "authenticate", "authdynenc", "authenc"
+$security = Get-AnsibleParam -obj $params -name "security" -type "str" -validateset $security_options
+$icmp_type_code = Get-AnsibleParam -obj $params -name "icmp_type_code" -type "list"
+
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present", "absent"
+
+if (-not $name -and -not $group) {
+ Fail-Json -obj $result -message "Either name or group must be specified"
+}
+
+if ($diff_support) {
+ $result.diff = @{}
+ $result.diff.prepared = ""
+}
+
+if ($null -ne $icmp_type_code) {
+ # COM representation is just "<type>:<code>,<type2>:<code>" so we just join our list
+ $icmp_type_code = $icmp_type_code -join ","
+}
+
+try {
+ $fw = New-Object -ComObject HNetCfg.FwPolicy2
+
+ # If name was specified, filter the rules by name, otherwise find all the rules in the group.
+ $existingRules = $fw.Rules | Where-Object {
+ if ($name) {
+ $_.Name -eq $name
+ }
+ else {
+ $_.Grouping -eq $group
+ }
+ }
+
+ # INetFwRule interface description: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365344(v=vs.85).aspx
+ $new_rule = New-Object -ComObject HNetCfg.FWRule
+ if ($name) {
+ $new_rule.Name = $name
+ }
+ # the default for enabled in module description is "true", but the actual COM object defaults to "false" when created
+ if ($null -ne $enabled) { $new_rule.Enabled = $enabled } else { $new_rule.Enabled = $true }
+ if ($null -ne $description) { $new_rule.Description = $description }
+ if ($null -ne $group) { $new_rule.Grouping = $group }
+ if ($null -ne $program -and $program -ne "any") { $new_rule.ApplicationName = [System.Environment]::ExpandEnvironmentVariables($program) }
+ if ($null -ne $service -and $service -ne "any") { $new_rule.ServiceName = $service }
+ if ($null -ne $protocol -and $protocol -ne "any") { $new_rule.Protocol = ConvertTo-ProtocolType -protocol $protocol }
+ if ($null -ne $localport -and $localport -ne "any") { $new_rule.LocalPorts = $localport }
+ if ($null -ne $remoteport -and $remoteport -ne "any") { $new_rule.RemotePorts = $remoteport }
+ if ($null -ne $localip -and $localip -ne "any") { $new_rule.LocalAddresses = $localip }
+ if ($null -ne $remoteip -and $remoteip -ne "any") { $new_rule.RemoteAddresses = $remoteip }
+ if ($null -ne $icmp_type_code -and $icmp_type_code -ne "any") { $new_rule.IcmpTypesAndCodes = $icmp_type_code }
+ if ($null -ne $direction) { $new_rule.Direction = ConvertTo-Direction -directionStr $direction }
+ if ($null -ne $action) { $new_rule.Action = ConvertTo-Action -actionStr $action }
+ # Profiles value cannot be a uint32, but the "all profiles" value (0x7FFFFFFF) will often become a uint32, so must cast to [int]
+ if ($null -ne $profiles) { $new_rule.Profiles = [int](ConvertTo-Profile -profilesList $profiles) }
+ if ($null -ne $interfacetypes -and @(Compare-Object -ReferenceObject $interfacetypes -DifferenceObject @("any")).Count -ne 0) {
+ $new_rule.InterfaceTypes = ConvertTo-InterfaceType -interfaceTypes $interfacetypes
+ }
+ if ($null -ne $edge -and $edge -ne "no") {
+ # EdgeTraversalOptions property exists only from Windows 7/Windows Server 2008 R2
+ # https://msdn.microsoft.com/en-us/library/windows/desktop/dd607256(v=vs.85).aspx
+ if ($new_rule | Get-Member -Name 'EdgeTraversalOptions') {
+ $new_rule.EdgeTraversalOptions = ConvertTo-EdgeTraversalOption -edgeTraversalOptionsStr $edge
+ }
+ }
+ if ($null -ne $security -and $security -ne "notrequired") {
+ # SecureFlags property exists only from Windows 8/Windows Server 2012
+ # https://msdn.microsoft.com/en-us/library/windows/desktop/hh447465(v=vs.85).aspx
+ if ($new_rule | Get-Member -Name 'SecureFlags') {
+ $new_rule.SecureFlags = ConvertTo-SecureFlag -secureFlagsStr $security
+ }
+ }
+
+ $fwPropertiesToCompare = @('Description', 'Direction', 'Action', 'ApplicationName', 'Grouping', 'ServiceName', 'Enabled',
+ 'Profiles', 'LocalAddresses', 'RemoteAddresses', 'LocalPorts', 'RemotePorts', 'Protocol', 'InterfaceTypes',
+ 'EdgeTraversalOptions', 'SecureFlags', 'IcmpTypesAndCodes')
+ $userPassedArguments = @($description, $direction, $action, $program, $group, $service, $enabled, $profiles, $localip,
+ $remoteip, $localport, $remoteport, $protocol, $interfacetypes, $edge, $security, $icmp_type_code)
+
+ if ($state -eq "absent") {
+ if (-not $existingRules) {
+ if ($name) {
+ $result.msg = "Firewall rule '$name' does not exist."
+ }
+ else {
+ $result.msg = "No firewall rules in group '$group' exist."
+ }
+
+ }
+ else {
+ $rules = foreach ($rule in $existingRules) {
+ $rule.Name # Output name for module msg string.
+
+ if ($diff_support) {
+ $result.diff.prepared += "-[$($rule.Name)]`n"
+ foreach ($prop in $fwPropertiesToCompare) {
+ $result.diff.prepared += "-$($prop)='$($rule.$prop)'`n"
+ }
+ }
+
+ if (-not $check_mode) {
+ $fw.Rules.Remove($rule.Name)
+ }
+ $result.changed = $true
+ }
+ $result.msg = "Firewall rule(s) '$($rules -join "', '")' removed."
+ }
+ }
+ elseif ($state -eq "present") {
+ if (-not $existingRules -and $name) {
+ # name was specified and no rules were found, create the rule
+ if ($diff_support) {
+ $result.diff.prepared += "+[$($new_rule.Name)]`n"
+ foreach ($prop in $fwPropertiesToCompare) {
+ $result.diff.prepared += "+$($prop)='$($new_rule.$prop)'`n"
+ }
+ }
+
+ if (-not $check_mode) {
+ $fw.Rules.Add($new_rule)
+ }
+ $result.changed = $true
+ $result.msg = "Firewall rule '$name' created."
+ }
+ elseif ($existingRules) {
+ # Either name or group was specified which matched existing rules, check the properties
+ $changedRules = [System.Collections.Generic.List[String]]@()
+ $unchangedRules = [System.Collections.Generic.List[String]]@()
+
+ foreach ($existingRule in $existingRules) {
+ if ($diff_support) {
+ $result.diff.prepared += "[$($existingRule.Name)]`n"
+ }
+
+ $changed = $false
+ for ($i = 0; $i -lt $fwPropertiesToCompare.Length; $i++) {
+ $prop = $fwPropertiesToCompare[$i]
+ if ($null -ne $userPassedArguments[$i]) {
+ # only change values the user passes in task definition
+ if ($existingRule.$prop -ne $new_rule.$prop) {
+ $haveSameAddresses = $false
+ if ($prop -like "*Addresses") {
+ $existingAddresses = $existingRule.$prop -split ','
+ $newAddresses = $new_rule.$prop -split ','
+ if (-not (Compare-Object $existingAddresses $newAddresses)) {
+ $haveSameAddresses = $true
+ }
+ }
+ if (-not $haveSameAddresses) {
+ if ($diff_support) {
+ $result.diff.prepared += "-$($prop)='$($existingRule.$prop)'`n"
+ $result.diff.prepared += "+$($prop)='$($new_rule.$prop)'`n"
+ }
+
+ if (-not $check_mode) {
+ # Profiles value cannot be a uint32, but the "all profiles" value (0x7FFFFFFF) will often become a uint32,
+ # so must cast to [int] to prevent InvalidCastException under PS5+
+ If ($prop -eq 'Profiles') {
+ $existingRule.Profiles = [int] $new_rule.$prop
+ }
+ Else {
+ $existingRule.$prop = $new_rule.$prop
+ }
+ }
+ $changed = $true
+ }
+ }
+ }
+ }
+
+ if ($changed) {
+ $result.changed = $true
+ $changedRules.Add($existingRule.Name)
+ }
+ else {
+ $unchangedRules.Add($existingRule.Name)
+ }
+ }
+
+ $result.msg = "Firewall rule(s) changed '$($changedRules -join "', '")' - unchanged '$($unchangedRules -join "', '")'"
+ }
+ }
+}
+catch [Exception] {
+ $ex = $_
+ $result['exception'] = $($ex | Out-String)
+ Fail-Json $result $ex.Exception.Message
+}
+
+Exit-Json $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_firewall_rule.py b/ansible_collections/community/windows/plugins/modules/win_firewall_rule.py
new file mode 100644
index 00000000..111d45a1
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_firewall_rule.py
@@ -0,0 +1,196 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2014, Timothy Vandenbrande <timothy.vandenbrande@gmail.com>
+# Copyright: (c) 2017, Artem Zinenko <zinenkoartem@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_firewall_rule
+short_description: Windows firewall automation
+description:
+ - Allows you to create/remove/update firewall rules.
+options:
+ enabled:
+ description:
+ - Whether this firewall rule is enabled or disabled.
+ - Defaults to C(true) when creating a new rule.
+ type: bool
+ aliases: [ enable ]
+ state:
+ description:
+ - Should this rule be added or removed.
+ type: str
+ choices: [ absent, present ]
+ default: present
+ name:
+ description:
+ - The rule's display name.
+ - This is required unless I(group) is specified.
+ type: str
+ group:
+ description:
+ - The group name for the rule.
+ - If I(name) is not specified then the module will set the firewall options for all the rules in this group.
+ type: str
+ direction:
+ description:
+ - Whether this rule is for inbound or outbound traffic.
+ - Defaults to C(in) when creating a new rule.
+ type: str
+ choices: [ in, out ]
+ action:
+ description:
+ - What to do with the items this rule is for.
+ - Defaults to C(allow) when creating a new rule.
+ type: str
+ choices: [ allow, block ]
+ description:
+ description:
+ - Description for the firewall rule.
+ type: str
+ localip:
+ description:
+ - The local ip address this rule applies to.
+ - Set to C(any) to apply to all local ip addresses.
+ - Defaults to C(any) when creating a new rule.
+ type: str
+ remoteip:
+ description:
+ - The remote ip address/range this rule applies to.
+ - Set to C(any) to apply to all remote ip addresses.
+ - Defaults to C(any) when creating a new rule.
+ type: str
+ localport:
+ description:
+ - The local port this rule applies to.
+ - Set to C(any) to apply to all local ports.
+ - Defaults to C(any) when creating a new rule.
+ - Must have I(protocol) set
+ type: str
+ remoteport:
+ description:
+ - The remote port this rule applies to.
+ - Set to C(any) to apply to all remote ports.
+ - Defaults to C(any) when creating a new rule.
+ - Must have I(protocol) set
+ type: str
+ program:
+ description:
+ - The program this rule applies to.
+ - Set to C(any) to apply to all programs.
+ - Defaults to C(any) when creating a new rule.
+ type: str
+ service:
+ description:
+ - The service this rule applies to.
+ - Set to C(any) to apply to all services.
+ - Defaults to C(any) when creating a new rule.
+ type: str
+ protocol:
+ description:
+ - The protocol this rule applies to.
+ - Set to C(any) to apply to all services.
+ - Defaults to C(any) when creating a new rule.
+ type: str
+ profiles:
+ description:
+ - The profile this rule applies to.
+ - Defaults to C(domain,private,public) when creating a new rule.
+ type: list
+ elements: str
+ aliases: [ profile ]
+ icmp_type_code:
+ description:
+ - The ICMP types and codes for the rule.
+ - This is only valid when I(protocol) is C(icmpv4) or C(icmpv6).
+ - Each entry follows the format C(type:code) where C(type) is the type
+ number and C(code) is the code number for that type or C(*) for all
+ codes.
+ - Set the value to just C(*) to apply the rule for all ICMP type codes.
+ - See U(https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml)
+ for a list of ICMP types and the codes that apply to them.
+ type: list
+ elements: str
+notes:
+- Multiple firewall rules can share the same I(name), if there are multiple matches then the module will set the user
+ defined options for each matching rule.
+seealso:
+- module: community.windows.win_firewall
+author:
+ - Artem Zinenko (@ar7z1)
+ - Timothy Vandenbrande (@TimothyVandenbrande)
+'''
+
+EXAMPLES = r'''
+- name: Firewall rule to allow SMTP on TCP port 25
+ community.windows.win_firewall_rule:
+ name: SMTP
+ localport: 25
+ action: allow
+ direction: in
+ protocol: tcp
+ state: present
+ enabled: yes
+
+- name: Firewall rule to allow RDP on TCP port 3389
+ community.windows.win_firewall_rule:
+ name: Remote Desktop
+ localport: 3389
+ action: allow
+ direction: in
+ protocol: tcp
+ profiles: private
+ state: present
+ enabled: yes
+
+- name: Firewall rule to be created for application group
+ community.windows.win_firewall_rule:
+ name: SMTP
+ group: application
+ localport: 25
+ action: allow
+ direction: in
+ protocol: tcp
+ state: present
+ enabled: yes
+
+- name: Enable all the Firewall rules in application group
+ win_firewall_rule:
+ group: application
+ enabled: yes
+
+- name: Firewall rule to allow port range
+ community.windows.win_firewall_rule:
+ name: Sample port range
+ localport: 5000-5010
+ action: allow
+ direction: in
+ protocol: tcp
+ state: present
+ enabled: yes
+
+- name: Firewall rule to allow ICMP v4 echo (ping)
+ community.windows.win_firewall_rule:
+ name: ICMP Allow incoming V4 echo request
+ enabled: yes
+ state: present
+ profiles: private
+ action: allow
+ direction: in
+ protocol: icmpv4
+ icmp_type_code:
+ - '8:*'
+
+- name: Firewall rule to alloc ICMP v4 on all type codes
+ community.windows.win_firewall_rule:
+ name: ICMP Allow incoming V4 echo request
+ enabled: yes
+ state: present
+ profiles: private
+ action: allow
+ direction: in
+ protocol: icmpv4
+ icmp_type_code: '*'
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_format.ps1 b/ansible_collections/community/windows/plugins/modules/win_format.ps1
new file mode 100644
index 00000000..0746fbe4
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_format.ps1
@@ -0,0 +1,232 @@
+#!powershell
+
+# Copyright: (c) 2019, Varun Chopra (@chopraaa) <v@chopraaa.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#AnsibleRequires -OSVersion 6.2
+
+Set-StrictMode -Version 2
+
+$ErrorActionPreference = "Stop"
+
+$spec = @{
+ options = @{
+ drive_letter = @{ type = "str" }
+ path = @{ type = "str" }
+ label = @{ type = "str" }
+ new_label = @{ type = "str" }
+ file_system = @{ type = "str"; choices = "ntfs", "refs", "exfat", "fat32", "fat" }
+ allocation_unit_size = @{ type = "int" }
+ large_frs = @{ type = "bool" }
+ full = @{ type = "bool"; default = $false }
+ compress = @{ type = "bool" }
+ integrity_streams = @{ type = "bool" }
+ force = @{ type = "bool"; default = $false }
+ }
+ mutually_exclusive = @(
+ , @('drive_letter', 'path', 'label')
+ )
+ required_one_of = @(
+ , @('drive_letter', 'path', 'label')
+ )
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$drive_letter = $module.Params.drive_letter
+$path = $module.Params.path
+$label = $module.Params.label
+$new_label = $module.Params.new_label
+$file_system = $module.Params.file_system
+$allocation_unit_size = $module.Params.allocation_unit_size
+$large_frs = $module.Params.large_frs
+$full_format = $module.Params.full
+$compress_volume = $module.Params.compress
+$integrity_streams = $module.Params.integrity_streams
+$force_format = $module.Params.force
+
+# Some pre-checks
+if ($null -ne $drive_letter -and $drive_letter -notmatch "^[a-zA-Z]$") {
+ $module.FailJson("The parameter drive_letter should be a single character A-Z")
+}
+if ($integrity_streams -eq $true -and $file_system -ne "refs") {
+ $module.FailJson("Integrity streams can be enabled only on ReFS volumes. You specified: $($file_system)")
+}
+if ($compress_volume -eq $true) {
+ if ($file_system -eq "ntfs") {
+ if ($null -ne $allocation_unit_size -and $allocation_unit_size -gt 4096) {
+ $module.FailJson("NTFS compression is not supported for allocation unit sizes above 4096")
+ }
+ }
+ else {
+ $module.FailJson("Compression can be enabled only on NTFS volumes. You specified: $($file_system)")
+ }
+}
+
+function Get-AnsibleVolume {
+ param(
+ $DriveLetter,
+ $Path,
+ $Label
+ )
+
+ if ($null -ne $DriveLetter) {
+ try {
+ $volume = Get-Volume -DriveLetter $DriveLetter
+ }
+ catch {
+ $module.FailJson("There was an error retrieving the volume using drive_letter $($DriveLetter): $($_.Exception.Message)", $_)
+ }
+ }
+ elseif ($null -ne $Path) {
+ try {
+ $volume = Get-Volume -Path $Path
+ }
+ catch {
+ $module.FailJson("There was an error retrieving the volume using path $($Path): $($_.Exception.Message)", $_)
+ }
+ }
+ elseif ($null -ne $Label) {
+ try {
+ $volume = Get-Volume -FileSystemLabel $Label
+ }
+ catch {
+ $module.FailJson("There was an error retrieving the volume using label $($Label): $($_.Exception.Message)", $_)
+ }
+ }
+ else {
+ $module.FailJson("Unable to locate volume: drive_letter, path and label were not specified")
+ }
+
+ return $volume
+}
+
+function Format-AnsibleVolume {
+ param(
+ $Path,
+ $Label,
+ $FileSystem,
+ $Full,
+ $UseLargeFRS,
+ $Compress,
+ $SetIntegrityStreams,
+ $AllocationUnitSize
+ )
+ $parameters = @{
+ Path = $Path
+ Full = $Full
+ }
+ if ($null -ne $UseLargeFRS) {
+ $parameters.Add("UseLargeFRS", $UseLargeFRS)
+ }
+ if ($null -ne $SetIntegrityStreams) {
+ $parameters.Add("SetIntegrityStreams", $SetIntegrityStreams)
+ }
+ if ($null -ne $Compress) {
+ $parameters.Add("Compress", $Compress)
+ }
+ if ($null -ne $Label) {
+ $parameters.Add("NewFileSystemLabel", $Label)
+ }
+ if ($null -ne $FileSystem) {
+ $parameters.Add("FileSystem", $FileSystem)
+ }
+ if ($null -ne $AllocationUnitSize) {
+ $parameters.Add("AllocationUnitSize", $AllocationUnitSize)
+ }
+
+ Format-Volume @parameters -Confirm:$false | Out-Null
+
+}
+
+$ansible_volume = Get-AnsibleVolume -DriveLetter $drive_letter -Path $path -Label $label
+$ansible_file_system = $ansible_volume.FileSystem
+$ansible_volume_size = $ansible_volume.Size
+$ansible_volume_alu = (Get-CimInstance -ClassName Win32_Volume -Filter "DeviceId = '$($ansible_volume.path.replace('\','\\'))'" -Property BlockSize).BlockSize
+
+$ansible_partition = Get-Partition -Volume $ansible_volume
+
+if (
+ -not $force_format -and
+ $null -ne $allocation_unit_size -and
+ $ansible_volume_alu -ne 0 -and
+ $null -ne $ansible_volume_alu -and
+ $allocation_unit_size -ne $ansible_volume_alu
+) {
+ $msg = -join @(
+ "Force format must be specified since target allocation unit size: $($allocation_unit_size) "
+ "is different from the current allocation unit size of the volume: $($ansible_volume_alu)"
+ )
+ $module.FailJson($msg)
+}
+
+foreach ($access_path in $ansible_partition.AccessPaths) {
+ if ($access_path -ne $Path) {
+ if ($null -ne $file_system -and
+ -not [string]::IsNullOrEmpty($ansible_file_system) -and
+ $file_system -ne $ansible_file_system) {
+ if (-not $force_format) {
+ $no_files_in_volume = (Get-ChildItem -LiteralPath $access_path -ErrorAction SilentlyContinue | Measure-Object).Count -eq 0
+ if ($no_files_in_volume) {
+ $msg = -join @(
+ "Force format must be specified since target file system: $($file_system) "
+ "is different from the current file system of the volume: $($ansible_file_system.ToLower())"
+ )
+ $module.FailJson($msg)
+ }
+ else {
+ $module.FailJson("Force format must be specified to format non-pristine volumes")
+ }
+ }
+ }
+ else {
+ $pristine = -not $force_format
+ }
+ }
+}
+
+if ($force_format) {
+ if (-not $module.CheckMode) {
+ $format_params = @{
+ Path = $ansible_volume.Path
+ Full = $full_format
+ Label = $new_label
+ FileSystem = $file_system
+ SetIntegrityStreams = $integrity_streams
+ UseLargeFRS = $large_frs
+ Compress = $compress_volume
+ AllocationUnitSize = $allocation_unit_size
+ }
+ Format-AnsibleVolume @format_params
+ }
+ $module.Result.changed = $true
+}
+else {
+ if ($pristine) {
+ if ($null -eq $new_label) {
+ $new_label = $ansible_volume.FileSystemLabel
+ }
+ # Conditions for formatting
+ if ($ansible_volume_size -eq 0 -or
+ $ansible_volume.FileSystemLabel -ne $new_label) {
+ if (-not $module.CheckMode) {
+ $format_params = @{
+ Path = $ansible_volume.Path
+ Full = $full_format
+ Label = $new_label
+ FileSystem = $file_system
+ SetIntegrityStreams = $integrity_streams
+ UseLargeFRS = $large_frs
+ Compress = $compress_volume
+ AllocationUnitSize = $allocation_unit_size
+ }
+ Format-AnsibleVolume @format_params
+ }
+ $module.Result.changed = $true
+ }
+ }
+}
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_format.py b/ansible_collections/community/windows/plugins/modules/win_format.py
new file mode 100644
index 00000000..3ff93de0
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_format.py
@@ -0,0 +1,98 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2019, Varun Chopra (@chopraaa) <v@chopraaa.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+module: win_format
+short_description: Formats an existing volume or a new volume on an existing partition on Windows
+description:
+ - The M(community.windows.win_format) module formats an existing volume or a new volume on an existing partition on Windows
+options:
+ drive_letter:
+ description:
+ - Used to specify the drive letter of the volume to be formatted.
+ type: str
+ path:
+ description:
+ - Used to specify the path to the volume to be formatted.
+ type: str
+ label:
+ description:
+ - Used to specify the label of the volume to be formatted.
+ type: str
+ new_label:
+ description:
+ - Used to specify the new file system label of the formatted volume.
+ type: str
+ file_system:
+ description:
+ - Used to specify the file system to be used when formatting the target volume.
+ type: str
+ choices: [ ntfs, refs, exfat, fat32, fat ]
+ allocation_unit_size:
+ description:
+ - Specifies the cluster size to use when formatting the volume.
+ - If no cluster size is specified when you format a partition, defaults are selected based on
+ the size of the partition.
+ - This value must be a multiple of the physical sector size of the disk.
+ type: int
+ large_frs:
+ description:
+ - Specifies that large File Record System (FRS) should be used.
+ type: bool
+ compress:
+ description:
+ - Enable compression on the resulting NTFS volume.
+ - NTFS compression is not supported where I(allocation_unit_size) is more than 4096.
+ type: bool
+ integrity_streams:
+ description:
+ - Enable integrity streams on the resulting ReFS volume.
+ type: bool
+ full:
+ description:
+ - A full format writes to every sector of the disk, takes much longer to perform than the
+ default (quick) format, and is not recommended on storage that is thinly provisioned.
+ - Specify C(true) for full format.
+ type: bool
+ default: no
+ force:
+ description:
+ - Specify if formatting should be forced for volumes that are not created from new partitions
+ or if the source and target file system are different.
+ type: bool
+ default: no
+notes:
+ - Microsoft Windows Server 2012 or Microsoft Windows 8 or newer is required to use this module. To check if your system is compatible, see
+ U(https://docs.microsoft.com/en-us/windows/desktop/sysinfo/operating-system-version).
+ - One of three parameters (I(drive_letter), I(path) and I(label)) are mandatory to identify the target
+ volume but more than one cannot be specified at the same time.
+ - This module is idempotent if I(force) is not specified and file system labels remain preserved.
+ - For more information, see U(https://docs.microsoft.com/en-us/previous-versions/windows/desktop/stormgmt/format-msft-volume)
+seealso:
+ - module: community.windows.win_disk_facts
+ - module: community.windows.win_partition
+author:
+ - Varun Chopra (@chopraaa) <v@chopraaa.com>
+'''
+
+EXAMPLES = r'''
+- name: Create a partition with drive letter D and size 5 GiB
+ community.windows.win_partition:
+ drive_letter: D
+ partition_size: 5 GiB
+ disk_number: 1
+
+- name: Full format the newly created partition as NTFS and label it
+ community.windows.win_format:
+ drive_letter: D
+ file_system: NTFS
+ new_label: Formatted
+ full: True
+'''
+
+RETURN = r'''
+#
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_hosts.ps1 b/ansible_collections/community/windows/plugins/modules/win_hosts.ps1
new file mode 100644
index 00000000..89766d37
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_hosts.ps1
@@ -0,0 +1,268 @@
+#!powershell
+
+# Copyright: (c) 2018, Micah Hunsberger (@mhunsber)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+Set-StrictMode -Version 2
+$ErrorActionPreference = "Stop"
+
+$spec = @{
+ options = @{
+ state = @{ type = "str"; choices = "absent", "present"; default = "present" }
+ aliases = @{ type = "list"; elements = "str" }
+ canonical_name = @{ type = "str" }
+ ip_address = @{ type = "str" }
+ action = @{ type = "str"; choices = "add", "remove", "set"; default = "set" }
+ }
+ required_if = @(, @( "state", "present", @("canonical_name", "ip_address")))
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$state = $module.Params.state
+$aliases = $module.Params.aliases
+$canonical_name = $module.Params.canonical_name
+$ip_address = $module.Params.ip_address
+$action = $module.Params.action
+
+$tmp = [ipaddress]::None
+if ($ip_address -and -not [ipaddress]::TryParse($ip_address, [ref]$tmp)) {
+ $module.FailJson("win_hosts: Argument ip_address needs to be a valid ip address, but was $ip_address")
+}
+$ip_address_type = $tmp.AddressFamily
+
+$hosts_file = Get-Item -LiteralPath "$env:SystemRoot\System32\drivers\etc\hosts"
+
+Function Get-CommentIndex($line) {
+ $c_index = $line.IndexOf('#')
+ if ($c_index -lt 0) {
+ $c_index = $line.Length
+ }
+ return $c_index
+}
+
+Function Get-HostEntryPart($line) {
+ $success = $true
+ $c_index = Get-CommentIndex -line $line
+ $pure_line = $line.Substring(0, $c_index).Trim()
+ $bits = $pure_line -split "\s+"
+ if ($bits.Length -lt 2) {
+ return @{
+ success = $false
+ ip_address = ""
+ ip_type = ""
+ canonical_name = ""
+ aliases = @()
+ }
+ }
+ $ip_obj = [ipaddress]::None
+ if (-not [ipaddress]::TryParse($bits[0], [ref]$ip_obj) ) {
+ $success = $false
+ }
+ $cname = $bits[1]
+ $als = New-Object string[] ($bits.Length - 2)
+ [array]::Copy($bits, 2, $als, 0, $als.Length)
+ return @{
+ success = $success
+ ip_address = $ip_obj.IPAddressToString
+ ip_type = $ip_obj.AddressFamily
+ canonical_name = $cname
+ aliases = $als
+ }
+}
+
+Function Find-HostName($line, $name) {
+ $c_idx = Get-CommentIndex -line $line
+ $re = New-Object regex ("\s+$($name.Replace('.',"\."))(\s|$)", [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
+ $match = $re.Match($line, 0, $c_idx)
+ return $match
+}
+
+Function Remove-HostEntry($list, $idx) {
+ $module.Result.changed = $true
+ $list.RemoveAt($idx)
+}
+
+Function Add-HostEntry($list, $cname, $aliases, $ip) {
+ $module.Result.changed = $true
+ $line = "$ip $cname $($aliases -join ' ')"
+ $list.Add($line) | Out-Null
+}
+
+Function Remove-HostnamesFromEntry($list, $idx, $aliases) {
+ $line = $list[$idx]
+ $line_removed = $false
+
+ foreach ($name in $aliases) {
+ $match = Find-HostName -line $line -name $name
+ if ($match.Success) {
+ $line = $line.Remove($match.Index + 1, $match.Length - 1)
+ # was this the last alias? (check for space characters after trimming)
+ if ($line.Substring(0, (Get-CommentIndex -line $line)).Trim() -inotmatch "\s") {
+ $list.RemoveAt($idx)
+ $line_removed = $true
+ # we're done
+ return @{
+ line_removed = $line_removed
+ }
+ }
+ }
+ }
+ if ($line -ne $list[$idx]) {
+ $module.Result.changed = $true
+ $list[$idx] = $line
+ }
+ return @{
+ line_removed = $line_removed
+ }
+}
+
+Function Add-AliasesToEntry($list, $idx, $aliases) {
+ $line = $list[$idx]
+ foreach ($name in $aliases) {
+ $match = Find-HostName -line $line -name $name
+ if (-not $match.Success) {
+ # just add the alias before the comment
+ $line = $line.Insert((Get-CommentIndex -line $line), " $name ")
+ }
+ }
+ if ($line -ne $list[$idx]) {
+ $module.Result.changed = $true
+ $list[$idx] = $line
+ }
+}
+
+$hosts_lines = New-Object System.Collections.ArrayList
+
+Get-Content -LiteralPath $hosts_file.FullName | ForEach-Object { $hosts_lines.Add($_) } | Out-Null
+$module.Diff.before = ($hosts_lines -join "`n") + "`n"
+
+if ($state -eq 'absent') {
+ # go through and remove canonical_name and ip
+ for ($idx = 0; $idx -lt $hosts_lines.Count; $idx++) {
+ $entry = $hosts_lines[$idx]
+ # skip comment lines
+ if (-not $entry.Trim().StartsWith('#')) {
+ $entry_parts = Get-HostEntryPart -line $entry
+ if ($entry_parts.success) {
+ if (-not $ip_address -or $entry_parts.ip_address -eq $ip_address) {
+ if (-not $canonical_name -or $entry_parts.canonical_name -eq $canonical_name) {
+ if (Remove-HostEntry -list $hosts_lines -idx $idx) {
+ # keep index correct if we removed the line
+ $idx = $idx - 1
+ }
+ }
+ }
+ }
+ }
+ }
+}
+if ($state -eq 'present') {
+ $entry_idx = -1
+ $aliases_to_keep = @()
+ # go through lines, find the entry and determine what to remove based on action
+ for ($idx = 0; $idx -lt $hosts_lines.Count; $idx++) {
+ $entry = $hosts_lines[$idx]
+ # skip comment lines
+ if (-not $entry.Trim().StartsWith('#')) {
+ $entry_parts = Get-HostEntryPart -line $entry
+ if ($entry_parts.success) {
+ $aliases_to_remove = @()
+ if ($entry_parts.ip_address -eq $ip_address) {
+ if ($entry_parts.canonical_name -eq $canonical_name) {
+ $entry_idx = $idx
+
+ if ($action -eq 'set') {
+ $aliases_to_remove = $entry_parts.aliases | Where-Object { $aliases -notcontains $_ }
+ }
+ elseif ($action -eq 'remove') {
+ $aliases_to_remove = $aliases
+ }
+ }
+ else {
+ # this is the right ip_address, but not the cname we were looking for.
+ # we need to make sure none of aliases or canonical_name exist for this entry
+ # since the given canonical_name should be an A/AAAA record,
+ # and aliases should be cname records for the canonical_name.
+ $aliases_to_remove = $aliases + $canonical_name
+ }
+ }
+ else {
+ # this is not the ip_address we are looking for
+ if ($ip_address_type -eq $entry_parts.ip_type) {
+ if ($entry_parts.canonical_name -eq $canonical_name) {
+ Remove-HostEntry -list $hosts_lines -idx $idx
+ $idx = $idx - 1
+ if ($action -ne "set") {
+ # keep old aliases intact
+ $aliases_to_keep += $entry_parts.aliases | Where-Object { ($aliases + $aliases_to_keep + $canonical_name) -notcontains $_ }
+ }
+ }
+ elseif ($action -eq "remove") {
+ $aliases_to_remove = $canonical_name
+ }
+ elseif ($aliases -contains $entry_parts.canonical_name) {
+ Remove-HostEntry -list $hosts_lines -idx $idx
+ $idx = $idx - 1
+ if ($action -eq "add") {
+ # keep old aliases intact
+ $aliases_to_keep += $entry_parts.aliases | Where-Object { ($aliases + $aliases_to_keep + $canonical_name) -notcontains $_ }
+ }
+ }
+ else {
+ $aliases_to_remove = $aliases + $canonical_name
+ }
+ }
+ else {
+ # TODO: Better ipv6 support. There is odd behavior for when an alias can be used for both ipv6 and ipv4
+ }
+ }
+
+ if ($aliases_to_remove) {
+ if ((Remove-HostnamesFromEntry -list $hosts_lines -idx $idx -aliases $aliases_to_remove).line_removed) {
+ $idx = $idx - 1
+ }
+ }
+ }
+ }
+ }
+
+ if ($entry_idx -ge 0) {
+ $aliases_to_add = @()
+ $entry_parts = Get-HostEntryPart -line $hosts_lines[$entry_idx]
+ if ($action -eq 'remove') {
+ $aliases_to_add = $aliases_to_keep | Where-Object { $entry_parts.aliases -notcontains $_ }
+ }
+ else {
+ $aliases_to_add = ($aliases + $aliases_to_keep) | Where-Object { $entry_parts.aliases -notcontains $_ }
+ }
+
+ if ($aliases_to_add) {
+ Add-AliasesToEntry -list $hosts_lines -idx $entry_idx -aliases $aliases_to_add
+ }
+ }
+ else {
+ # add the entry at the end
+ if ($action -eq 'remove') {
+ if ($aliases_to_keep) {
+ Add-HostEntry -list $hosts_lines -ip $ip_address -cname $canonical_name -aliases $aliases_to_keep
+ }
+ else {
+ Add-HostEntry -list $hosts_lines -ip $ip_address -cname $canonical_name
+ }
+ }
+ else {
+ Add-HostEntry -list $hosts_lines -ip $ip_address -cname $canonical_name -aliases ($aliases + $aliases_to_keep)
+ }
+ }
+}
+
+$module.Diff.after = ($hosts_lines -join "`n") + "`n"
+if ( $module.Result.changed -and -not $module.CheckMode ) {
+ Set-Content -LiteralPath $hosts_file.FullName -Value $hosts_lines
+}
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_hosts.py b/ansible_collections/community/windows/plugins/modules/win_hosts.py
new file mode 100644
index 00000000..ba583a56
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_hosts.py
@@ -0,0 +1,119 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Micah Hunsberger (@mhunsber)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_hosts
+short_description: Manages hosts file entries on Windows.
+description:
+ - Manages hosts file entries on Windows.
+ - Maps IPv4 or IPv6 addresses to canonical names.
+ - Adds, removes, or sets cname records for ip and hostname pairs.
+ - Modifies %windir%\\system32\\drivers\\etc\\hosts.
+options:
+ state:
+ description:
+ - Whether the entry should be present or absent.
+ - If only I(canonical_name) is provided when C(state=absent), then
+ all hosts entries with the canonical name of I(canonical_name)
+ will be removed.
+ - If only I(ip_address) is provided when C(state=absent), then all
+ hosts entries with the ip address of I(ip_address) will be removed.
+ - If I(ip_address) and I(canonical_name) are both omitted when
+ C(state=absent), then all hosts entries will be removed.
+ choices:
+ - absent
+ - present
+ default: present
+ type: str
+ canonical_name:
+ description:
+ - A canonical name for the host entry.
+ - required for C(state=present).
+ type: str
+ ip_address:
+ description:
+ - The ip address for the host entry.
+ - Can be either IPv4 (A record) or IPv6 (AAAA record).
+ - Required for C(state=present).
+ type: str
+ aliases:
+ description:
+ - A list of additional names (cname records) for the host entry.
+ - Only applicable when C(state=present).
+ type: list
+ elements: str
+ action:
+ choices:
+ - add
+ - remove
+ - set
+ description:
+ - Controls the behavior of I(aliases).
+ - Only applicable when C(state=present).
+ - If C(add), each alias in I(aliases) will be added to the host entry.
+ - If C(set), each alias in I(aliases) will be added to the host entry,
+ and other aliases will be removed from the entry.
+ default: set
+ type: str
+author:
+ - Micah Hunsberger (@mhunsber)
+notes:
+ - Each canonical name can only be mapped to one IPv4 and one IPv6 address.
+ If I(canonical_name) is provided with C(state=present) and is found
+ to be mapped to another IP address that is the same type as, but unique
+ from I(ip_address), then I(canonical_name) and all I(aliases) will
+ be removed from the entry and added to an entry with the provided IP address.
+ - Each alias can only be mapped to one canonical name. If I(aliases) is provided
+ with C(state=present) and an alias is found to be mapped to another canonical
+ name, then the alias will be removed from the entry and either added to or removed
+ from (depending on I(action)) an entry with the provided canonical name.
+seealso:
+ - module: ansible.windows.win_template
+ - module: ansible.windows.win_file
+ - module: ansible.windows.win_copy
+'''
+
+EXAMPLES = r'''
+- name: Add 127.0.0.1 as an A record for localhost
+ community.windows.win_hosts:
+ state: present
+ canonical_name: localhost
+ ip_address: 127.0.0.1
+
+- name: Add ::1 as an AAAA record for localhost
+ community.windows.win_hosts:
+ state: present
+ canonical_name: localhost
+ ip_address: '::1'
+
+- name: Remove 'bar' and 'zed' from the list of aliases for foo (192.168.1.100)
+ community.windows.win_hosts:
+ state: present
+ canonical_name: foo
+ ip_address: 192.168.1.100
+ action: remove
+ aliases:
+ - bar
+ - zed
+
+- name: Remove hosts entries with canonical name 'bar'
+ community.windows.win_hosts:
+ state: absent
+ canonical_name: bar
+
+- name: Remove 10.2.0.1 from the list of hosts
+ community.windows.win_hosts:
+ state: absent
+ ip_address: 10.2.0.1
+
+- name: Ensure all name resolution is handled by DNS
+ community.windows.win_hosts:
+ state: absent
+'''
+
+RETURN = r'''
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_hotfix.ps1 b/ansible_collections/community/windows/plugins/modules/win_hotfix.ps1
new file mode 100644
index 00000000..2ef663d2
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_hotfix.ps1
@@ -0,0 +1,270 @@
+#!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" -type "bool" -default $false
+
+$hotfix_kb = Get-AnsibleParam -obj $params -name "hotfix_kb" -type "str"
+$hotfix_identifier = Get-AnsibleParam -obj $params -name "hotfix_identifier" -type "str"
+$state = Get-AnsibleParam -obj $params -name "state" -type "state" -default "present" -validateset "absent", "present"
+$source = Get-AnsibleParam -obj $params -name "source" -type "path"
+
+$result = @{
+ changed = $false
+ reboot_required = $false
+}
+
+if (Get-Module -Name DISM -ListAvailable) {
+ Import-Module -Name DISM
+}
+else {
+ # Server 2008 R2 doesn't have the DISM module installed on the path, check the Windows ADK path
+ $adk_root = [System.Environment]::ExpandEnvironmentVariables("%PROGRAMFILES(X86)%\Windows Kits\*\Assessment and Deployment Kit\Deployment Tools\amd64\DISM")
+ if (Test-Path -LiteralPath $adk_root) {
+ Import-Module -Name (Get-Item -LiteralPath $adk_root).FullName
+ }
+ else {
+ Fail-Json $result "The DISM PS module needs to be installed, this can be done through the windows-adk chocolately package"
+ }
+}
+
+
+Function Expand-MSU($msu) {
+ $temp_path = [IO.Path]::GetTempPath()
+ $temp_foldername = [Guid]::NewGuid()
+ $output_path = Join-Path -Path $temp_path -ChildPath $temp_foldername
+ New-Item -Path $output_path -ItemType Directory | Out-Null
+
+ $expand_args = @($msu, $output_path, "-F:*")
+
+ try {
+ &expand.exe $expand_args | Out-NUll
+ }
+ catch {
+ Fail-Json $result "failed to run expand.exe $($expand_args): $($_.Exception.Message)"
+ }
+ if ($LASTEXITCODE -ne 0) {
+ Fail-Json $result "failed to run expand.exe $($expand_args): RC = $LASTEXITCODE"
+ }
+
+ return $output_path
+}
+
+Function Get-HotfixMetadataFromName($name) {
+ try {
+ $dism_package_info = Get-WindowsPackage -Online -PackageName $name
+ }
+ catch {
+ # build a basic stub for a missing result
+ $dism_package_info = @{
+ PackageState = "NotPresent"
+ Description = ""
+ PackageName = $name
+ }
+ }
+
+ if ($dism_package_info.Description -match "(KB\d*)") {
+ $hotfix_kb = $Matches[0]
+ }
+ else {
+ $hotfix_kb = "UNKNOWN"
+ }
+
+ $metadata = @{
+ name = $dism_package_info.PackageName
+ state = $dism_package_info.PackageState
+ kb = $hotfix_kb
+ }
+
+ return $metadata
+}
+
+Function Get-HotfixMetadataFromFile($extract_path) {
+ # MSU contents https://support.microsoft.com/en-us/help/934307/description-of-the-windows-update-standalone-installer-in-windows
+ $metadata_path = Get-ChildItem -LiteralPath $extract_path | Where-Object { $_.Extension -eq ".xml" }
+ if ($null -eq $metadata_path) {
+ Fail-Json $result "failed to get metadata xml inside MSU file, cannot get hotfix metadata required for this task"
+ }
+ [xml]$xml = Get-Content -LiteralPath $metadata_path.FullName
+
+ $xml.unattend.servicing.package.source.location | ForEach-Object {
+ $cab_source_filename = Split-Path -Path $_ -Leaf
+ $cab_file = Join-Path -Path $extract_path -ChildPath $cab_source_filename
+
+ try {
+ $dism_package_info = Get-WindowsPackage -Online -PackagePath $cab_file
+ }
+ catch {
+ Fail-Json $result "failed to get DISM package metadata from path $($extract_path): $($_.Exception.Message)"
+ }
+ if ($dism_package_info.Applicable -eq $false) {
+ Fail-Json $result "hotfix package is not applicable for this server"
+ }
+
+ $package_properties_path = Get-ChildItem -LiteralPath $extract_path | Where-Object { $_.Extension -eq ".txt" }
+ if ($null -eq $package_properties_path) {
+ $hotfix_kb = "UNKNOWN"
+ }
+ else {
+ $package_ini = Get-Content -LiteralPath $package_properties_path.FullName
+ $entry = $package_ini | Where-Object { $_.StartsWith("KB Article Number") }
+ if ($null -eq $entry) {
+ $hotfix_kb = "UNKNOWN"
+ }
+ else {
+ $hotfix_kb = ($entry -split '=')[-1]
+ $hotfix_kb = "KB$($hotfix_kb.Substring(1, $hotfix_kb.Length - 2))"
+ }
+ }
+
+ [pscustomobject]@{
+ path = $cab_file
+ name = $dism_package_info.PackageName
+ state = $dism_package_info.PackageState
+ kb = $hotfix_kb
+ }
+ }
+}
+
+Function Get-HotfixMetadataFromKB($kb) {
+ # I really hate doing it this way
+ $packages = Get-WindowsPackage -Online
+ $identifier = $packages | Where-Object { $_.PackageName -like "*$kb*" }
+
+ if ($null -eq $identifier) {
+ # still haven't found the KB, need to loop through the results and check the description
+ foreach ($package in $packages) {
+ $raw_metadata = Get-HotfixMetadataFromName -name $package.PackageName
+ if ($raw_metadata.kb -eq $kb) {
+ $identifier = $raw_metadata
+ break
+ }
+ }
+
+ # if we still haven't found the package then we need to throw an error
+ if ($null -eq $metadata) {
+ Fail-Json $result "failed to get DISM package from KB, to continue specify hotfix_identifier instead"
+ }
+ }
+ else {
+ $metadata = Get-HotfixMetadataFromName -name $identifier.PackageName
+ }
+
+ return $metadata
+}
+
+if ($state -eq "absent") {
+ # uninstall hotfix
+ # this is a pretty poor way of doing this, is there a better way?
+
+ if ($null -ne $hotfix_identifier) {
+ $hotfix_metadata = Get-HotfixMetadataFromName -name $hotfix_identifier
+ }
+ elseif ($null -ne $hotfix_kb) {
+ $hotfix_install_info = Get-Hotfix -Id $hotfix_kb -ErrorAction SilentlyContinue
+ if ($null -ne $hotfix_install_info) {
+ $hotfix_metadata = Get-HotfixMetadataFromKB -kb $hotfix_kb
+ }
+ else {
+ $hotfix_metadata = @{state = "NotPresent" }
+ }
+ }
+ else {
+ Fail-Json $result "either hotfix_identifier or hotfix_kb needs to be set when state=absent"
+ }
+
+ # how do we want to deal with the other states?
+ if ($hotfix_metadata.state -eq "UninstallPending") {
+ $result.identifier = $hotfix_metadata.name
+ $result.kb = $hotfix_metadata.kb
+ $result.reboot_required = $true
+ }
+ elseif ($hotfix_metadata.state -eq "Installed") {
+ $result.identifier = $hotfix_metadata.name
+ $result.kb = $hotfix_metadata.kb
+
+ if (-not $check_mode) {
+ try {
+ $remove_result = Remove-WindowsPackage -Online -PackageName $hotfix_metadata.name -NoRestart
+ }
+ catch {
+ Fail-Json $result "failed to remove package $($hotfix_metadata.name): $($_.Exception.Message)"
+ }
+ $result.reboot_required = $remove_Result.RestartNeeded
+ }
+
+ $result.changed = $true
+ }
+}
+else {
+ if ($null -eq $source) {
+ Fail-Json $result "source must be set when state=present"
+ }
+ if (-not (Test-Path -LiteralPath $source -PathType Leaf)) {
+ Fail-Json $result "the path set for source $source does not exist or is not a file"
+ }
+
+ # while we do extract the file in check mode we need to do so for valid checking
+ $extract_path = Expand-MSU -msu $source
+ try {
+ $hotfix_metadata = Get-HotfixMetadataFromFile -extract_path $extract_path
+
+ # validate the hotfix matches if the hotfix id has been passed in
+ if ($null -ne $hotfix_identifier) {
+ if ($hotfix_metadata.name -ne $hotfix_identifier) {
+ $msg = -join @(
+ "the hotfix identifier $hotfix_identifier does not match with the source msu identifier $($hotfix_metadata.name), "
+ "please omit or specify the correct identifier to continue"
+ )
+ Fail-Json $result $msg
+ }
+ }
+ if ($null -ne $hotfix_kb) {
+ if ($hotfix_metadata.kb -ne $hotfix_kb) {
+ $msg = -join @(
+ "the hotfix KB $hotfix_kb does not match with the source msu KB $($hotfix_metadata.kb), "
+ "please omit or specify the correct KB to continue"
+ )
+ Fail-Json $result $msg
+ }
+ }
+
+ $result.identifiers = @($hotfix_metadata.name)
+ $result.identifier = $result.identifiers[0]
+ $result.kbs = @($hotfix_metadata.kb)
+ $result.kb = $result.kbs[0]
+
+ # how do we want to deal with other states
+ if ($hotfix_metadata.state -eq "InstallPending") {
+ # return the reboot required flag, should we fail here instead
+ $result.reboot_required = $true
+ }
+ elseif ($hotfix_metadata.state -ne "Installed") {
+ if (-not $check_mode) {
+ try {
+ $install_result = @(
+ Foreach ($path in $hotfix_metadata.path) {
+ Add-WindowsPackage -Online -PackagePath $path -NoRestart
+ }
+ )
+ }
+ catch {
+ Fail-Json $result "failed to add windows package from path $($hotfix_metadata.path): $($_.Exception.Message)"
+ }
+ $result.reboot_required = [bool]($install_result.RestartNeeded -eq $true)
+ }
+ $result.changed = $true
+ }
+ }
+ finally {
+ Remove-Item -LiteralPath $extract_path -Force -Recurse
+ }
+}
+
+Exit-Json $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_hotfix.py b/ansible_collections/community/windows/plugins/modules/win_hotfix.py
new file mode 100644
index 00000000..41d03fb9
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_hotfix.py
@@ -0,0 +1,150 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_hotfix
+short_description: Install and uninstalls Windows hotfixes
+description:
+- Install, uninstall a Windows hotfix.
+options:
+ hotfix_identifier:
+ description:
+ - The name of the hotfix as shown in DISM, see examples for details.
+ - This or C(hotfix_kb) MUST be set when C(state=absent).
+ - If C(state=present) then the hotfix at C(source) will be validated
+ against this value, if it does not match an error will occur.
+ - You can get the identifier by running
+ 'Get-WindowsPackage -Online -PackagePath path-to-cab-in-msu' after
+ expanding the msu file.
+ type: str
+ hotfix_kb:
+ description:
+ - The name of the KB the hotfix relates to, see examples for details.
+ - This or C(hotfix_identifier) MUST be set when C(state=absent).
+ - If C(state=present) then the hotfix at C(source) will be validated
+ against this value, if it does not match an error will occur.
+ - Because DISM uses the identifier as a key and doesn't refer to a KB in
+ all cases it is recommended to use C(hotfix_identifier) instead.
+ type: str
+ state:
+ description:
+ - Whether to install or uninstall the hotfix.
+ - When C(present), C(source) MUST be set.
+ - When C(absent), C(hotfix_identifier) or C(hotfix_kb) MUST be set.
+ type: str
+ default: present
+ choices: [ absent, present ]
+ source:
+ description:
+ - The path to the downloaded hotfix .msu file.
+ - This MUST be set if C(state=present) and MUST be a .msu hotfix file.
+ type: path
+notes:
+- This must be run on a host that has the DISM powershell module installed and
+ a Powershell version >= 4.
+- This module is installed by default on Windows 8 and Server 2012 and newer.
+- You can manually install this module on Windows 7 and Server 2008 R2 by
+ installing the Windows ADK
+ U(https://developer.microsoft.com/en-us/windows/hardware/windows-assessment-deployment-kit),
+ see examples to see how to do it with chocolatey.
+- You can download hotfixes from U(https://www.catalog.update.microsoft.com/Home.aspx).
+seealso:
+- module: ansible.windows.win_package
+- module: ansible.windows.win_updates
+author:
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = r'''
+- name: Install Windows ADK with DISM for Server 2008 R2
+ chocolatey.chocolatey.win_chocolatey:
+ name: windows-adk
+ version: 8.100.26866.0
+ state: present
+ install_args: /features OptionId.DeploymentTools
+
+- name: Install hotfix without validating the KB and Identifier
+ community.windows.win_hotfix:
+ source: C:\temp\windows8.1-kb3172729-x64_e8003822a7ef4705cbb65623b72fd3cec73fe222.msu
+ state: present
+ register: hotfix_install
+
+- ansible.windows.win_reboot:
+ when: hotfix_install.reboot_required
+
+- name: Install hotfix validating KB
+ community.windows.win_hotfix:
+ hotfix_kb: KB3172729
+ source: C:\temp\windows8.1-kb3172729-x64_e8003822a7ef4705cbb65623b72fd3cec73fe222.msu
+ state: present
+ register: hotfix_install
+
+- ansible.windows.win_reboot:
+ when: hotfix_install.reboot_required
+
+- name: Install hotfix validating Identifier
+ community.windows.win_hotfix:
+ hotfix_identifier: Package_for_KB3172729~31bf3856ad364e35~amd64~~6.3.1.0
+ source: C:\temp\windows8.1-kb3172729-x64_e8003822a7ef4705cbb65623b72fd3cec73fe222.msu
+ state: present
+ register: hotfix_install
+
+- ansible.windows.win_reboot:
+ when: hotfix_install.reboot_required
+
+- name: Uninstall hotfix with Identifier
+ community.windows.win_hotfix:
+ hotfix_identifier: Package_for_KB3172729~31bf3856ad364e35~amd64~~6.3.1.0
+ state: absent
+ register: hotfix_uninstall
+
+- ansible.windows.win_reboot:
+ when: hotfix_uninstall.reboot_required
+
+- name: Uninstall hotfix with KB (not recommended)
+ community.windows.win_hotfix:
+ hotfix_kb: KB3172729
+ state: absent
+ register: hotfix_uninstall
+
+- ansible.windows.win_reboot:
+ when: hotfix_uninstall.reboot_required
+'''
+
+RETURN = r'''
+identifier:
+ description: The DISM identifier for the hotfix.
+ returned: success
+ type: str
+ sample: Package_for_KB3172729~31bf3856ad364e35~amd64~~6.3.1.0
+identifiers:
+ description: The DISM identifiers for each hotfix in the msu.
+ returned: success
+ type: list
+ elements: str
+ sample:
+ - Package_for_KB3172729~31bf3856ad364e35~amd64~~6.3.1.0
+ version_added: '1.10.0'
+kb:
+ description: The KB the hotfix relates to.
+ returned: success
+ type: str
+ sample: KB3172729
+kbs:
+ description: The KB for each hotfix in the msu,
+ returned: success
+ type: list
+ elements: str
+ sample:
+ - KB3172729
+ version_added: '1.10.0'
+reboot_required:
+ description: Whether a reboot is required for the install or uninstall to
+ finalise.
+ returned: success
+ type: str
+ sample: true
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_http_proxy.ps1 b/ansible_collections/community/windows/plugins/modules/win_http_proxy.ps1
new file mode 100644
index 00000000..3b564cae
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_http_proxy.ps1
@@ -0,0 +1,268 @@
+#!powershell
+
+# Copyright: (c) 2019, 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.AddType
+
+$spec = @{
+ options = @{
+ bypass = @{ type = "list"; elements = "str"; no_log = $false }
+ proxy = @{ type = "raw" }
+ source = @{ type = "str"; choices = @("ie") }
+ }
+ mutually_exclusive = @(
+ @("proxy", "source"),
+ @("bypass", "source")
+ )
+ required_by = @{
+ bypass = @("proxy")
+ }
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$proxy = $module.Params.proxy
+$bypass = $module.Params.bypass
+$source = $module.Params.source
+
+# Parse the raw value, it should be a Dictionary or String
+if ($proxy -is [System.Collections.IDictionary]) {
+ $valid_keys = [System.Collections.Generic.List`1[String]]@("http", "https", "ftp", "socks")
+ # Check to make sure we don't have any invalid keys in the dict
+ $invalid_keys = [System.Collections.Generic.List`1[String]]@()
+ foreach ($k in $proxy.Keys) {
+ if ($k -notin $valid_keys) {
+ $invalid_keys.Add($k)
+ }
+ }
+
+ if ($invalid_keys.Count -gt 0) {
+ $invalid_keys = $invalid_keys | Sort-Object # So our test assertion doesn't fail due to random ordering
+ $module.FailJson("Invalid keys found in proxy: $($invalid_keys -join ', '). Valid keys are $($valid_keys -join ', ').")
+ }
+
+ # Build the proxy string in the form 'protocol=host;', the order of valid_keys is also important
+ $proxy_list = [System.Collections.Generic.List`1[String]]@()
+ foreach ($k in $valid_keys) {
+ if ($proxy.ContainsKey($k)) {
+ $proxy_list.Add("$k=$($proxy.$k)")
+ }
+ }
+ $proxy = $proxy_list -join ";"
+}
+elseif ($null -ne $proxy) {
+ $proxy = $proxy.ToString()
+}
+
+if ($bypass) {
+ if ([System.String]::IsNullOrEmpty($proxy)) {
+ $module.FailJson("missing parameter(s) required by ''bypass'': proxy")
+ }
+ $bypass = $bypass -join ';'
+}
+
+$win_http_invoke = @'
+using System;
+using System.Runtime.InteropServices;
+
+namespace Ansible.WinHttpProxy
+{
+ internal class NativeHelpers
+ {
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ public class WINHTTP_CURRENT_USER_IE_PROXY_CONFIG : IDisposable
+ {
+ public bool fAutoDetect;
+ public IntPtr lpszAutoConfigUrl;
+ public IntPtr lpszProxy;
+ public IntPtr lpszProxyBypass;
+
+ public void Dispose()
+ {
+ if (lpszAutoConfigUrl != IntPtr.Zero)
+ Marshal.FreeHGlobal(lpszAutoConfigUrl);
+ if (lpszProxy != IntPtr.Zero)
+ Marshal.FreeHGlobal(lpszProxy);
+ if (lpszProxyBypass != IntPtr.Zero)
+ Marshal.FreeHGlobal(lpszProxyBypass);
+ GC.SuppressFinalize(this);
+ }
+ ~WINHTTP_CURRENT_USER_IE_PROXY_CONFIG() { this.Dispose(); }
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ public class WINHTTP_PROXY_INFO : IDisposable
+ {
+ public UInt32 dwAccessType;
+ public IntPtr lpszProxy;
+ public IntPtr lpszProxyBypass;
+
+ public void Dispose()
+ {
+ if (lpszProxy != IntPtr.Zero)
+ Marshal.FreeHGlobal(lpszProxy);
+ if (lpszProxyBypass != IntPtr.Zero)
+ Marshal.FreeHGlobal(lpszProxyBypass);
+ GC.SuppressFinalize(this);
+ }
+ ~WINHTTP_PROXY_INFO() { this.Dispose(); }
+ }
+ }
+
+ internal class NativeMethods
+ {
+ [DllImport("Winhttp.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ public static extern bool WinHttpGetDefaultProxyConfiguration(
+ [Out] NativeHelpers.WINHTTP_PROXY_INFO pProxyInfo);
+
+ [DllImport("Winhttp.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ public static extern bool WinHttpGetIEProxyConfigForCurrentUser(
+ [Out] NativeHelpers.WINHTTP_CURRENT_USER_IE_PROXY_CONFIG pProxyConfig);
+
+ [DllImport("Winhttp.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ public static extern bool WinHttpSetDefaultProxyConfiguration(
+ NativeHelpers.WINHTTP_PROXY_INFO pProxyInfo);
+ }
+
+ 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 WinINetProxy
+ {
+ public bool AutoDetect;
+ public string AutoConfigUrl;
+ public string Proxy;
+ public string ProxyBypass;
+ }
+
+ public class WinHttpProxy
+ {
+ public string Proxy;
+ public string ProxyBypass;
+
+ public WinHttpProxy()
+ {
+ Refresh();
+ }
+
+ public void Set()
+ {
+ using (NativeHelpers.WINHTTP_PROXY_INFO proxyInfo = new NativeHelpers.WINHTTP_PROXY_INFO())
+ {
+ if (String.IsNullOrEmpty(Proxy))
+ proxyInfo.dwAccessType = 1; // WINHTTP_ACCESS_TYPE_NO_PROXY
+ else
+ {
+ proxyInfo.dwAccessType = 3; // WINHTTP_ACCESS_TYPE_NAMED_PROXY
+ proxyInfo.lpszProxy = Marshal.StringToHGlobalUni(Proxy);
+
+ if (!String.IsNullOrEmpty(ProxyBypass))
+ proxyInfo.lpszProxyBypass = Marshal.StringToHGlobalUni(ProxyBypass);
+ }
+
+ if (!NativeMethods.WinHttpSetDefaultProxyConfiguration(proxyInfo))
+ throw new Win32Exception("WinHttpSetDefaultProxyConfiguration() failed");
+ }
+ }
+
+ public void Refresh()
+ {
+ using (NativeHelpers.WINHTTP_PROXY_INFO proxyInfo = new NativeHelpers.WINHTTP_PROXY_INFO())
+ {
+ if (!NativeMethods.WinHttpGetDefaultProxyConfiguration(proxyInfo))
+ throw new Win32Exception("WinHttpGetDefaultProxyConfiguration() failed");
+
+ Proxy = Marshal.PtrToStringUni(proxyInfo.lpszProxy);
+ ProxyBypass = Marshal.PtrToStringUni(proxyInfo.lpszProxyBypass);
+ }
+ }
+
+ public static WinINetProxy GetIEProxyConfig()
+ {
+ using (NativeHelpers.WINHTTP_CURRENT_USER_IE_PROXY_CONFIG ieProxy = new NativeHelpers.WINHTTP_CURRENT_USER_IE_PROXY_CONFIG())
+ {
+ if (!NativeMethods.WinHttpGetIEProxyConfigForCurrentUser(ieProxy))
+ throw new Win32Exception("WinHttpGetIEProxyConfigForCurrentUser() failed");
+
+ return new WinINetProxy
+ {
+ AutoDetect = ieProxy.fAutoDetect,
+ AutoConfigUrl = Marshal.PtrToStringUni(ieProxy.lpszAutoConfigUrl),
+ Proxy = Marshal.PtrToStringUni(ieProxy.lpszProxy),
+ ProxyBypass = Marshal.PtrToStringUni(ieProxy.lpszProxyBypass),
+ };
+ }
+ }
+ }
+}
+'@
+Add-CSharpType -References $win_http_invoke -AnsibleModule $module
+
+$actual_proxy = New-Object -TypeName Ansible.WinHttpProxy.WinHttpProxy
+
+$module.Diff.before = @{
+ proxy = $actual_proxy.Proxy
+ bypass = $actual_proxy.ProxyBypass
+}
+
+if ($source -eq "ie") {
+ # If source=ie we need to get the server and bypass values from the IE configuration
+ $ie_proxy = [Ansible.WinHttpProxy.WinHttpProxy]::GetIEProxyConfig()
+ $proxy = $ie_proxy.Proxy
+ $bypass = $ie_proxy.ProxyBypass
+}
+
+$previous_proxy = $actual_proxy.Proxy
+$previous_bypass = $actual_proxy.ProxyBypass
+
+# Make sure an empty string is converted to $null for easier comparisons
+if ([String]::IsNullOrEmpty($proxy)) {
+ $proxy = $null
+}
+if ([String]::IsNullOrEmpty($bypass)) {
+ $bypass = $null
+}
+
+if ($previous_proxy -ne $proxy -or $previous_bypass -ne $bypass) {
+ $actual_proxy.Proxy = $proxy
+ $actual_proxy.ProxyBypass = $bypass
+
+ if (-not $module.CheckMode) {
+ $actual_proxy.Set()
+
+ # Validate that the change was made correctly and revert if it wasn't. The Set() method won't fail on invalid
+ # values so we need to check again to make sure all was good.
+ $actual_proxy.Refresh()
+ if ($actual_proxy.Proxy -ne $proxy -or $actual_proxy.ProxyBypass -ne $bypass) {
+ $actual_proxy.Proxy = $previous_proxy
+ $actual_proxy.ProxyBypass = $previous_bypass
+ $actual_proxy.Set()
+
+ $module.FailJson("Unknown error when trying to set proxy '$proxy' or bypass '$bypass'")
+ }
+ }
+
+ $module.Result.changed = $true
+}
+
+$module.Diff.after = @{
+ proxy = $proxy
+ bypass = $bypass
+}
+
+$module.ExitJson()
+
diff --git a/ansible_collections/community/windows/plugins/modules/win_http_proxy.py b/ansible_collections/community/windows/plugins/modules/win_http_proxy.py
new file mode 100644
index 00000000..92f7e5c6
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_http_proxy.py
@@ -0,0 +1,100 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2019, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_http_proxy
+short_description: Manages proxy settings for WinHTTP
+description:
+- Used to set, remove, or import proxy settings for Windows HTTP Services
+ C(WinHTTP).
+- WinHTTP is a framework used by applications or services, typically .NET
+ applications or non-interactive services, to make web requests.
+options:
+ bypass:
+ description:
+ - A list of hosts that will bypass the set proxy when being accessed.
+ - Use C(<local>) to match hostnames that are not fully qualified domain
+ names. This is useful when needing to connect to intranet sites using
+ just the hostname.
+ - Omit, set to null or an empty string/list to remove the bypass list.
+ - If this is set then I(proxy) must also be set.
+ type: list
+ elements: str
+ proxy:
+ description:
+ - A string or dict that specifies the proxy to be set.
+ - If setting a string, should be in the form C(hostname), C(hostname:port),
+ or C(protocol=hostname:port).
+ - If the port is undefined, the default port for the protocol in use is
+ used.
+ - If setting a dict, the keys should be the protocol and the values should
+ be the hostname and/or port for that protocol.
+ - Valid protocols are C(http), C(https), C(ftp), and C(socks).
+ - Omit, set to null or an empty string to remove the proxy settings.
+ type: raw
+ source:
+ description:
+ - Instead of manually specifying the I(proxy) and/or I(bypass), set this to
+ import the proxy from a set source like Internet Explorer.
+ - Using C(ie) will import the Internet Explorer proxy settings for the
+ current active network connection of the current user.
+ - Only IE's proxy URL and bypass list will be imported into WinHTTP.
+ - This is like running C(netsh winhttp import proxy source=ie).
+ - The value is imported when the module runs and will not automatically
+ be updated if the IE configuration changes in the future. The module will
+ have to be run again to sync the latest changes.
+ choices:
+ - ie
+ type: str
+notes:
+- This is not the same as the proxy settings set in Internet Explorer, also
+ known as C(WinINet); use the M(community.windows.win_inet_proxy) module to manage that instead.
+- These settings are set system wide and not per user, it will require
+ Administrative privileges to run.
+seealso:
+- module: community.windows.win_inet_proxy
+author:
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = r'''
+- name: Set a proxy to use for all protocols
+ community.windows.win_http_proxy:
+ proxy: hostname
+
+- name: Set a proxy with a specific port with a bypass list
+ community.windows.win_http_proxy:
+ proxy: hostname:8080
+ bypass:
+ - server1
+ - server2
+ - <local>
+
+- name: Set the proxy based on the IE proxy settings
+ community.windows.win_http_proxy:
+ source: ie
+
+- name: Set a proxy for specific protocols
+ community.windows.win_http_proxy:
+ proxy:
+ http: hostname:8080
+ https: hostname:8443
+
+- name: Set a proxy for specific protocols using a string
+ community.windows.win_http_proxy:
+ proxy: http=hostname:8080;https=hostname:8443
+ bypass: server1,server2,<local>
+
+- name: Remove any proxy settings
+ community.windows.win_http_proxy:
+ proxy: ''
+ bypass: ''
+'''
+
+RETURN = r'''
+#
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_iis_virtualdirectory.ps1 b/ansible_collections/community/windows/plugins/modules/win_iis_virtualdirectory.ps1
new file mode 100644
index 00000000..d2c3ba07
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_iis_virtualdirectory.ps1
@@ -0,0 +1,139 @@
+#!powershell
+
+# Copyright: (c) 2015, Henrik Wallström <henrik@wallstroms.nu>
+# 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
+$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
+$site = Get-AnsibleParam -obj $params -name "site" -type "str" -failifempty $true
+$application = Get-AnsibleParam -obj $params -name "application" -type "str"
+$physical_path = Get-AnsibleParam -obj $params -name "physical_path" -type "str"
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent", "present"
+$connect_as = Get-AnsibleParam -obj $params -name 'connect_as' -type 'str' -validateset 'specific_user', 'pass_through'
+$username = Get-AnsibleParam -obj $params -name "username" -type "str" -failifempty ($connect_as -eq 'specific_user')
+$password = Get-AnsibleParam -obj $params -name "password" -type "str" -failifempty ($connect_as -eq 'specific_user')
+
+# Ensure WebAdministration module is loaded
+if ($null -eq (Get-Module "WebAdministration" -ErrorAction SilentlyContinue)) {
+ Import-Module WebAdministration
+}
+
+# Result
+$result = @{
+ directory = @{}
+ changed = $false
+}
+
+# Construct path
+$directory_path = if ($application) {
+ "IIS:\Sites\$($site)\$($application)\$($name)"
+}
+else {
+ "IIS:\Sites\$($site)\$($name)"
+}
+
+# Directory info
+$directory = if ($application) {
+ Get-WebVirtualDirectory -Site $site -Name $name -Application $application
+}
+else {
+ Get-WebVirtualDirectory -Site $site -Name $name
+}
+
+try {
+ # Add directory
+ If (($state -eq 'present') -and (-not $directory)) {
+ If (-not $physical_path) {
+ Fail-Json -obj $result -message "missing required arguments: physical_path"
+ }
+ If (-not (Test-Path -LiteralPath $physical_path)) {
+ Fail-Json -obj $result -message "specified folder must already exist: physical_path"
+ }
+
+ $directory_parameters = @{
+ Site = $site
+ Name = $name
+ PhysicalPath = $physical_path
+ }
+
+ If ($application) {
+ $directory_parameters.Application = $application
+ }
+
+ $directory = New-WebVirtualDirectory @directory_parameters -Force
+ $result.changed = $true
+ }
+
+ # Remove directory
+ If ($state -eq 'absent' -and $directory) {
+ Remove-Item -LiteralPath $directory_path -Recurse -Force
+ $result.changed = $true
+ }
+
+ $directory = if ($application) {
+ Get-WebVirtualDirectory -Site $site -Name $name -Application $application
+ }
+ else {
+ Get-WebVirtualDirectory -Site $site -Name $name
+ }
+
+ If ($directory) {
+
+ # Change Physical Path if needed
+ if ($physical_path) {
+ If (-not (Test-Path -LiteralPath $physical_path)) {
+ Fail-Json -obj $result -message "specified folder must already exist: physical_path"
+ }
+
+ $vdir_folder = Get-Item -LiteralPath $directory.PhysicalPath
+ $folder = Get-Item -LiteralPath $physical_path
+ If ($folder.FullName -ne $vdir_folder.FullName) {
+ Set-ItemProperty -LiteralPath $directory_path -name physicalPath -value $physical_path
+ $result.changed = $true
+ }
+ }
+
+ # Change username or password if needed
+ if ($connect_as -eq 'pass_through') {
+ if ($directory.username -ne '') {
+ Clear-ItemProperty -LiteralPath $directory_path -Name 'userName'
+ $result.changed = $true
+ }
+ if ($directory.password -ne '') {
+ Clear-ItemProperty -LiteralPath $directory_path -Name 'password'
+ $result.changed = $true
+ }
+ }
+ elseif ($connect_as -eq 'specific_user') {
+ if ($directory.username -ne $username) {
+ Set-ItemProperty -LiteralPath $directory_path -Name 'userName' -Value $username
+ $result.changed = $true
+ }
+ if ($directory.password -ne $password) {
+ Set-ItemProperty -LiteralPath $directory_path -Name 'password' -Value $password
+ $result.changed = $true
+ }
+ }
+ }
+}
+catch {
+ Fail-Json $result $_.Exception.Message
+}
+
+# Result
+$directory = if ($application) {
+ Get-WebVirtualDirectory -Site $site -Name $name -Application $application
+}
+else {
+ Get-WebVirtualDirectory -Site $site -Name $name
+}
+
+$result.directory = @{
+ PhysicalPath = $directory.PhysicalPath
+}
+
+Exit-Json -obj $result \ No newline at end of file
diff --git a/ansible_collections/community/windows/plugins/modules/win_iis_virtualdirectory.py b/ansible_collections/community/windows/plugins/modules/win_iis_virtualdirectory.py
new file mode 100644
index 00000000..5d572f8f
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_iis_virtualdirectory.py
@@ -0,0 +1,90 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2015, Henrik Wallström <henrik@wallstroms.nu>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_iis_virtualdirectory
+short_description: Configures a virtual directory in IIS
+description:
+ - Creates, Removes and configures a virtual directory in IIS.
+options:
+ name:
+ description:
+ - The name of the virtual directory to create or remove.
+ type: str
+ required: yes
+ state:
+ description:
+ - Whether to add or remove the specified virtual directory.
+ - Removing will remove the virtual directory and all under it (Recursively).
+ type: str
+ choices: [ absent, present ]
+ default: present
+ site:
+ description:
+ - The site name under which the virtual directory is created or exists.
+ type: str
+ required: yes
+ application:
+ description:
+ - The application under which the virtual directory is created or exists.
+ type: str
+ physical_path:
+ description:
+ - The physical path to the folder in which the new virtual directory is created.
+ - The specified folder must already exist.
+ type: str
+ connect_as:
+ description:
+ - The type of authentication to use for the virtual directory. Either C(pass_through) or C(specific_user)
+ - If C(pass_through), IIS will use the identity of the user or application pool identity to access the physical path.
+ - If C(specific_user), IIS will use the credentials provided in I(username) and I(password) to access the physical path.
+ type: str
+ choices: [pass_through, specific_user]
+ version_added: 1.9.0
+ username:
+ description:
+ - Specifies the user name of an account that can access configuration files and content for the virtual directory.
+ - Required when I(connect_as) is set to C(specific_user).
+ type: str
+ version_added: 1.9.0
+ password:
+ description:
+ - The password associated with I(username).
+ - Required when I(connect_as) is set to C(specific_user).
+ type: str
+ version_added: 1.9.0
+seealso:
+- module: community.windows.win_iis_webapplication
+- module: community.windows.win_iis_webapppool
+- module: community.windows.win_iis_webbinding
+- module: community.windows.win_iis_website
+author:
+- Henrik Wallström (@henrikwallstrom)
+'''
+
+EXAMPLES = r'''
+- name: Create a virtual directory if it does not exist
+ community.windows.win_iis_virtualdirectory:
+ name: somedirectory
+ site: somesite
+ state: present
+ physical_path: C:\virtualdirectory\some
+
+- name: Remove a virtual directory if it exists
+ community.windows.win_iis_virtualdirectory:
+ name: somedirectory
+ site: somesite
+ state: absent
+
+- name: Create a virtual directory on an application if it does not exist
+ community.windows.win_iis_virtualdirectory:
+ name: somedirectory
+ site: somesite
+ application: someapp
+ state: present
+ physical_path: C:\virtualdirectory\some
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_iis_webapplication.ps1 b/ansible_collections/community/windows/plugins/modules/win_iis_webapplication.ps1
new file mode 100644
index 00000000..86fa850c
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_iis_webapplication.ps1
@@ -0,0 +1,140 @@
+#!powershell
+
+# Copyright: (c) 2015, Henrik Wallström <henrik@wallstroms.nu>
+# 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
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
+
+$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
+$site = Get-AnsibleParam -obj $params -name "site" -type "str" -failifempty $true
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent", "present"
+$physical_path = Get-AnsibleParam -obj $params -name "physical_path" -type "str" -aliases "path"
+$application_pool = Get-AnsibleParam -obj $params -name "application_pool" -type "str"
+$connect_as = Get-AnsibleParam -obj $params -name 'connect_as' -type 'str' -validateset 'specific_user', 'pass_through'
+$username = Get-AnsibleParam -obj $params -name "username" -type "str" -failifempty ($connect_as -eq 'specific_user')
+$password = Get-AnsibleParam -obj $params -name "password" -type "str" -failifempty ($connect_as -eq 'specific_user')
+
+$result = @{
+ application_pool = $application_pool
+ changed = $false
+ physical_path = $physical_path
+}
+
+# Ensure WebAdministration module is loaded
+if ($null -eq (Get-Module "WebAdministration" -ErrorAction SilentlyContinue)) {
+ Import-Module WebAdministration
+}
+
+# Application info
+$application = Get-WebApplication -Site $site -Name $name
+$website = Get-Website -Name $site
+
+# Set ApplicationPool to current if not specified
+if (!$application_pool) {
+ $application_pool = $website.applicationPool
+}
+
+try {
+ # Add application
+ if (($state -eq 'present') -and (-not $application)) {
+ if (-not $physical_path) {
+ Fail-Json $result "missing required arguments: path"
+ }
+ if (-not (Test-Path -LiteralPath $physical_path)) {
+ Fail-Json $result "specified folder must already exist: path"
+ }
+
+ $application_parameters = @{
+ Name = $name
+ PhysicalPath = $physical_path
+ Site = $site
+ }
+
+ if ($application_pool) {
+ $application_parameters.ApplicationPool = $application_pool
+ }
+
+ if (-not $check_mode) {
+ $application = New-WebApplication @application_parameters -Force
+ }
+ $result.changed = $true
+ }
+
+ # Remove application
+ if ($state -eq 'absent' -and $application) {
+ $application = Remove-WebApplication -Site $site -Name $name -WhatIf:$check_mode
+ $result.changed = $true
+ }
+
+ $application = Get-WebApplication -Site $site -Name $name
+ if ($application) {
+
+ # Change Physical Path if needed
+ if ($physical_path) {
+ if (-not (Test-Path -LiteralPath $physical_path)) {
+ Fail-Json $result "specified folder must already exist: path"
+ }
+
+ $folder = Get-Item -LiteralPath $physical_path
+ if ($folder.FullName -ne $application.PhysicalPath) {
+ Set-ItemProperty -LiteralPath "IIS:\Sites\$($site)\$($name)" -name physicalPath -value $physical_path -WhatIf:$check_mode
+ $result.changed = $true
+ }
+ }
+
+ # Change Application Pool if needed
+ if ($application_pool) {
+ if ($application_pool -ne $application.applicationPool) {
+ Set-ItemProperty -LiteralPath "IIS:\Sites\$($site)\$($name)" -name applicationPool -value $application_pool -WhatIf:$check_mode
+ $result.changed = $true
+ }
+ }
+
+ # Change username and password if needed
+ $app_user = Get-ItemProperty -LiteralPath "IIS:\Sites\$($site)\$($name)" -Name 'userName'
+ $app_pass = Get-ItemProperty -LiteralPath "IIS:\Sites\$($site)\$($name)" -Name 'password'
+ if ($connect_as -eq 'pass_through') {
+ if ($app_user -ne '') {
+ Clear-ItemProperty -LiteralPath "IIS:\Sites\$($site)\$($name)" -Name 'userName' -WhatIf:$check_mode
+ $result.changed = $true
+ }
+ if ($app_pass -ne '') {
+ Clear-ItemProperty -LiteralPath "IIS:\Sites\$($site)\$($name)" -Name 'password' -WhatIf:$check_mode
+ $result.changed = $true
+ }
+ }
+ elseif ($connect_as -eq 'specific_user') {
+ if ($app_user -ne $username) {
+ Set-ItemProperty -LiteralPath "IIS:\Sites\$($site)\$($name)" -Name 'userName' -Value $username -WhatIf:$check_mode
+ $result.changed = $true
+ }
+ if ($app_pass -ne $password) {
+ Set-ItemProperty -LiteralPath "IIS:\Sites\$($site)\$($name)" -Name 'password' -Value $password -WhatIf:$check_mode
+ $result.changed = $true
+ }
+ }
+ }
+}
+catch {
+ Fail-Json $result $_.Exception.Message
+}
+
+# When in check-mode or on removal, this may fail
+$application = Get-WebApplication -Site $site -Name $name
+if ($application) {
+ $app_user = Get-ItemProperty -LiteralPath "IIS:\Sites\$($site)\$($name)" -Name 'userName'
+ if ($app_user -eq '') {
+ $result.connect_as = 'pass_through'
+ }
+ else {
+ $result.connect_as = 'specific_user'
+ }
+
+ $result.physical_path = $application.PhysicalPath
+ $result.application_pool = $application.ApplicationPool
+}
+
+Exit-Json $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_iis_webapplication.py b/ansible_collections/community/windows/plugins/modules/win_iis_webapplication.py
new file mode 100644
index 00000000..50149da7
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_iis_webapplication.py
@@ -0,0 +1,91 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2015, Henrik Wallström <henrik@wallstroms.nu>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_iis_webapplication
+short_description: Configures IIS web applications
+description:
+- Creates, removes, and configures IIS web applications.
+options:
+ name:
+ description:
+ - Name of the web application.
+ type: str
+ required: yes
+ site:
+ description:
+ - Name of the site on which the application is created.
+ type: str
+ required: yes
+ state:
+ description:
+ - State of the web application.
+ type: str
+ choices: [ absent, present ]
+ default: present
+ physical_path:
+ description:
+ - The physical path on the remote host to use for the new application.
+ - The specified folder must already exist.
+ type: str
+ application_pool:
+ description:
+ - The application pool in which the new site executes.
+ - If not specified, the application pool of the current website will be used.
+ type: str
+ connect_as:
+ description:
+ - The type of authentication to use for this application. Either C(pass_through) or C(specific_user)
+ - If C(pass_through), IIS will use the identity of the user or application pool identity to access the file system or network.
+ - If C(specific_user), IIS will use the credentials provided in I(username) and I(password) to access the file system or network.
+ type: str
+ choices: [pass_through, specific_user]
+ username:
+ description:
+ - Specifies the user name of an account that can access configuration files and content for this application.
+ - Required when I(connect_as) is set to C(specific_user).
+ type: str
+ password:
+ description:
+ - The password associated with I(username).
+ - Required when I(connect_as) is set to C(specific_user).
+ type: str
+seealso:
+- module: community.windows.win_iis_virtualdirectory
+- module: community.windows.win_iis_webapppool
+- module: community.windows.win_iis_webbinding
+- module: community.windows.win_iis_website
+author:
+- Henrik Wallström (@henrikwallstrom)
+'''
+
+EXAMPLES = r'''
+- name: Add ACME webapplication on IIS.
+ community.windows.win_iis_webapplication:
+ name: api
+ site: acme
+ state: present
+ physical_path: C:\apps\acme\api
+'''
+
+RETURN = r'''
+application_pool:
+ description: The used/implemented application_pool value.
+ returned: success
+ type: str
+ sample: DefaultAppPool
+physical_path:
+ description: The used/implemented physical_path value.
+ returned: success
+ type: str
+ sample: C:\apps\acme\api
+connect_as:
+ description: How IIS will try to authenticate to the physical_path.
+ returned: when the application exists
+ type: str
+ sample: specific_user
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_iis_webapppool.ps1 b/ansible_collections/community/windows/plugins/modules/win_iis_webapppool.ps1
new file mode 100644
index 00000000..28b339b4
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_iis_webapppool.ps1
@@ -0,0 +1,341 @@
+#!powershell
+
+# Copyright: (c) 2015, Henrik Wallström <henrik@wallstroms.nu>
+# 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 -arguments $args -supports_check_mode $true
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
+
+$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateSet "started", "restarted", "stopped", "absent", "present"
+$result = @{
+ changed = $false
+ attributes = @{}
+ info = @{
+ name = $name
+ state = $state
+ attributes = @{}
+ cpu = @{}
+ failure = @{}
+ processModel = @{}
+ recycling = @{
+ periodicRestart = @{}
+ }
+ }
+}
+
+# Stores the free form attributes for the module
+$attributes = @{}
+$input_attributes = Get-AnsibleParam -obj $params -name "attributes"
+if ($input_attributes) {
+ if ($input_attributes -is [System.Collections.Hashtable]) {
+ # Uses dict style parameters, newer and recommended style
+ $attributes = $input_attributes
+ }
+ else {
+ Fail-Json -obj $result -message "Using a string for the attributes parameter is not longer supported, please use a dict instead"
+ }
+}
+$result.attributes = $attributes
+
+Function Get-DotNetClassForAttribute($attribute_parent) {
+ switch ($attribute_parent) {
+ "attributes" { [Microsoft.Web.Administration.ApplicationPool] }
+ "cpu" { [Microsoft.Web.Administration.ApplicationPoolCpu] }
+ "failure" { [Microsoft.Web.Administration.ApplicationPoolFailure] }
+ "processModel" { [Microsoft.Web.Administration.ApplicationPoolProcessModel] }
+ "recycling" { [Microsoft.Web.Administration.ApplicationPoolRecycling] }
+ default { [Microsoft.Web.Administration.ApplicationPool] }
+ }
+}
+
+Function Convert-CollectionToList($collection) {
+ $list = @()
+
+ if ($collection -is [String]) {
+ $raw_list = $collection -split ","
+ foreach ($entry in $raw_list) {
+ $list += $entry.Trim()
+ }
+ }
+ elseif ($collection -is [Microsoft.IIs.PowerShell.Framework.ConfigurationElement]) {
+ # the collection is the value from IIS itself, we need to conver accordingly
+ foreach ($entry in $collection.Collection) {
+ $list += $entry.Value.ToString()
+ }
+ }
+ elseif ($collection -isnot [Array]) {
+ $list += $collection
+ }
+ else {
+ $list = $collection
+ }
+
+ return , $list
+}
+
+Function Compare-Value($current, $new) {
+ if ($null -eq $current) {
+ return $true
+ }
+
+ if ($current -is [Array]) {
+ if ($new -isnot [Array]) {
+ return $true
+ }
+
+ if ($current.Count -ne $new.Count) {
+ return $true
+ }
+ for ($i = 0; $i -lt $current.Count; $i++) {
+ if ($current[$i] -ne $new[$i]) {
+ return $true
+ }
+ }
+ }
+ else {
+ if ($current -ne $new) {
+ return $true
+ }
+ }
+ return $false
+}
+
+Function Convert-ToPropertyValue($pool, $attribute_key, $attribute_value) {
+ # Will convert the new value to the enum value expected and cast accordingly to the type
+ if ([bool]($attribute_value.PSobject.Properties -match "Value")) {
+ $attribute_value = $attribute_value.Value
+ }
+ $attribute_key_split = $attribute_key -split "\."
+ if ($attribute_key_split.Length -eq 1) {
+ $attribute_parent = "attributes"
+ $attribute_child = $attribute_key
+ $attribute_meta = $pool.Attributes | Where-Object { $_.Name -eq $attribute_child }
+ }
+ elseif ($attribute_key_split.Length -gt 1) {
+ $attribute_parent = $attribute_key_split[0]
+ $attribute_key_split = $attribute_key_split[1..$($attribute_key_split.Length - 1)]
+ $parent = $pool.$attribute_parent
+
+ foreach ($key in $attribute_key_split) {
+ $attribute_meta = $parent.Attributes | Where-Object { $_.Name -eq $key }
+ $parent = $parent.$key
+ if ($null -eq $attribute_meta) {
+ $attribute_meta = $parent
+ }
+ }
+ $attribute_child = $attribute_key_split[-1]
+ }
+
+ if ($attribute_meta) {
+ if (($attribute_meta.PSObject.Properties.Name -eq "Collection").Count -gt 0) {
+ return , (Convert-CollectionToList -collection $attribute_value)
+ }
+ $type = $attribute_meta.Schema.Type
+ $value = $attribute_value
+ if ($type -eq "enum") {
+ # Attempt to convert the value from human friendly to enum value - use existing value if we fail
+ $dot_net_class = Get-DotNetClassForAttribute -attribute_parent $attribute_parent
+ $enum_attribute_name = $attribute_child.Substring(0, 1).ToUpper() + $attribute_child.Substring(1)
+ $enum = $dot_net_class.GetProperty($enum_attribute_name).PropertyType.FullName
+ if ($enum) {
+ $enum_values = [Enum]::GetValues($enum)
+ foreach ($enum_value in $enum_values) {
+ if ($attribute_value.GetType() -is $enum_value.GetType()) {
+ if ($enum_value -eq $attribute_value) {
+ $value = $enum_value
+ break
+ }
+ }
+ else {
+ if ([System.String]$enum_value -eq [System.String]$attribute_value) {
+ $value = $enum_value
+ break
+ }
+ }
+ }
+ }
+ }
+ # Try and cast the variable using the chosen type, revert to the default if it fails
+ Set-Variable -Name casted_value -Value ($value -as ([type] $attribute_meta.TypeName))
+ if ($null -eq $casted_value) {
+ $value
+ }
+ else {
+ $casted_value
+ }
+ }
+ else {
+ $attribute_value
+ }
+}
+
+# Ensure WebAdministration module is loaded
+if ($null -eq (Get-Module -Name "WebAdministration" -ErrorAction SilentlyContinue)) {
+ Import-Module WebAdministration
+ $web_admin_dll_path = Join-Path $env:SystemRoot system32\inetsrv\Microsoft.Web.Administration.dll
+ Add-Type -LiteralPath $web_admin_dll_path
+}
+
+$pool = Get-Item -LiteralPath IIS:\AppPools\$name -ErrorAction SilentlyContinue
+if ($state -eq "absent") {
+ # Remove pool if present
+ if ($pool) {
+ try {
+ Remove-WebAppPool -Name $name -WhatIf:$check_mode
+ }
+ catch {
+ Fail-Json $result "Failed to remove Web App pool $($name): $($_.Exception.Message)"
+ }
+ $result.changed = $true
+ }
+}
+else {
+ # Add pool if absent
+ if (-not $pool) {
+ if (-not $check_mode) {
+ try {
+ New-WebAppPool -Name $name > $null
+ }
+ catch {
+ Fail-Json $result "Failed to create new Web App Pool $($name): $($_.Exception.Message)"
+ }
+ }
+ $result.changed = $true
+ # If in check mode this pool won't actually exists so skip it
+ if (-not $check_mode) {
+ $pool = Get-Item -LiteralPath IIS:\AppPools\$name
+ }
+ }
+
+ # Cannot run the below in check mode if the pool did not always exist
+ if ($pool) {
+ # Modify pool based on parameters
+ foreach ($attribute in $attributes.GetEnumerator()) {
+ $attribute_key = $attribute.Name
+ $new_raw_value = $attribute.Value
+ $new_value = Convert-ToPropertyValue -pool $pool -attribute_key $attribute_key -attribute_value $new_raw_value
+
+ $current_raw_value = Get-ItemProperty -LiteralPath IIS:\AppPools\$name -Name $attribute_key -ErrorAction SilentlyContinue
+ $current_value = Convert-ToPropertyValue -pool $pool -attribute_key $attribute_key -attribute_value $current_raw_value
+
+ $changed = Compare-Value -current $current_value -new $new_value
+ if ($changed -eq $true) {
+ if ($new_value -is [Array]) {
+ try {
+ Clear-ItemProperty -LiteralPath IIS:\AppPools\$name -Name $attribute_key -WhatIf:$check_mode
+ }
+ catch {
+ $msg = -join @(
+ "Failed to clear attribute to Web App Pool $name. Attribute: $attribute_key, "
+ "Exception: $($_.Exception.Message)"
+ )
+ Fail-Json -obj $result -message $msg
+ }
+ foreach ($value in $new_value) {
+ try {
+ New-ItemProperty -LiteralPath IIS:\AppPools\$name -Name $attribute_key -Value @{value = $value } -WhatIf:$check_mode > $null
+ }
+ catch {
+ $msg = -join @(
+ "Failed to add new attribute to Web App Pool $name. Attribute: $attribute_key, "
+ "Value: $value, Exception: $($_.Exception.Message)"
+ )
+ Fail-Json -obj $result -message $msg
+ }
+ }
+ }
+ else {
+ try {
+ Set-ItemProperty -LiteralPath IIS:\AppPools\$name -Name $attribute_key -Value $new_value -WhatIf:$check_mode
+ }
+ catch {
+ $msg = -join @(
+ "Failed to set attribute to Web App Pool $name. Attribute: $attribute_key, "
+ "Value: $new_value, Exception: $($_.Exception.Message)"
+ )
+ Fail-Json $result $msg
+ }
+ }
+ $result.changed = $true
+ }
+ }
+
+ # Set the state of the pool
+ if ($pool.State -eq "Stopped") {
+ if ($state -eq "started" -or $state -eq "restarted") {
+ if (-not $check_mode) {
+ try {
+ Start-WebAppPool -Name $name > $null
+ }
+ catch {
+ Fail-Json $result "Failed to start Web App Pool $($name): $($_.Exception.Message)"
+ }
+ }
+ $result.changed = $true
+ }
+ }
+ else {
+ if ($state -eq "stopped") {
+ if (-not $check_mode) {
+ try {
+ Stop-WebAppPool -Name $name > $null
+ }
+ catch {
+ Fail-Json $result "Failed to stop Web App Pool $($name): $($_.Exception.Message)"
+ }
+ }
+ $result.changed = $true
+ }
+ elseif ($state -eq "restarted") {
+ if (-not $check_mode) {
+ try {
+ Restart-WebAppPool -Name $name > $null
+ }
+ catch {
+ Fail-Json $result "Failed to restart Web App Pool $($name): $($_.Exception.Message)"
+ }
+ }
+ $result.changed = $true
+ }
+ }
+ }
+}
+
+# Get all the current attributes for the pool
+$pool = Get-Item -LiteralPath IIS:\AppPools\$name -ErrorAction SilentlyContinue
+$elements = @("attributes", "cpu", "failure", "processModel", "recycling")
+
+foreach ($element in $elements) {
+ if ($element -eq "attributes") {
+ $attribute_collection = $pool.Attributes
+ $attribute_parent = $pool
+ }
+ else {
+ $attribute_collection = $pool.$element.Attributes
+ $attribute_parent = $pool.$element
+ }
+
+ foreach ($attribute in $attribute_collection) {
+ $attribute_name = $attribute.Name
+ if ($attribute_name -notlike "*password*") {
+ $attribute_value = $attribute_parent.$attribute_name
+
+ $result.info.$element.Add($attribute_name, $attribute_value)
+ }
+ }
+}
+
+# Manually get the periodicRestart attributes in recycling
+foreach ($attribute in $pool.recycling.periodicRestart.Attributes) {
+ $attribute_name = $attribute.Name
+ $attribute_value = $pool.recycling.periodicRestart.$attribute_name
+ $result.info.recycling.periodicRestart.Add($attribute_name, $attribute_value)
+}
+
+Exit-Json $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_iis_webapppool.py b/ansible_collections/community/windows/plugins/modules/win_iis_webapppool.py
new file mode 100644
index 00000000..7cca2e3b
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_iis_webapppool.py
@@ -0,0 +1,206 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2015, Henrik Wallström <henrik@wallstroms.nu>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_iis_webapppool
+short_description: Configure IIS Web Application Pools
+description:
+ - Creates, removes and configures an IIS Web Application Pool.
+options:
+ attributes:
+ description:
+ - This field is a free form dictionary value for the application pool
+ attributes.
+ - These attributes are based on the naming standard at
+ U(https://www.iis.net/configreference/system.applicationhost/applicationpools/add#005),
+ see the examples section for more details on how to set this.
+ - You can also set the attributes of child elements like cpu and
+ processModel, see the examples to see how it is done.
+ - While you can use the numeric values for enums it is recommended to use
+ the enum name itself, e.g. use SpecificUser instead of 3 for
+ processModel.identityType.
+ - managedPipelineMode may be either "Integrated" or "Classic".
+ - startMode may be either "OnDemand" or "AlwaysRunning".
+ - Use C(state) module parameter to modify the state of the app pool.
+ - When trying to set 'processModel.password' and you receive a 'Value
+ does fall within the expected range' error, you have a corrupted
+ keystore. Please follow
+ U(http://structuredsight.com/2014/10/26/im-out-of-range-youre-out-of-range/)
+ to help fix your host.
+ name:
+ description:
+ - Name of the application pool.
+ type: str
+ required: yes
+ state:
+ description:
+ - The state of the application pool.
+ - If C(absent) will ensure the app pool is removed.
+ - If C(present) will ensure the app pool is configured and exists.
+ - If C(restarted) will ensure the app pool exists and will restart, this
+ is never idempotent.
+ - If C(started) will ensure the app pool exists and is started.
+ - If C(stopped) will ensure the app pool exists and is stopped.
+ type: str
+ choices: [ absent, present, restarted, started, stopped ]
+ default: present
+seealso:
+- module: community.windows.win_iis_virtualdirectory
+- module: community.windows.win_iis_webapplication
+- module: community.windows.win_iis_webbinding
+- module: community.windows.win_iis_website
+author:
+- Henrik Wallström (@henrikwallstrom)
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = r'''
+- name: Return information about an existing application pool
+ community.windows.win_iis_webapppool:
+ name: DefaultAppPool
+ state: present
+
+- name: Create a new application pool in 'Started' state
+ community.windows.win_iis_webapppool:
+ name: AppPool
+ state: started
+
+- name: Stop an application pool
+ community.windows.win_iis_webapppool:
+ name: AppPool
+ state: stopped
+
+- name: Restart an application pool (non-idempotent)
+ community.windows.win_iis_webapppool:
+ name: AppPool
+ state: restarted
+
+- name: Change application pool attributes using new dict style
+ community.windows.win_iis_webapppool:
+ name: AppPool
+ attributes:
+ managedRuntimeVersion: v4.0
+ autoStart: no
+
+- name: Creates an application pool, sets attributes and starts it
+ community.windows.win_iis_webapppool:
+ name: AnotherAppPool
+ state: started
+ attributes:
+ managedRuntimeVersion: v4.0
+ autoStart: no
+
+# In the below example we are setting attributes in child element processModel
+# https://www.iis.net/configreference/system.applicationhost/applicationpools/add/processmodel
+- name: Manage child element and set identity of application pool
+ community.windows.win_iis_webapppool:
+ name: IdentitiyAppPool
+ state: started
+ attributes:
+ managedPipelineMode: Classic
+ processModel.identityType: SpecificUser
+ processModel.userName: '{{ansible_user}}'
+ processModel.password: '{{ansible_password}}'
+ processModel.loadUserProfile: true
+
+- name: Manage a timespan attribute
+ community.windows.win_iis_webapppool:
+ name: TimespanAppPool
+ state: started
+ attributes:
+ # Timespan with full string "day:hour:minute:second.millisecond"
+ recycling.periodicRestart.time: "00:00:05:00.000000"
+ recycling.periodicRestart.schedule: ["00:10:00", "05:30:00"]
+ # Shortened timespan "hour:minute:second"
+ processModel.pingResponseTime: "00:03:00"
+'''
+
+RETURN = r'''
+attributes:
+ description: Application Pool attributes that were set and processed by this
+ module invocation.
+ returned: success
+ type: dict
+ sample:
+ enable32BitAppOnWin64: "true"
+ managedRuntimeVersion: "v4.0"
+ managedPipelineMode: "Classic"
+info:
+ description: Information on current state of the Application Pool. See
+ https://www.iis.net/configreference/system.applicationhost/applicationpools/add#005
+ for the full list of return attributes based on your IIS version.
+ returned: success
+ type: complex
+ sample:
+ contains:
+ attributes:
+ description: Key value pairs showing the current Application Pool attributes.
+ returned: success
+ type: dict
+ sample:
+ autoStart: true
+ managedRuntimeLoader: "webengine4.dll"
+ managedPipelineMode: "Classic"
+ name: "DefaultAppPool"
+ CLRConfigFile: ""
+ passAnonymousToken: true
+ applicationPoolSid: "S-1-5-82-1352790163-598702362-1775843902-1923651883-1762956711"
+ queueLength: 1000
+ managedRuntimeVersion: "v4.0"
+ state: "Started"
+ enableConfigurationOverride: true
+ startMode: "OnDemand"
+ enable32BitAppOnWin64: true
+ cpu:
+ description: Key value pairs showing the current Application Pool cpu attributes.
+ returned: success
+ type: dict
+ sample:
+ action: "NoAction"
+ limit: 0
+ resetInterval:
+ Days: 0
+ Hours: 0
+ failure:
+ description: Key value pairs showing the current Application Pool failure attributes.
+ returned: success
+ type: dict
+ sample:
+ autoShutdownExe: ""
+ orphanActionExe: ""
+ rapidFailProtextionInterval:
+ Days: 0
+ Hours: 0
+ name:
+ description: Name of Application Pool that was processed by this module invocation.
+ returned: success
+ type: str
+ sample: "DefaultAppPool"
+ processModel:
+ description: Key value pairs showing the current Application Pool processModel attributes.
+ returned: success
+ type: dict
+ sample:
+ identityType: "ApplicationPoolIdentity"
+ logonType: "LogonBatch"
+ pingInterval:
+ Days: 0
+ Hours: 0
+ recycling:
+ description: Key value pairs showing the current Application Pool recycling attributes.
+ returned: success
+ type: dict
+ sample:
+ disallowOverlappingRotation: false
+ disallowRotationOnConfigChange: false
+ logEventOnRecycle: "Time,Requests,Schedule,Memory,IsapiUnhealthy,OnDemand,ConfigChange,PrivateMemory"
+ state:
+ description: Current runtime state of the pool as the module completed.
+ returned: success
+ type: str
+ sample: "Started"
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_iis_webbinding.ps1 b/ansible_collections/community/windows/plugins/modules/win_iis_webbinding.ps1
new file mode 100644
index 00000000..b6073f4f
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_iis_webbinding.ps1
@@ -0,0 +1,348 @@
+#!powershell
+
+# Copyright: (c) 2017, Noah Sparks <nsparks@outlook.com>
+# Copyright: (c) 2015, Henrik Wallström <henrik@wallstroms.nu>
+# 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 -arguments $args -supports_check_mode $true
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
+
+$name = Get-AnsibleParam $params -name "name" -type str -failifempty $true -aliases 'website'
+$state = Get-AnsibleParam $params "state" -default "present" -validateSet "present", "absent"
+$host_header = Get-AnsibleParam $params -name "host_header" -type str
+$protocol = Get-AnsibleParam $params -name "protocol" -type str -default 'http'
+$port = Get-AnsibleParam $params -name "port" -default '80'
+$ip = Get-AnsibleParam $params -name "ip" -default '*'
+$certificateHash = Get-AnsibleParam $params -name "certificate_hash" -type str -default ([string]::Empty)
+$certificateStoreName = Get-AnsibleParam $params -name "certificate_store_name" -type str -default ([string]::Empty)
+$sslFlags = Get-AnsibleParam $params -name "ssl_flags" -default '0' -ValidateSet '0', '1', '2', '3'
+
+$result = @{
+ changed = $false
+}
+
+#################
+### Functions ###
+#################
+function New-BindingInfo {
+ $ht = @{
+ 'bindingInformation' = $args[0].bindingInformation
+ 'ip' = $args[0].bindingInformation.split(':')[0]
+ 'port' = [int]$args[0].bindingInformation.split(':')[1]
+ 'hostheader' = $args[0].bindingInformation.split(':')[2]
+ #'isDsMapperEnabled' = $args[0].isDsMapperEnabled
+ 'protocol' = $args[0].protocol
+ 'certificateStoreName' = $args[0].certificateStoreName
+ 'certificateHash' = $args[0].certificateHash
+ }
+
+ #handle sslflag support
+ If ([version][System.Environment]::OSVersion.Version -lt [version]'6.2') {
+ $ht.sslFlags = 'not supported'
+ }
+ Else {
+ $ht.sslFlags = [int]$args[0].sslFlags
+ }
+
+ Return $ht
+}
+
+# Used instead of get-webbinding to ensure we always return a single binding
+# We can't filter properly with get-webbinding...ex get-webbinding ip * returns all bindings
+# pass it $binding_parameters hashtable
+function Get-SingleWebBinding {
+
+ Try {
+ $site_bindings = get-webbinding -name $args[0].name
+ }
+ Catch {
+ # 2k8r2 throws this error when you run get-webbinding with no bindings in iis
+ $msg = 'Cannot process argument because the value of argument "obj" is null. Change the value of argument "obj" to a non-null value'
+ If (-not $_.Exception.Message.CompareTo($msg)) {
+ Throw $_.Exception.Message
+ }
+ Else { return }
+ }
+
+ Foreach ($binding in $site_bindings) {
+ $splits = $binding.bindingInformation -split ':'
+
+ if (
+ $args[0].protocol -eq $binding.protocol -and
+ $args[0].ipaddress -eq $splits[0] -and
+ $args[0].port -eq $splits[1] -and
+ $args[0].hostheader -eq $splits[2]
+ ) {
+ Return $binding
+ }
+ }
+}
+
+
+#############################
+### Pre-Action Validation ###
+#############################
+$os_version = [version][System.Environment]::OSVersion.Version
+
+# Ensure WebAdministration module is loaded
+If ($os_version -lt [version]'6.1') {
+ Try {
+ Add-PSSnapin WebAdministration
+ }
+ Catch {
+ Fail-Json -obj $result -message "The WebAdministration snap-in is not present. Please make sure it is installed."
+ }
+}
+Else {
+ Try {
+ Import-Module WebAdministration
+ }
+ Catch {
+ Fail-Json -obj $result -message "Failed to load WebAdministration module. Is IIS installed? $($_.Exception.Message)"
+ }
+}
+
+# ensure website targetted exists. -Name filter doesn't work on 2k8r2 so do where-object instead
+$website_check = get-website | Where-Object { $_.name -eq $name }
+If (-not $website_check) {
+ Fail-Json -obj $result -message "Unable to retrieve website with name $Name. Make sure the website name is valid and exists."
+}
+
+# if OS older than 2012 (6.2) and ssl flags are set, fail. Otherwise toggle sni_support
+If ($os_version -lt [version]'6.2') {
+ If ($sslFlags -ne 0) {
+ Fail-Json -obj $result -message "SNI and Certificate Store support is not available for systems older than 2012 (6.2)"
+ }
+ $sni_support = $false #will cause the sslflags check later to skip
+}
+Else {
+ $sni_support = $true
+}
+
+# make sure ssl flags only specified with https protocol
+If ($protocol -ne 'https' -and $sslFlags -gt 0) {
+ Fail-Json -obj $result -message "SSLFlags can only be set for HTTPS protocol"
+}
+
+# validate certificate details if provided
+# we don't do anything with cert on state: absent, so only validate present
+If ($certificateHash -and $state -eq 'present') {
+ If ($protocol -ne 'https') {
+ Fail-Json -obj $result -message "You can only provide a certificate thumbprint when protocol is set to https"
+ }
+
+ #apply default for cert store name
+ If (-Not $certificateStoreName) {
+ $certificateStoreName = 'my'
+ }
+
+ #validate cert path
+ $cert_path = "cert:\LocalMachine\$certificateStoreName\$certificateHash"
+ If (-Not (Test-Path -LiteralPath $cert_path) ) {
+ Fail-Json -obj $result -message "Unable to locate certificate at $cert_path"
+ }
+}
+
+# make sure binding info is valid for central cert store if sslflags -gt 1
+If ($sslFlags -gt 1 -and ($certificateHash -ne [string]::Empty -or $certificateStoreName -ne [string]::Empty)) {
+ Fail-Json -obj $result -message "You set sslFlags to $sslFlags. This indicates you wish to use the Central Certificate Store feature.
+ This cannot be used in combination with certficiate_hash and certificate_store_name. When using the Central Certificate Store feature,
+ the certificate is automatically retrieved from the store rather than manually assigned to the binding."
+}
+
+# disallow host_header: '*'
+If ($host_header -eq '*') {
+ Fail-Json -obj $result -message "To make or remove a catch-all binding, please omit the host_header parameter entirely rather than specify host_header *"
+}
+
+##########################
+### start action items ###
+##########################
+
+# create binding search splat
+$binding_parameters = @{
+ Name = $name
+ Protocol = $protocol
+ Port = $port
+ IPAddress = $ip
+}
+
+# insert host header to search if specified, otherwise it will return * (all bindings matching protocol/ip)
+If ($host_header) {
+ $binding_parameters.HostHeader = $host_header
+}
+Else {
+ $binding_parameters.HostHeader = [string]::Empty
+}
+
+# Get bindings matching parameters
+Try {
+ $current_bindings = Get-SingleWebBinding $binding_parameters
+}
+Catch {
+ Fail-Json -obj $result -message "Failed to retrieve bindings with Get-SingleWebBinding - $($_.Exception.Message)"
+}
+
+################################################
+### Remove binding or exit if already absent ###
+################################################
+If ($current_bindings -and $state -eq 'absent') {
+ Try {
+ #there is a bug in this method that will result in all bindings being removed if the IP in $current_bindings is a *
+ #$current_bindings | Remove-WebBinding -verbose -WhatIf:$check_mode
+
+ #another method that did not work. It kept failing to match on element and removed everything.
+ #$element = @{protocol="$protocol";bindingInformation="$ip`:$port`:$host_header"}
+ #Remove-WebconfigurationProperty -filter $current_bindings.ItemXPath -Name Bindings.collection -AtElement $element -WhatIf #:$check_mode
+
+ #this method works
+ [array]$bindings = Get-WebconfigurationProperty -filter $current_bindings.ItemXPath -Name Bindings.collection
+
+ $index = Foreach ($item in $bindings) {
+ If ( $protocol -eq $item.protocol -and $current_bindings.bindingInformation -eq $item.bindingInformation ) {
+ $bindings.indexof($item)
+ break
+ }
+ }
+
+ Remove-WebconfigurationProperty -filter $current_bindings.ItemXPath -Name Bindings.collection -AtIndex $index -WhatIf:$check_mode
+ $result.changed = $true
+ }
+
+ Catch {
+ Fail-Json -obj $result -message "Failed to remove the binding from IIS - $($_.Exception.Message)"
+ }
+
+ # removing bindings from iis may not also remove them from iis:\sslbindings
+
+ $result.operation_type = 'removed'
+ $result.binding_info = $current_bindings | ForEach-Object { New-BindingInfo $_ }
+ Exit-Json -obj $result
+}
+ElseIf (-Not $current_bindings -and $state -eq 'absent') {
+ # exit changed: false since it's already gone
+ Exit-Json -obj $result
+}
+
+
+################################
+### Modify existing bindings ###
+################################
+<#
+since we have already have the parameters available to get-webbinding,
+we just need to check here for the ones that are not available which are the
+ssl settings (hash, store, sslflags). If they aren't set we update here, or
+exit with changed: false
+#>
+ElseIf ($current_bindings) {
+ #ran into a strange edge case in testing where I was able to retrieve bindings but not expand all the properties
+ #when adding a self-signed wildcard cert to a binding. it seemed to permanently break the binding. only removing it
+ #would cause the error to stop.
+ Try {
+ $null = $current_bindings | Select-Object *
+ }
+ Catch {
+ $msg = -join @(
+ "Found a matching binding, but failed to expand it's properties (get-binding | FL *). "
+ "In testing, this was caused by using a self-signed wildcard certificate. $($_.Exception.Message)"
+ )
+ Fail-Json -obj $result -message $msg
+ }
+
+ # check if there is a match on the ssl parameters
+ If ( ($current_bindings.sslFlags -ne $sslFlags -and $sni_support) -or
+ $current_bindings.certificateHash -ne $certificateHash -or
+ $current_bindings.certificateStoreName -ne $certificateStoreName) {
+ # match/update SNI
+ If ($current_bindings.sslFlags -ne $sslFlags -and $sni_support) {
+ Try {
+ Set-WebBinding -Name $name -IPAddress $ip -Port $port -HostHeader $host_header -PropertyName sslFlags -value $sslFlags -whatif:$check_mode
+ $result.changed = $true
+ }
+ Catch {
+ Fail-Json -obj $result -message "Failed to update sslFlags on binding - $($_.Exception.Message)"
+ }
+
+ # Refresh the binding object since it has been changed
+ Try {
+ $current_bindings = Get-SingleWebBinding $binding_parameters
+ }
+ Catch {
+ Fail-Json -obj $result -message "Failed to refresh bindings after setting sslFlags - $($_.Exception.Message)"
+ }
+ }
+ # match/update certificate
+ If ($current_bindings.certificateHash -ne $certificateHash -or $current_bindings.certificateStoreName -ne $certificateStoreName) {
+ If (-Not $check_mode) {
+ Try {
+ $current_bindings.AddSslCertificate($certificateHash, $certificateStoreName)
+ }
+ Catch {
+ Fail-Json -obj $result -message "Failed to set new SSL certificate - $($_.Exception.Message)"
+ }
+ }
+ }
+ $result.changed = $true
+ $result.operation_type = 'updated'
+ $result.website_state = (Get-Website | Where-Object { $_.Name -eq $Name }).State
+ $result.binding_info = New-BindingInfo (Get-SingleWebBinding $binding_parameters)
+ Exit-Json -obj $result #exit changed true
+ }
+ Else {
+ $result.operation_type = 'matched'
+ $result.website_state = (Get-Website | Where-Object { $_.Name -eq $Name }).State
+ $result.binding_info = New-BindingInfo (Get-SingleWebBinding $binding_parameters)
+ Exit-Json -obj $result #exit changed false
+ }
+}
+
+########################
+### Add new bindings ###
+########################
+ElseIf (-not $current_bindings -and $state -eq 'present') {
+ # add binding. this creates the binding, but does not apply a certificate to it.
+ Try {
+ If (-not $check_mode) {
+ If ($sni_support) {
+ New-WebBinding @binding_parameters -SslFlags $sslFlags -Force
+ }
+ Else {
+ New-WebBinding @binding_parameters -Force
+ }
+ }
+ $result.changed = $true
+ }
+ Catch {
+ $result.website_state = (Get-Website | Where-Object { $_.Name -eq $Name }).State
+ Fail-Json -obj $result -message "Failed at creating new binding (note: creating binding and adding ssl are separate steps) - $($_.Exception.Message)"
+ }
+
+ # add certificate to binding
+ If ($certificateHash -and -not $check_mode) {
+ Try {
+ #$new_binding = get-webbinding -Name $name -IPAddress $ip -port $port -Protocol $protocol -hostheader $host_header
+ $new_binding = Get-SingleWebBinding $binding_parameters
+ $new_binding.addsslcertificate($certificateHash, $certificateStoreName)
+ }
+ Catch {
+ $result.website_state = (Get-Website | Where-Object { $_.Name -eq $Name }).State
+ Fail-Json -obj $result -message "Failed to set new SSL certificate - $($_.Exception.Message)"
+ }
+ }
+
+ $result.changed = $true
+ $result.operation_type = 'added'
+ $result.website_state = (Get-Website | Where-Object { $_.Name -eq $Name }).State
+
+ # incase there are no bindings we do a check before calling New-BindingInfo
+ $web_binding = Get-SingleWebBinding $binding_parameters
+ if ($web_binding) {
+ $result.binding_info = New-BindingInfo $web_binding
+ }
+ else {
+ $result.binding_info = $null
+ }
+ Exit-Json $result
+}
diff --git a/ansible_collections/community/windows/plugins/modules/win_iis_webbinding.py b/ansible_collections/community/windows/plugins/modules/win_iis_webbinding.py
new file mode 100644
index 00000000..cf3d42b1
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_iis_webbinding.py
@@ -0,0 +1,145 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2017, Noah Sparks <nsparks@outlook.com>
+# Copyright: (c) 2017, Henrik Wallström <henrik@wallstroms.nu>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_iis_webbinding
+short_description: Configures a IIS Web site binding
+description:
+ - Creates, removes and configures a binding to an existing IIS Web site.
+options:
+ name:
+ description:
+ - Names of web site.
+ type: str
+ required: yes
+ aliases: [ website ]
+ state:
+ description:
+ - State of the binding.
+ type: str
+ choices: [ absent, present ]
+ default: present
+ port:
+ description:
+ - The port to bind to / use for the new site.
+ type: int
+ default: 80
+ ip:
+ description:
+ - The IP address to bind to / use for the new site.
+ type: str
+ default: '*'
+ host_header:
+ description:
+ - The host header to bind to / use for the new site.
+ - If you are creating/removing a catch-all binding, omit this parameter rather than defining it as '*'.
+ type: str
+ protocol:
+ description:
+ - The protocol to be used for the Web binding (usually HTTP, HTTPS, or FTP).
+ type: str
+ default: http
+ certificate_hash:
+ description:
+ - Certificate hash (thumbprint) for the SSL binding. The certificate hash is the unique identifier for the certificate.
+ type: str
+ certificate_store_name:
+ description:
+ - Name of the certificate store where the certificate for the binding is located.
+ type: str
+ default: my
+ ssl_flags:
+ description:
+ - This parameter is only valid on Server 2012 and newer.
+ - Primarily used for enabling and disabling server name indication (SNI).
+ - Set to C(0) to disable SNI.
+ - Set to C(1) to enable SNI.
+ type: str
+seealso:
+- module: community.windows.win_iis_virtualdirectory
+- module: community.windows.win_iis_webapplication
+- module: community.windows.win_iis_webapppool
+- module: community.windows.win_iis_website
+author:
+ - Noah Sparks (@nwsparks)
+ - Henrik Wallström (@henrikwallstrom)
+'''
+
+EXAMPLES = r'''
+- name: Add a HTTP binding on port 9090
+ community.windows.win_iis_webbinding:
+ name: Default Web Site
+ port: 9090
+ state: present
+
+- name: Remove the HTTP binding on port 9090
+ community.windows.win_iis_webbinding:
+ name: Default Web Site
+ port: 9090
+ state: absent
+
+- name: Remove the default http binding
+ community.windows.win_iis_webbinding:
+ name: Default Web Site
+ port: 80
+ ip: '*'
+ state: absent
+
+- name: Add a HTTPS binding
+ community.windows.win_iis_webbinding:
+ name: Default Web Site
+ protocol: https
+ port: 443
+ ip: 127.0.0.1
+ certificate_hash: B0D0FA8408FC67B230338FCA584D03792DA73F4C
+ state: present
+
+- name: Add a HTTPS binding with host header and SNI enabled
+ community.windows.win_iis_webbinding:
+ name: Default Web Site
+ protocol: https
+ port: 443
+ host_header: test.com
+ ssl_flags: 1
+ certificate_hash: D1A3AF8988FD32D1A3AF8988FD323792DA73F4C
+ state: present
+'''
+
+RETURN = r'''
+website_state:
+ description:
+ - The state of the website being targetted
+ - Can be helpful in case you accidentally cause a binding collision
+ which can result in the targetted site being stopped
+ returned: always
+ type: str
+ sample: "Started"
+operation_type:
+ description:
+ - The type of operation performed
+ - Can be removed, updated, matched, or added
+ returned: on success
+ type: str
+ sample: "removed"
+binding_info:
+ description:
+ - Information on the binding being manipulated
+ returned: on success
+ type: dict
+ sample: |-
+ "binding_info": {
+ "bindingInformation": "127.0.0.1:443:",
+ "certificateHash": "FF3910CE089397F1B5A77EB7BAFDD8F44CDE77DD",
+ "certificateStoreName": "MY",
+ "hostheader": "",
+ "ip": "127.0.0.1",
+ "port": 443,
+ "protocol": "https",
+ "sslFlags": "not supported"
+ }
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_iis_website.ps1 b/ansible_collections/community/windows/plugins/modules/win_iis_website.ps1
new file mode 100644
index 00000000..571fb3d5
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_iis_website.ps1
@@ -0,0 +1,175 @@
+#!powershell
+
+# Copyright: (c) 2015, Henrik Wallström <henrik@wallstroms.nu>
+# 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
+$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
+$application_pool = Get-AnsibleParam -obj $params -name "application_pool" -type "str"
+$physical_path = Get-AnsibleParam -obj $params -name "physical_path" -type "str"
+$site_id = Get-AnsibleParam -obj $params -name "site_id" -type "str"
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -validateset "absent", "restarted", "started", "stopped"
+
+# Binding Parameters
+$bind_port = Get-AnsibleParam -obj $params -name "port" -type "int"
+$bind_ip = Get-AnsibleParam -obj $params -name "ip" -type "str"
+$bind_hostname = Get-AnsibleParam -obj $params -name "hostname" -type "str"
+
+# Custom site Parameters from string where properties
+# are separated by a pipe and property name/values by colon.
+# Ex. "foo:1|bar:2"
+$parameters = Get-AnsibleParam -obj $params -name "parameters" -type "str"
+if ($null -ne $parameters) {
+ $parameters = @($parameters -split '\|' | ForEach-Object {
+ return , ($_ -split "\:", 2)
+ })
+}
+
+
+# Ensure WebAdministration module is loaded
+if ($null -eq (Get-Module "WebAdministration" -ErrorAction SilentlyContinue)) {
+ Import-Module WebAdministration
+}
+
+# Result
+$result = @{
+ site = @{}
+ changed = $false
+}
+
+# Site info
+$site = Get-Website | Where-Object { $_.Name -eq $name }
+
+Try {
+ # Add site
+ If (($state -ne 'absent') -and (-not $site)) {
+ If (-not $physical_path) {
+ Fail-Json -obj $result -message "missing required arguments: physical_path"
+ }
+ ElseIf (-not (Test-Path -LiteralPath $physical_path)) {
+ Fail-Json -obj $result -message "specified folder must already exist: physical_path"
+ }
+
+ $site_parameters = @{
+ Name = $name
+ PhysicalPath = $physical_path
+ }
+
+ If ($application_pool) {
+ $site_parameters.ApplicationPool = $application_pool
+ }
+
+ If ($site_id) {
+ $site_parameters.ID = $site_id
+ }
+
+ If ($bind_port) {
+ $site_parameters.Port = $bind_port
+ }
+
+ If ($bind_ip) {
+ $site_parameters.IPAddress = $bind_ip
+ }
+
+ If ($bind_hostname) {
+ $site_parameters.HostHeader = $bind_hostname
+ }
+
+ # Fix for error "New-Item : Index was outside the bounds of the array."
+ # This is a bug in the New-WebSite commandlet. Apparently there must be at least one site configured in IIS otherwise New-WebSite crashes.
+ # For more details, see http://stackoverflow.com/questions/3573889/ps-c-new-website-blah-throws-index-was-outside-the-bounds-of-the-array
+ $sites_list = Get-ChildItem -LiteralPath IIS:\sites
+ if ($null -eq $sites_list) {
+ if ($site_id) {
+ $site_parameters.ID = $site_id
+ }
+ else {
+ $site_parameters.ID = 1
+ }
+ }
+
+ $site = New-Website @site_parameters -Force
+ $result.changed = $true
+ }
+
+ # Remove site
+ If ($state -eq 'absent' -and $site) {
+ $site = Remove-Website -Name $name
+ $result.changed = $true
+ }
+
+ $site = Get-Website | Where-Object { $_.Name -eq $name }
+ If ($site) {
+ # Change Physical Path if needed
+ if ($physical_path) {
+ If (-not (Test-Path -LiteralPath $physical_path)) {
+ Fail-Json -obj $result -message "specified folder must already exist: physical_path"
+ }
+
+ $folder = Get-Item -LiteralPath $physical_path
+ If ($folder.FullName -ne $site.PhysicalPath) {
+ Set-ItemProperty -LiteralPath "IIS:\Sites\$($site.Name)" -name physicalPath -value $folder.FullName
+ $result.changed = $true
+ }
+ }
+
+ # Change Application Pool if needed
+ if ($application_pool) {
+ If ($application_pool -ne $site.applicationPool) {
+ Set-ItemProperty -LiteralPath "IIS:\Sites\$($site.Name)" -name applicationPool -value $application_pool
+ $result.changed = $true
+ }
+ }
+
+ # Set properties
+ if ($parameters) {
+ $parameters | ForEach-Object {
+ $property_value = Get-ItemProperty -LiteralPath "IIS:\Sites\$($site.Name)" $_[0]
+
+ switch ($property_value.GetType().Name) {
+ "ConfigurationAttribute" { $parameter_value = $property_value.value }
+ "String" { $parameter_value = $property_value }
+ }
+
+ if ((-not $parameter_value) -or ($parameter_value) -ne $_[1]) {
+ Set-ItemProperty -LiteralPath "IIS:\Sites\$($site.Name)" $_[0] $_[1]
+ $result.changed = $true
+ }
+ }
+ }
+
+ # Set run state
+ if ((($state -eq 'stopped') -or ($state -eq 'restarted')) -and ($site.State -eq 'Started')) {
+ Stop-Website -Name $name -ErrorAction Stop
+ $result.changed = $true
+ }
+ if ((($state -eq 'started') -and ($site.State -eq 'Stopped')) -or ($state -eq 'restarted')) {
+ Start-Website -Name $name -ErrorAction Stop
+ $result.changed = $true
+ }
+ }
+}
+Catch {
+ Fail-Json -obj $result -message $_.Exception.Message
+}
+
+if ($state -ne 'absent') {
+ $site = Get-Website | Where-Object { $_.Name -eq $name }
+}
+
+if ($site) {
+ $result.site = @{
+ Name = $site.Name
+ ID = $site.ID
+ State = $site.State
+ PhysicalPath = $site.PhysicalPath
+ ApplicationPool = $site.applicationPool
+ Bindings = @($site.Bindings.Collection | ForEach-Object { $_.BindingInformation })
+ }
+}
+
+Exit-Json -obj $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_iis_website.py b/ansible_collections/community/windows/plugins/modules/win_iis_website.py
new file mode 100644
index 00000000..c1fe192d
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_iis_website.py
@@ -0,0 +1,130 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2015, Henrik Wallström <henrik@wallstroms.nu>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_iis_website
+short_description: Configures a IIS Web site
+description:
+ - Creates, Removes and configures a IIS Web site.
+options:
+ name:
+ description:
+ - Names of web site.
+ type: str
+ required: yes
+ site_id:
+ description:
+ - Explicitly set the IIS numeric ID for a site.
+ - Note that this value cannot be changed after the website has been created.
+ type: str
+ state:
+ description:
+ - State of the web site
+ type: str
+ choices: [ absent, started, stopped, restarted ]
+ physical_path:
+ description:
+ - The physical path on the remote host to use for the new site.
+ - The specified folder must already exist.
+ type: str
+ application_pool:
+ description:
+ - The application pool in which the new site executes.
+ type: str
+ port:
+ description:
+ - The port to bind to / use for the new site.
+ type: int
+ ip:
+ description:
+ - The IP address to bind to / use for the new site.
+ type: str
+ hostname:
+ description:
+ - The host header to bind to / use for the new site.
+ type: str
+ parameters:
+ description:
+ - Custom site Parameters from string where properties are separated by a pipe and property name/values by colon Ex. "foo:1|bar:2"
+ - Some custom parameters that you can use are listed below, this isn't a definitive list but some common parameters.
+ - C(logfile.directory) - Physical path to store Logs, e.g. C(D:\IIS-LOGs\).
+ - C(logfile.period) - Log file rollover scheduled accepting these values, how frequently the log file should be rolled-over,
+ e.g. C(Hourly, Daily, Weekly, Monthly).
+ - C(logfile.LogFormat) - Log file format, by default IIS uses C(W3C).
+ - C(logfile.truncateSize) - The size at which the log file contents will be trunsted, expressed in bytes.
+ type: str
+seealso:
+- module: community.windows.win_iis_virtualdirectory
+- module: community.windows.win_iis_webapplication
+- module: community.windows.win_iis_webapppool
+- module: community.windows.win_iis_webbinding
+author:
+- Henrik Wallström (@henrikwallstrom)
+'''
+
+EXAMPLES = r'''
+
+# Start a website
+
+- name: Acme IIS site
+ community.windows.win_iis_website:
+ name: Acme
+ state: started
+ port: 80
+ ip: 127.0.0.1
+ hostname: acme.local
+ application_pool: acme
+ physical_path: C:\sites\acme
+ parameters: logfile.directory:C:\sites\logs
+ register: website
+
+# Remove Default Web Site and the standard port 80 binding
+- name: Remove Default Web Site
+ community.windows.win_iis_website:
+ name: "Default Web Site"
+ state: absent
+
+# Create a WebSite with custom Logging configuration (Logs Location, Format and Rolling Over).
+
+- name: Creating WebSite with Custom Log location, Format 3WC and rolling over every hour.
+ community.windows.win_iis_website:
+ name: MyCustom_Web_Shop_Site
+ state: started
+ port: 80
+ ip: '*'
+ hostname: '*'
+ physical_path: D:\wwwroot\websites\my-shop-site
+ parameters: logfile.directory:D:\IIS-LOGS\websites\my-shop-site|logfile.period:Hourly|logFile.logFormat:W3C
+ application_pool: my-shop-site
+
+# Some commandline examples:
+
+# This return information about an existing host
+# $ ansible -i vagrant-inventory -m community.windows.win_iis_website -a "name='Default Web Site'" window
+# host | success >> {
+# "changed": false,
+# "site": {
+# "ApplicationPool": "DefaultAppPool",
+# "Bindings": [
+# "*:80:"
+# ],
+# "ID": 1,
+# "Name": "Default Web Site",
+# "PhysicalPath": "%SystemDrive%\\inetpub\\wwwroot",
+# "State": "Stopped"
+# }
+# }
+
+# This stops an existing site.
+# $ ansible -i hosts -m community.windows.win_iis_website -a "name='Default Web Site' state=stopped" host
+
+# This creates a new site.
+# $ ansible -i hosts -m community.windows.win_iis_website -a "name=acme physical_path=C:\\sites\\acme" host
+
+# Change logfile.
+# $ ansible -i hosts -m community.windows.win_iis_website -a "name=acme physical_path=C:\\sites\\acme" host
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_inet_proxy.ps1 b/ansible_collections/community/windows/plugins/modules/win_inet_proxy.ps1
new file mode 100644
index 00000000..769a8f72
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_inet_proxy.ps1
@@ -0,0 +1,496 @@
+#!powershell
+
+# Copyright: (c) 2019, 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.AddType
+
+$spec = @{
+ options = @{
+ auto_detect = @{ type = "bool"; default = $true }
+ auto_config_url = @{ type = "str" }
+ proxy = @{ type = "raw" }
+ bypass = @{ type = "list"; elements = "str"; no_log = $false }
+ connection = @{ type = "str" }
+ }
+ required_by = @{
+ bypass = @("proxy")
+ }
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$auto_detect = $module.Params.auto_detect
+$auto_config_url = $module.Params.auto_config_url
+$proxy = $module.Params.proxy
+$bypass = $module.Params.bypass
+$connection = $module.Params.connection
+
+# Parse the raw value, it should be a Dictionary or String
+if ($proxy -is [System.Collections.IDictionary]) {
+ $valid_keys = [System.Collections.Generic.List`1[String]]@("http", "https", "ftp", "socks")
+ # Check to make sure we don't have any invalid keys in the dict
+ $invalid_keys = [System.Collections.Generic.List`1[String]]@()
+ foreach ($k in $proxy.Keys) {
+ if ($k -notin $valid_keys) {
+ $invalid_keys.Add($k)
+ }
+ }
+
+ if ($invalid_keys.Count -gt 0) {
+ $invalid_keys = $invalid_keys | Sort-Object # So our test assertion doesn't fail due to random ordering
+ $module.FailJson("Invalid keys found in proxy: $($invalid_keys -join ', '). Valid keys are $($valid_keys -join ', ').")
+ }
+
+ # Build the proxy string in the form 'protocol=host;', the order of valid_keys is also important
+ $proxy_list = [System.Collections.Generic.List`1[String]]@()
+ foreach ($k in $valid_keys) {
+ if ($proxy.ContainsKey($k)) {
+ $proxy_list.Add("$k=$($proxy.$k)")
+ }
+ }
+ $proxy = $proxy_list -join ";"
+}
+elseif ($null -ne $proxy) {
+ $proxy = $proxy.ToString()
+}
+
+if ($bypass) {
+ if ([System.String]::IsNullOrEmpty($proxy)) {
+ $module.FailJson("missing parameter(s) required by ''bypass'': proxy")
+ }
+ $bypass = $bypass -join ';'
+}
+
+$win_inet_invoke = @'
+using Microsoft.Win32.SafeHandles;
+using System;
+using System.Collections.Generic;
+using System.Runtime.ConstrainedExecution;
+using System.Runtime.InteropServices;
+
+namespace Ansible.WinINetProxy
+{
+ internal class NativeHelpers
+ {
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ public class INTERNET_PER_CONN_OPTION_LISTW : IDisposable
+ {
+ public UInt32 dwSize;
+ public IntPtr pszConnection;
+ public UInt32 dwOptionCount;
+ public UInt32 dwOptionError;
+ public IntPtr pOptions;
+
+ public INTERNET_PER_CONN_OPTION_LISTW()
+ {
+ dwSize = (UInt32)Marshal.SizeOf(this);
+ }
+
+ public void Dispose()
+ {
+ if (pszConnection != IntPtr.Zero)
+ Marshal.FreeHGlobal(pszConnection);
+ if (pOptions != IntPtr.Zero)
+ Marshal.FreeHGlobal(pOptions);
+ GC.SuppressFinalize(this);
+ }
+ ~INTERNET_PER_CONN_OPTION_LISTW() { this.Dispose(); }
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ public class INTERNET_PER_CONN_OPTIONW : IDisposable
+ {
+ public INTERNET_PER_CONN_OPTION dwOption;
+ public ValueUnion Value;
+
+ [StructLayout(LayoutKind.Explicit)]
+ public class ValueUnion
+ {
+ [FieldOffset(0)]
+ public UInt32 dwValue;
+
+ [FieldOffset(0)]
+ public IntPtr pszValue;
+
+ [FieldOffset(0)]
+ public System.Runtime.InteropServices.ComTypes.FILETIME ftValue;
+ }
+
+ public void Dispose()
+ {
+ // We can't just check if Value.pszValue is not IntPtr.Zero as the union means it could be set even
+ // when the value is a UInt32 or FILETIME. We check against a known string option type and only free
+ // the value in those cases.
+ List<INTERNET_PER_CONN_OPTION> stringOptions = new List<INTERNET_PER_CONN_OPTION>
+ {
+ { INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_AUTOCONFIG_URL },
+ { INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_BYPASS },
+ { INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_SERVER }
+ };
+ if (Value != null && Value.pszValue != IntPtr.Zero && stringOptions.Contains(dwOption))
+ Marshal.FreeHGlobal(Value.pszValue);
+ GC.SuppressFinalize(this);
+ }
+ ~INTERNET_PER_CONN_OPTIONW() { this.Dispose(); }
+ }
+
+ public enum INTERNET_OPTION : uint
+ {
+ INTERNET_OPTION_PER_CONNECTION_OPTION = 75,
+ INTERNET_OPTION_PROXY_SETTINGS_CHANGED = 95,
+ }
+
+ public enum INTERNET_PER_CONN_OPTION : uint
+ {
+ INTERNET_PER_CONN_FLAGS = 1,
+ INTERNET_PER_CONN_PROXY_SERVER = 2,
+ INTERNET_PER_CONN_PROXY_BYPASS = 3,
+ INTERNET_PER_CONN_AUTOCONFIG_URL = 4,
+ INTERNET_PER_CONN_AUTODISCOVERY_FLAGS = 5,
+ INTERNET_PER_CONN_FLAGS_UI = 10, // IE8+ - Included with Windows 7 and Server 2008 R2
+ }
+
+ [Flags]
+ public enum PER_CONN_FLAGS : uint
+ {
+ PROXY_TYPE_DIRECT = 0x00000001,
+ PROXY_TYPE_PROXY = 0x00000002,
+ PROXY_TYPE_AUTO_PROXY_URL = 0x00000004,
+ PROXY_TYPE_AUTO_DETECT = 0x00000008,
+ }
+ }
+
+ internal class NativeMethods
+ {
+ [DllImport("Wininet.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ public static extern bool InternetQueryOptionW(
+ IntPtr hInternet,
+ NativeHelpers.INTERNET_OPTION dwOption,
+ SafeMemoryBuffer lpBuffer,
+ ref UInt32 lpdwBufferLength);
+
+ [DllImport("Wininet.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ public static extern bool InternetSetOptionW(
+ IntPtr hInternet,
+ NativeHelpers.INTERNET_OPTION dwOption,
+ SafeMemoryBuffer lpBuffer,
+ UInt32 dwBufferLength);
+
+ [DllImport("Rasapi32.dll", CharSet = CharSet.Unicode)]
+ public static extern UInt32 RasValidateEntryNameW(
+ string lpszPhonebook,
+ string lpszEntry);
+ }
+
+ internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ public SafeMemoryBuffer() : base(true) { }
+ public SafeMemoryBuffer(int cb) : base(true)
+ {
+ base.SetHandle(Marshal.AllocHGlobal(cb));
+ }
+ public SafeMemoryBuffer(IntPtr handle) : base(true)
+ {
+ base.SetHandle(handle);
+ }
+
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
+ protected override bool ReleaseHandle()
+ {
+ Marshal.FreeHGlobal(handle);
+ return true;
+ }
+ }
+
+ 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 WinINetProxy
+ {
+ private string Connection;
+
+ public string AutoConfigUrl;
+ public bool AutoDetect;
+ public string Proxy;
+ public string ProxyBypass;
+
+ public WinINetProxy(string connection)
+ {
+ Connection = connection;
+ Refresh();
+ }
+
+ public static bool IsValidConnection(string name)
+ {
+ // RasValidateEntryName is used to verify is a name can be a valid phonebook entry. It returns 0 if no
+ // entry exists and 183 if it already exists. We just need to check if it returns 183 to verify the
+ // connection name.
+ return NativeMethods.RasValidateEntryNameW(null, name) == 183;
+ }
+
+ public void Refresh()
+ {
+ using (var connFlags = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_FLAGS_UI))
+ using (var autoConfigUrl = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_AUTOCONFIG_URL))
+ using (var server = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_SERVER))
+ using (var bypass = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_BYPASS))
+ {
+ NativeHelpers.INTERNET_PER_CONN_OPTIONW[] options = new NativeHelpers.INTERNET_PER_CONN_OPTIONW[]
+ {
+ connFlags, autoConfigUrl, server, bypass
+ };
+
+ try
+ {
+ QueryOption(options, Connection);
+ }
+ catch (Win32Exception e)
+ {
+ if (e.NativeErrorCode == 87) // ERROR_INVALID_PARAMETER
+ {
+ // INTERNET_PER_CONN_FLAGS_UI only works for IE8+, try the fallback in case we are still working
+ // with an ancient version.
+ connFlags.dwOption = NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_FLAGS;
+ QueryOption(options, Connection);
+ }
+ else
+ throw;
+ }
+
+ NativeHelpers.PER_CONN_FLAGS flags = (NativeHelpers.PER_CONN_FLAGS)connFlags.Value.dwValue;
+
+ AutoConfigUrl = flags.HasFlag(NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_AUTO_PROXY_URL)
+ ? Marshal.PtrToStringUni(autoConfigUrl.Value.pszValue) : null;
+ AutoDetect = flags.HasFlag(NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_AUTO_DETECT);
+ if (flags.HasFlag(NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_PROXY))
+ {
+ Proxy = Marshal.PtrToStringUni(server.Value.pszValue);
+ ProxyBypass = Marshal.PtrToStringUni(bypass.Value.pszValue);
+ }
+ else
+ {
+ Proxy = null;
+ ProxyBypass = null;
+ }
+ }
+ }
+
+ public void Set()
+ {
+ using (var connFlags = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_FLAGS_UI))
+ using (var autoConfigUrl = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_AUTOCONFIG_URL))
+ using (var server = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_SERVER))
+ using (var bypass = CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION.INTERNET_PER_CONN_PROXY_BYPASS))
+ {
+ List<NativeHelpers.INTERNET_PER_CONN_OPTIONW> options = new List<NativeHelpers.INTERNET_PER_CONN_OPTIONW>();
+
+ // PROXY_TYPE_DIRECT seems to always be set, need to verify
+ NativeHelpers.PER_CONN_FLAGS flags = NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_DIRECT;
+ if (AutoDetect)
+ flags |= NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_AUTO_DETECT;
+
+ if (!String.IsNullOrEmpty(AutoConfigUrl))
+ {
+ flags |= NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_AUTO_PROXY_URL;
+ autoConfigUrl.Value.pszValue = Marshal.StringToHGlobalUni(AutoConfigUrl);
+ }
+ options.Add(autoConfigUrl);
+
+ if (!String.IsNullOrEmpty(Proxy))
+ {
+ flags |= NativeHelpers.PER_CONN_FLAGS.PROXY_TYPE_PROXY;
+ server.Value.pszValue = Marshal.StringToHGlobalUni(Proxy);
+ }
+ options.Add(server);
+
+ if (!String.IsNullOrEmpty(ProxyBypass))
+ bypass.Value.pszValue = Marshal.StringToHGlobalUni(ProxyBypass);
+ options.Add(bypass);
+
+ connFlags.Value.dwValue = (UInt32)flags;
+ options.Add(connFlags);
+
+ SetOption(options.ToArray(), Connection);
+
+ // Tell IE that the proxy settings have been changed.
+ if (!NativeMethods.InternetSetOptionW(
+ IntPtr.Zero,
+ NativeHelpers.INTERNET_OPTION.INTERNET_OPTION_PROXY_SETTINGS_CHANGED,
+ new SafeMemoryBuffer(IntPtr.Zero),
+ 0))
+ {
+ throw new Win32Exception("InternetSetOptionW(INTERNET_OPTION_PROXY_SETTINGS_CHANGED) failed");
+ }
+ }
+ }
+
+ internal static NativeHelpers.INTERNET_PER_CONN_OPTIONW CreateConnOption(NativeHelpers.INTERNET_PER_CONN_OPTION option)
+ {
+ return new NativeHelpers.INTERNET_PER_CONN_OPTIONW
+ {
+ dwOption = option,
+ Value = new NativeHelpers.INTERNET_PER_CONN_OPTIONW.ValueUnion(),
+ };
+ }
+
+ internal static void QueryOption(NativeHelpers.INTERNET_PER_CONN_OPTIONW[] options, string connection = null)
+ {
+ using (NativeHelpers.INTERNET_PER_CONN_OPTION_LISTW optionList = new NativeHelpers.INTERNET_PER_CONN_OPTION_LISTW())
+ using (SafeMemoryBuffer optionListPtr = MarshalOptionList(optionList, options, connection))
+ {
+ UInt32 bufferSize = optionList.dwSize;
+ if (!NativeMethods.InternetQueryOptionW(
+ IntPtr.Zero,
+ NativeHelpers.INTERNET_OPTION.INTERNET_OPTION_PER_CONNECTION_OPTION,
+ optionListPtr,
+ ref bufferSize))
+ {
+ throw new Win32Exception("InternetQueryOptionW(INTERNET_OPTION_PER_CONNECTION_OPTION) failed");
+ }
+
+ for (int i = 0; i < options.Length; i++)
+ {
+ IntPtr opt = IntPtr.Add(optionList.pOptions, i * Marshal.SizeOf(typeof(NativeHelpers.INTERNET_PER_CONN_OPTIONW)));
+ NativeHelpers.INTERNET_PER_CONN_OPTIONW option = (NativeHelpers.INTERNET_PER_CONN_OPTIONW)Marshal.PtrToStructure(opt,
+ typeof(NativeHelpers.INTERNET_PER_CONN_OPTIONW));
+ options[i].Value = option.Value;
+ option.Value = null; // Stops the GC from freeing the same memory twice
+ }
+ }
+ }
+
+ internal static void SetOption(NativeHelpers.INTERNET_PER_CONN_OPTIONW[] options, string connection = null)
+ {
+ using (NativeHelpers.INTERNET_PER_CONN_OPTION_LISTW optionList = new NativeHelpers.INTERNET_PER_CONN_OPTION_LISTW())
+ using (SafeMemoryBuffer optionListPtr = MarshalOptionList(optionList, options, connection))
+ {
+ if (!NativeMethods.InternetSetOptionW(
+ IntPtr.Zero,
+ NativeHelpers.INTERNET_OPTION.INTERNET_OPTION_PER_CONNECTION_OPTION,
+ optionListPtr,
+ optionList.dwSize))
+ {
+ throw new Win32Exception("InternetSetOptionW(INTERNET_OPTION_PER_CONNECTION_OPTION) failed");
+ }
+ }
+ }
+
+ internal static SafeMemoryBuffer MarshalOptionList(NativeHelpers.INTERNET_PER_CONN_OPTION_LISTW optionList,
+ NativeHelpers.INTERNET_PER_CONN_OPTIONW[] options, string connection)
+ {
+ optionList.pszConnection = Marshal.StringToHGlobalUni(connection);
+ optionList.dwOptionCount = (UInt32)options.Length;
+
+ int optionSize = Marshal.SizeOf(typeof(NativeHelpers.INTERNET_PER_CONN_OPTIONW));
+ optionList.pOptions = Marshal.AllocHGlobal(optionSize * options.Length);
+ for (int i = 0; i < options.Length; i++)
+ {
+ IntPtr option = IntPtr.Add(optionList.pOptions, i * optionSize);
+ Marshal.StructureToPtr(options[i], option, false);
+ }
+
+ SafeMemoryBuffer optionListPtr = new SafeMemoryBuffer((int)optionList.dwSize);
+ Marshal.StructureToPtr(optionList, optionListPtr.DangerousGetHandle(), false);
+ return optionListPtr;
+ }
+ }
+}
+'@
+Add-CSharpType -References $win_inet_invoke -AnsibleModule $module
+
+# We need to validate the connection because WinINet will just silently continue even if the connection does not
+# already exist.
+if ($null -ne $connection -and -not [Ansible.WinINetProxy.WinINetProxy]::IsValidConnection($connection)) {
+ $module.FailJson("The connection '$connection' does not exist.")
+}
+
+$actual_proxy = New-Object -TypeName Ansible.WinINetProxy.WinINetProxy -ArgumentList @(, $connection)
+$module.Diff.before = @{
+ auto_config_url = $actual_proxy.AutoConfigUrl
+ auto_detect = $actual_proxy.AutoDetect
+ bypass = $actual_proxy.ProxyBypass
+ server = $actual_proxy.Proxy
+}
+
+# Make sure an empty string is converted to $null for easier comparisons
+if ([String]::IsNullOrEmpty($auto_config_url)) {
+ $auto_config_url = $null
+}
+if ([String]::IsNullOrEmpty($proxy)) {
+ $proxy = $null
+}
+if ([String]::IsNullOrEmpty($bypass)) {
+ $bypass = $null
+}
+
+# Record the original values in case we need to revert on a failure
+$previous_auto_config_url = $actual_proxy.AutoConfigUrl
+$previous_auto_detect = $actual_proxy.AutoDetect
+$previous_proxy = $actual_proxy.Proxy
+$previous_bypass = $actual_proxy.ProxyBypass
+
+$changed = $false
+if ($auto_config_url -ne $previous_auto_config_url) {
+ $actual_proxy.AutoConfigUrl = $auto_config_url
+ $changed = $true
+}
+
+if ($auto_detect -ne $previous_auto_detect) {
+ $actual_proxy.AutoDetect = $auto_detect
+ $changed = $true
+}
+
+if ($proxy -ne $previous_proxy) {
+ $actual_proxy.Proxy = $proxy
+ $changed = $true
+}
+
+if ($bypass -ne $previous_bypass) {
+ $actual_proxy.ProxyBypass = $bypass
+ $changed = $true
+}
+
+if ($changed -and -not $module.CheckMode) {
+ $actual_proxy.Set()
+
+ # Validate that the change was made correctly and revert if it wasn't. THe Set() method won't fail on invalid
+ # values so we need to check again to make sure all was good
+ $actual_proxy.Refresh()
+ if ($actual_proxy.AutoConfigUrl -ne $auto_config_url -or
+ $actual_proxy.AutoDetect -ne $auto_detect -or
+ $actual_proxy.Proxy -ne $proxy -or
+ $actual_proxy.ProxyBypass -ne $bypass) {
+
+ $actual_proxy.AutoConfigUrl = $previous_auto_config_url
+ $actual_proxy.AutoDetect = $previous_auto_detect
+ $actual_proxy.Proxy = $previous_proxy
+ $actual_proxy.ProxyBypass = $previous_bypass
+ $actual_proxy.Set()
+
+ $module.FailJson("Unknown error when trying to set auto_config_url '$auto_config_url', proxy '$proxy', or bypass '$bypass'")
+ }
+}
+$module.Result.changed = $changed
+
+$module.Diff.after = @{
+ auto_config_url = $auto_config_url
+ auto_detect = $auto_detect
+ bypass = $bypass
+ proxy = $proxy
+}
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_inet_proxy.py b/ansible_collections/community/windows/plugins/modules/win_inet_proxy.py
new file mode 100644
index 00000000..2810606b
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_inet_proxy.py
@@ -0,0 +1,171 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2019, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+
+DOCUMENTATION = r'''
+---
+module: win_inet_proxy
+short_description: Manages proxy settings for WinINet and Internet Explorer
+description:
+- Used to set or remove proxy settings for Windows INet which includes Internet
+ Explorer.
+- WinINet is a framework used by interactive applications to submit web
+ requests through.
+- The proxy settings can also be used by other applications like Firefox,
+ Chrome, and others but there is no definitive list.
+options:
+ auto_detect:
+ description:
+ - Whether to configure WinINet to automatically detect proxy settings
+ through Web Proxy Auto-Detection C(WPAD).
+ - This corresponds to the checkbox I(Automatically detect settings) in the
+ connection settings window.
+ default: yes
+ type: bool
+ auto_config_url:
+ description:
+ - The URL of a proxy configuration script.
+ - Proxy configuration scripts are typically JavaScript files with the
+ C(.pac) extension that implement the C(FindProxyForURL(url, host)
+ function.
+ - Omit, set to null or an empty string to remove the auto config URL.
+ - This corresponds to the checkbox I(Use automatic configuration script) in
+ the connection settings window.
+ type: str
+ bypass:
+ description:
+ - A list of hosts that will bypass the set proxy when being accessed.
+ - Use C(<local>) to match hostnames that are not fully qualified domain
+ names. This is useful when needing to connect to intranet sites using
+ just the hostname. If defined, this should be the last entry in the
+ bypass list.
+ - Use C(<-loopback>) to stop automatically bypassing the proxy when
+ connecting through any loopback address like C(127.0.0.1), C(localhost),
+ or the local hostname.
+ - Omit, set to null or an empty string/list to remove the bypass list.
+ - If this is set then I(proxy) must also be set.
+ type: list
+ elements: str
+ connection:
+ description:
+ - The name of the IE connection to set the proxy settings for.
+ - These are the connections under the I(Dial-up and Virtual Private Network)
+ header in the IE settings.
+ - When omitted, the default LAN connection is used.
+ type: str
+ proxy:
+ description:
+ - A string or dict that specifies the proxy to be set.
+ - If setting a string, should be in the form C(hostname), C(hostname:port),
+ or C(protocol=hostname:port).
+ - If the port is undefined, the default port for the protocol in use is
+ used.
+ - If setting a dict, the keys should be the protocol and the values should
+ be the hostname and/or port for that protocol.
+ - Valid protocols are C(http), C(https), C(ftp), and C(socks).
+ - Omit, set to null or an empty string to remove the proxy settings.
+ type: raw
+notes:
+- This is not the same as the proxy settings set in WinHTTP through the
+ C(netsh) command. Use the M(community.windows.win_http_proxy) module to manage that instead.
+- These settings are by default set per user and not system wide. A registry
+ property must be set independently from this module if you wish to apply the
+ proxy for all users. See examples for more detail.
+- If per user proxy settings are desired, use I(become) to become any local
+ user on the host. No password is needed to be set for this to work.
+- If the proxy requires authentication, set the credentials using the
+ M(community.windows.win_credential) module. This requires I(become) to be used so the
+ credential store can be accessed.
+seealso:
+- module: community.windows.win_http_proxy
+- module: community.windows.win_credential
+author:
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = r'''
+# This should be set before running the win_inet_proxy module
+- name: Configure IE proxy settings to apply to all users
+ ansible.windows.win_regedit:
+ path: HKLM:\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings
+ name: ProxySettingsPerUser
+ data: 0
+ type: dword
+ state: present
+
+# This should be set before running the win_inet_proxy module
+- name: Configure IE proxy settings to apply per user
+ ansible.windows.win_regedit:
+ path: HKLM:\SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings
+ name: ProxySettingsPerUser
+ data: 1
+ type: dword
+ state: present
+
+- name: Configure IE proxy to use auto detected settings without an explicit proxy
+ win_inet_proxy:
+ auto_detect: yes
+
+- name: Configure IE proxy to use auto detected settings with a configuration script
+ win_inet_proxy:
+ auto_detect: yes
+ auto_config_url: http://proxy.ansible.com/proxy.pac
+
+- name: Configure IE to use explicit proxy host
+ win_inet_proxy:
+ auto_detect: yes
+ proxy: ansible.proxy
+
+- name: Configure IE to use explicit proxy host with port and without auto detection
+ win_inet_proxy:
+ auto_detect: no
+ proxy: ansible.proxy:8080
+
+- name: Configure IE to use a specific proxy per protocol
+ win_inet_proxy:
+ proxy:
+ http: ansible.proxy:8080
+ https: ansible.proxy:8443
+
+- name: Configure IE to use a specific proxy per protocol using a string
+ win_inet_proxy:
+ proxy: http=ansible.proxy:8080;https=ansible.proxy:8443
+
+- name: Set a proxy with a bypass list
+ win_inet_proxy:
+ proxy: ansible.proxy
+ bypass:
+ - server1
+ - server2
+ - <-loopback>
+ - <local>
+
+- name: Remove any explicit proxies that are set
+ win_inet_proxy:
+ proxy: ''
+ bypass: ''
+
+# This should be done after setting the IE proxy with win_inet_proxy
+- name: Import IE proxy configuration to WinHTTP
+ win_http_proxy:
+ source: ie
+
+# Explicit credentials can only be set per user and require become to work
+- name: Set credential to use for proxy auth
+ win_credential:
+ name: ansible.proxy # The name should be the FQDN of the proxy host
+ type: generic_password
+ username: proxyuser
+ secret: proxypass
+ state: present
+ become: yes
+ become_user: '{{ ansible_user }}'
+ become_method: runas
+'''
+
+RETURN = r'''
+#
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_initialize_disk.ps1 b/ansible_collections/community/windows/plugins/modules/win_initialize_disk.ps1
new file mode 100644
index 00000000..21a6b980
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_initialize_disk.ps1
@@ -0,0 +1,163 @@
+#!powershell
+
+# Copyright: (c) 2019, Brant Evans <bevans@redhat.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#AnsibleRequires -OSVersion 6.2
+
+Set-StrictMode -Version 2
+
+$spec = @{
+ options = @{
+ disk_number = @{ type = "int" }
+ uniqueid = @{ type = "str" }
+ path = @{ type = "str" }
+ style = @{ type = "str"; choices = "gpt", "mbr"; default = "gpt" }
+ online = @{ type = "bool"; default = $true }
+ force = @{ type = "bool"; default = $false }
+ }
+ mutually_exclusive = @(
+ , @('disk_number', 'uniqueid', 'path')
+ )
+ required_one_of = @(
+ , @('disk_number', 'uniqueid', 'path')
+ )
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$disk_number = $module.Params.disk_number
+$uniqueid = $module.Params.uniqueid
+$path = $module.Params.path
+$partition_style = $module.Params.style
+$bring_online = $module.Params.online
+$force_init = $module.Params.force
+
+function Get-AnsibleDisk {
+ param(
+ $DiskNumber,
+ $UniqueId,
+ $Path
+ )
+
+ if ($null -ne $DiskNumber) {
+ try {
+ $disk = Get-Disk -Number $DiskNumber
+ }
+ catch {
+ $module.FailJson("There was an error retrieving the disk using disk_number $($DiskNumber): $($_.Exception.Message)")
+ }
+ }
+ elseif ($null -ne $UniqueId) {
+ try {
+ $disk = Get-Disk -UniqueId $UniqueId
+ }
+ catch {
+ $module.FailJson("There was an error retrieving the disk using id $($UniqueId): $($_.Exception.Message)")
+ }
+ }
+ elseif ($null -ne $Path) {
+ try {
+ $disk = Get-Disk -Path $Path
+ }
+ catch {
+ $module.FailJson("There was an error retrieving the disk using path $($Path): $($_.Exception.Message)")
+ }
+ }
+ else {
+ $module.FailJson("Unable to retrieve disk: disk_number, id, or path was not specified")
+ }
+
+ return $disk
+}
+
+function Initialize-AnsibleDisk {
+ param(
+ $AnsibleDisk,
+ $PartitionStyle
+ )
+
+ if ($AnsibleDisk.IsReadOnly) {
+ $module.FailJson("Unable to initialize disk as it is read-only")
+ }
+
+ $parameters = @{
+ Number = $AnsibleDisk.Number
+ PartitionStyle = $PartitionStyle
+ }
+
+ if (-Not $module.CheckMode) {
+ Initialize-Disk @parameters -Confirm:$false
+ }
+
+ $module.Result.changed = $true
+}
+
+function Clear-AnsibleDisk {
+ param(
+ $AnsibleDisk
+ )
+
+ $parameters = @{
+ Number = $AnsibleDisk.Number
+ }
+
+ if (-Not $module.CheckMode) {
+ Clear-Disk @parameters -RemoveData -RemoveOEM -Confirm:$false
+ }
+}
+
+function Set-AnsibleDisk {
+ param(
+ $AnsibleDisk,
+ $BringOnline
+ )
+
+ $refresh_disk_status = $false
+
+ if ($BringOnline) {
+ if (-Not $module.CheckMode) {
+ if ($AnsibleDisk.IsOffline) {
+ Set-Disk -Number $AnsibleDisk.Number -IsOffline:$false
+ $refresh_disk_status = $true
+ }
+
+ if ($AnsibleDisk.IsReadOnly) {
+ Set-Disk -Number $AnsibleDisk.Number -IsReadOnly:$false
+ $refresh_disk_status = $true
+ }
+ }
+ }
+
+ if ($refresh_disk_status) {
+ $AnsibleDisk = Get-AnsibleDisk -DiskNumber $AnsibleDisk.Number
+ }
+
+ return $AnsibleDisk
+}
+
+$ansible_disk = Get-AnsibleDisk -DiskNumber $disk_number -UniqueId $uniqueid -Path $path
+$ansible_part_style = $ansible_disk.PartitionStyle
+
+if ("RAW" -eq $ansible_part_style) {
+ $ansible_disk = Set-AnsibleDisk -AnsibleDisk $ansible_disk -BringOnline $bring_online
+ Initialize-AnsibleDisk -AnsibleDisk $ansible_disk -PartitionStyle $partition_style
+}
+else {
+ if (($ansible_part_style -ne $partition_style.ToUpper()) -And -Not $force_init) {
+ $msg = -join @(
+ "Force initialization must be specified since the target partition style: $($partition_style.ToLower()) "
+ "is different from the current partition style: $($ansible_part_style.ToLower())"
+ )
+ $module.FailJson($msg)
+ }
+ elseif ($force_init) {
+ $ansible_disk = Set-AnsibleDisk -AnsibleDisk $ansible_disk -BringOnline $bring_online
+ Clear-AnsibleDisk -AnsibleDisk $ansible_disk
+ if ( $bring_online ) { Initialize-AnsibleDisk -AnsibleDisk $ansible_disk -PartitionStyle $partition_style }
+ }
+}
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_initialize_disk.py b/ansible_collections/community/windows/plugins/modules/win_initialize_disk.py
new file mode 100644
index 00000000..485b4a51
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_initialize_disk.py
@@ -0,0 +1,75 @@
+#!/usr/bin/python
+
+# Copyright: (c) 2019, Brant Evans <bevans@redhat.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = '''
+---
+module: win_initialize_disk
+short_description: Initializes disks on Windows Server
+description:
+ - "The M(community.windows.win_initialize_disk) module initializes disks"
+options:
+ disk_number:
+ description:
+ - Used to specify the disk number of the disk to be initialized.
+ type: int
+ uniqueid:
+ description:
+ - Used to specify the uniqueid of the disk to be initialized.
+ type: str
+ path:
+ description:
+ - Used to specify the path to the disk to be initialized.
+ type: str
+ style:
+ description:
+ - The partition style to use for the disk. Valid options are mbr or gpt.
+ type: str
+ choices: [ gpt, mbr ]
+ default: gpt
+ online:
+ description:
+ - If the disk is offline and/or readonly update the disk to be online and not readonly.
+ type: bool
+ default: true
+ force:
+ description:
+ - Specify if initializing should be forced for disks that are already initialized.
+ type: bool
+ default: no
+
+notes:
+ - One of three parameters (I(disk_number), I(uniqueid), and I(path)) are mandatory to identify the target disk, but
+ more than one cannot be specified at the same time.
+ - A minimum Operating System Version of Server 2012 or Windows 8 is required to use this module.
+ - This module is idempotent if I(force) is not specified.
+
+seealso:
+ - module: community.windows.win_disk_facts
+ - module: community.windows.win_partition
+ - module: community.windows.win_format
+
+author:
+ - Brant Evans (@branic)
+'''
+
+EXAMPLES = '''
+- name: Initialize a disk
+ community.windows.win_initialize_disk:
+ disk_number: 1
+
+- name: Initialize a disk with an MBR partition style
+ community.windows.win_initialize_disk:
+ disk_number: 1
+ style: mbr
+
+- name: Forcefully initiallize a disk
+ community.windows.win_initialize_disk:
+ disk_number: 2
+ force: yes
+'''
+
+RETURN = '''
+#
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_lineinfile.ps1 b/ansible_collections/community/windows/plugins/modules/win_lineinfile.ps1
new file mode 100644
index 00000000..25af6782
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_lineinfile.ps1
@@ -0,0 +1,483 @@
+#!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 -LiteralPath $temppath -force
+ Fail-Json @{} "failed to validate $cmdname $cmdargs with error: $output $error"
+ }
+
+ }
+
+ # Commit changes to the path
+ $cleanpath = $path.Replace("/", "\")
+ Try {
+ Copy-Item -LiteralPath $temppath -Destination $cleanpath -Force -WhatIf:$check_mode
+ }
+ Catch {
+ Fail-Json @{} "Cannot write to: $cleanpath ($($_.Exception.Message))"
+ }
+
+ Try {
+ Remove-Item -LiteralPath $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 {
+ param (
+ $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("/", "\")
+ $endswithnewline = $null
+
+ # 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)
+ $endswithnewline = $false
+ }
+
+ # Initialize result information
+ $result = @{
+ backup = ""
+ changed = $false
+ msg = ""
+ }
+
+ If ($insertbefore -and $insertafter) {
+ Add-Warning $result "Both insertbefore and insertafter parameters found, ignoring `"insertafter=$insertafter`""
+ }
+
+ # 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 ($null -eq $endswithnewline ) {
+ $alltext = [System.IO.File]::ReadAllText($cleanpath, $encodingobj)
+ $endswithnewline = (($alltext[-1] -eq "`n") -or ($alltext[-1] -eq "`r"))
+ }
+ }
+
+ if ($diff_support) {
+ if ($endswithnewline) {
+ $before += ""
+ }
+ $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
+ }
+
+ if ($endswithnewline) {
+ $lines.Add("")
+ }
+
+ $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
+ $alltext = [System.IO.File]::ReadAllText($cleanpath, $encodingobj)
+ If (($alltext[-1] -eq "`n") -or ($alltext[-1] -eq "`r")) {
+ $lines.Add("")
+ $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 (-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/ansible_collections/community/windows/plugins/modules/win_lineinfile.py b/ansible_collections/community/windows/plugins/modules/win_lineinfile.py
new file mode 100644
index 00000000..64b5a7d4
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_lineinfile.py
@@ -0,0 +1,171 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+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.
+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.
+ 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
+seealso:
+- module: ansible.builtin.assemble
+- module: ansible.builtin.lineinfile
+author:
+- Brian Lloyd (@brianlloyd)
+'''
+
+EXAMPLES = r'''
+- name: Insert path without converting \r\n
+ community.windows.win_lineinfile:
+ path: c:\file.txt
+ line: c:\return\new
+
+- community.windows.win_lineinfile:
+ path: C:\Temp\example.conf
+ regex: '^name='
+ line: 'name=JohnDoe'
+
+- community.windows.win_lineinfile:
+ path: C:\Temp\example.conf
+ regex: '^name='
+ state: absent
+
+- community.windows.win_lineinfile:
+ path: C:\Temp\example.conf
+ regex: '^127\.0\.0\.1'
+ line: '127.0.0.1 localhost'
+
+- community.windows.win_lineinfile:
+ path: C:\Temp\httpd.conf
+ regex: '^Listen '
+ insertafter: '^#Listen '
+ line: Listen 8080
+
+- community.windows.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
+ community.windows.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
+ community.windows.win_lineinfile:
+ path: C:\Temp\testfile.txt
+ line: Line added to file
+ newline: unix
+
+- name: Update a line using backrefs
+ community.windows.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/ansible_collections/community/windows/plugins/modules/win_listen_ports_facts.ps1 b/ansible_collections/community/windows/plugins/modules/win_listen_ports_facts.ps1
new file mode 100644
index 00000000..4ad3d68d
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_listen_ports_facts.ps1
@@ -0,0 +1,90 @@
+#!powershell
+
+# Copyright: (c) 2022, DataDope (@datadope-io)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+$spec = @{
+ options = @{
+ date_format = @{ type = 'str'; default = '%c' }
+ tcp_filter = @{ type = 'list'; elements = 'str'; default = 'Listen' }
+ }
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$date_format = $module.Params.date_format
+$tcp_filter = $module.Params.tcp_filter
+
+# Structure of the response the script will return
+$ansibleFacts = @{
+ tcp_listen = @()
+ udp_listen = @()
+}
+
+# Build an index of the processes based on the PID
+$processes = @{}
+Get-CimInstance -ClassName Win32_Process | ForEach-Object {
+ $processes[[int]$_.ProcessId] = $_
+}
+
+
+# Format the given date with the same format as listen_port_facts stime (Date and time - abbreviated) by default, or
+# with the given format
+function Format-Date {
+ param (
+ $date
+ )
+
+ if ($null -ne $date) {
+ $date = Get-Date $date -UFormat $date_format
+ }
+
+ return $date
+}
+
+# Return the processed listener and the associated PID data
+function Build-Listener {
+ param (
+ $listener,
+ $type
+ )
+
+ $process = $processes[[int]$listener.OwningProcess]
+ $process_owner = Invoke-CimMethod -InputObject $process -MethodName GetOwner
+
+ $owner = $null
+ if ($null -ne $process_owner.User -and $null -ne $process_owner.Domain) {
+ $owner = $process_owner.Domain + '\' + $process_owner.User
+ }
+
+ return @{
+ address = $listener.LocalAddress
+ name = $process.Name
+ pid = $listener.OwningProcess
+ port = $listener.LocalPort
+ protocol = $type
+ stime = Format-Date $process.CreationDate
+ user = $owner
+ }
+}
+
+try {
+ # Retrieve the information of the TCP ports with Listen status by default, or with the given state/s
+ Get-NetTCPConnection -State $tcp_filter -ErrorAction SilentlyContinue | Foreach-Object {
+ $ansibleFacts.tcp_listen += Build-Listener $_ "tcp"
+ }
+
+ # Retrieve the information of the UDP ports
+ Get-NetUDPEndpoint | Foreach-Object {
+ $ansibleFacts.udp_listen += Build-Listener $_ "udp"
+ }
+}
+catch {
+ $module.FailJson("An error occurred while retrieving ports facts: $($_.Exception.Message)", $_)
+}
+
+$module.Result.ansible_facts = $ansibleFacts
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_listen_ports_facts.py b/ansible_collections/community/windows/plugins/modules/win_listen_ports_facts.py
new file mode 100644
index 00000000..89d51be8
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_listen_ports_facts.py
@@ -0,0 +1,95 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2022, DataDope (@datadope-io)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_listen_ports_facts
+version_added: '1.10.0'
+short_description: Recopilates the facts of the listening ports of the machine
+description:
+ - Recopilates the information of the TCP and UDP ports of the machine and
+ the related processes.
+ - State of the TCP ports could be filtered, as well as the format of the
+ date when the parent process was launched.
+ - The module's goal is to replicate the functionality of the linux module
+ listen_ports_facts, mantaining the format of the said module.
+options:
+ date_format:
+ description:
+ - The format of the date when the process that owns the port started.
+ - The date specification is UFormat
+ type: str
+ default: '%c'
+ tcp_filter:
+ description:
+ - Filter for the state of the TCP ports that will be recopilated.
+ - Supports multiple states (Bound, Closed, CloseWait, Closing, DeleteTCB,
+ Established, FinWait1, FinWait2, LastAck, Listen, SynReceived, SynSent
+ and TimeWait), that can be used alone or combined. Note that the Bound
+ state is only available on PowerShell version 4.0 or later.
+ type: list
+ elements: str
+ default: [ Listen ]
+notes:
+- The generated data (tcp_listen and udp_listen) and the fields within follows
+ the listen_ports_facts schema to achieve compatibility with the said module
+ output, even though this module if capable of extracting ports with a state
+ other than Listen
+seealso:
+- module: community.general.listen_ports_facts
+author:
+- David Nieto (@david-ns)
+'''
+
+EXAMPLES = r'''
+- name: Recopilate ports facts
+ community.windows.win_listen_ports_facts:
+
+- name: Retrieve only ports with Closing and Established states
+ community.windows.win_listen_ports_facts:
+ tcp_filter:
+ - Closing
+ - Established
+
+- name: Get ports facts with only the year within the date field
+ community.windows.win_listen_ports_facts:
+ date_format: '%Y'
+'''
+
+RETURN = r'''
+tcp_listen:
+ description: List of dicts with the detected TCP ports
+ returned: success
+ type: list
+ elements: dict
+ sample: [
+ {
+ "address": "127.0.0.1",
+ "name": "python",
+ "pid": 5332,
+ "port": 82,
+ "protocol": "tcp",
+ "stime": "Thu Nov 18 15:27:42 2021",
+ "user": "SERVER\\Administrator"
+ }
+ ]
+udp_listen:
+ description: List of dicts with the detected UDP ports
+ returned: success
+ type: list
+ elements: dict
+ sample: [
+ {
+ "address": "127.0.0.1",
+ "name": "python",
+ "pid": 5332,
+ "port": 82,
+ "protocol": "udp",
+ "stime": "Thu Nov 18 15:27:42 2021",
+ "user": "SERVER\\Administrator"
+ }
+ ]
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_mapped_drive.ps1 b/ansible_collections/community/windows/plugins/modules/win_mapped_drive.ps1
new file mode 100644
index 00000000..210ae223
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_mapped_drive.ps1
@@ -0,0 +1,449 @@
+#!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.AccessToken
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#Requires -Module Ansible.ModuleUtils.AddType
+
+$spec = @{
+ options = @{
+ letter = @{ type = "str"; required = $true }
+ path = @{ type = "path"; }
+ state = @{ type = "str"; default = "present"; choices = @("absent", "present") }
+ username = @{ type = "str" }
+ password = @{ type = "str"; no_log = $true }
+ }
+ required_if = @(
+ , @("state", "present", @("path"))
+ )
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$letter = $module.Params.letter
+$path = $module.Params.path
+$state = $module.Params.state
+$username = $module.Params.username
+$password = $module.Params.password
+
+if ($letter -notmatch "^[a-zA-z]{1}$") {
+ $module.FailJson("letter must be a single letter from A-Z, was: $letter")
+}
+$letter_root = "$($letter):"
+
+$module.Diff.before = ""
+$module.Diff.after = ""
+
+Add-CSharpType -AnsibleModule $module -References @'
+using Microsoft.Win32.SafeHandles;
+using System;
+using System.Collections.Generic;
+using System.Runtime.ConstrainedExecution;
+using System.Runtime.InteropServices;
+
+namespace Ansible.MappedDrive
+{
+ internal class NativeHelpers
+ {
+ public enum ResourceScope : uint
+ {
+ Connected = 0x00000001,
+ GlobalNet = 0x00000002,
+ Remembered = 0x00000003,
+ Recent = 0x00000004,
+ Context = 0x00000005,
+ }
+
+ [Flags]
+ public enum ResourceType : uint
+ {
+ Any = 0x0000000,
+ Disk = 0x00000001,
+ Print = 0x00000002,
+ Reserved = 0x00000008,
+ Unknown = 0xFFFFFFFF,
+ }
+
+ public enum CloseFlags : uint
+ {
+ None = 0x00000000,
+ UpdateProfile = 0x00000001,
+ }
+
+ [Flags]
+ public enum AddFlags : uint
+ {
+ UpdateProfile = 0x00000001,
+ UpdateRecent = 0x00000002,
+ Temporary = 0x00000004,
+ Interactive = 0x00000008,
+ Prompt = 0x00000010,
+ Redirect = 0x00000080,
+ CurrentMedia = 0x00000200,
+ CommandLine = 0x00000800,
+ CmdSaveCred = 0x00001000,
+ CredReset = 0x00002000,
+ }
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ public struct NETRESOURCEW
+ {
+ public ResourceScope dwScope;
+ public ResourceType dwType;
+ public UInt32 dwDisplayType;
+ public UInt32 dwUsage;
+ [MarshalAs(UnmanagedType.LPWStr)] public string lpLocalName;
+ [MarshalAs(UnmanagedType.LPWStr)] public string lpRemoteName;
+ [MarshalAs(UnmanagedType.LPWStr)] public string lpComment;
+ [MarshalAs(UnmanagedType.LPWStr)] public string lpProvider;
+ }
+ }
+
+ internal class NativeMethods
+ {
+ [DllImport("kernel32.dll", SetLastError = true)]
+ public static extern bool CloseHandle(
+ IntPtr hObject);
+
+ [DllImport("advapi32.dll", SetLastError = true)]
+ public static extern bool ImpersonateLoggedOnUser(
+ IntPtr hToken);
+
+ [DllImport("advapi32.dll", SetLastError = true)]
+ public static extern bool RevertToSelf();
+
+ [DllImport("Mpr.dll", CharSet = CharSet.Unicode)]
+ public static extern UInt32 WNetAddConnection2W(
+ NativeHelpers.NETRESOURCEW lpNetResource,
+ [MarshalAs(UnmanagedType.LPWStr)] string lpPassword,
+ [MarshalAs(UnmanagedType.LPWStr)] string lpUserName,
+ NativeHelpers.AddFlags dwFlags);
+
+ [DllImport("Mpr.dll", CharSet = CharSet.Unicode)]
+ public static extern UInt32 WNetCancelConnection2W(
+ [MarshalAs(UnmanagedType.LPWStr)] string lpName,
+ NativeHelpers.CloseFlags dwFlags,
+ bool fForce);
+
+ [DllImport("Mpr.dll")]
+ public static extern UInt32 WNetCloseEnum(
+ IntPtr hEnum);
+
+ [DllImport("Mpr.dll", CharSet = CharSet.Unicode)]
+ public static extern UInt32 WNetEnumResourceW(
+ IntPtr hEnum,
+ ref Int32 lpcCount,
+ SafeMemoryBuffer lpBuffer,
+ ref UInt32 lpBufferSize);
+
+ [DllImport("Mpr.dll", CharSet = CharSet.Unicode)]
+ public static extern UInt32 WNetOpenEnumW(
+ NativeHelpers.ResourceScope dwScope,
+ NativeHelpers.ResourceType dwType,
+ UInt32 dwUsage,
+ IntPtr lpNetResource,
+ out IntPtr lphEnum);
+ }
+
+ internal class SafeMemoryBuffer : SafeHandleZeroOrMinusOneIsInvalid
+ {
+ public SafeMemoryBuffer() : base(true) { }
+ public SafeMemoryBuffer(int cb) : base(true)
+ {
+ base.SetHandle(Marshal.AllocHGlobal(cb));
+ }
+ public SafeMemoryBuffer(IntPtr handle) : base(true)
+ {
+ base.SetHandle(handle);
+ }
+
+ [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
+ protected override bool ReleaseHandle()
+ {
+ Marshal.FreeHGlobal(handle);
+ return true;
+ }
+ }
+
+ internal class Impersonation : IDisposable
+ {
+ private IntPtr hToken = IntPtr.Zero;
+
+ public Impersonation(IntPtr token)
+ {
+ hToken = token;
+ if (token != IntPtr.Zero)
+ if (!NativeMethods.ImpersonateLoggedOnUser(hToken))
+ throw new Win32Exception("Failed to impersonate token with ImpersonateLoggedOnUser()");
+ }
+
+ public void Dispose()
+ {
+ if (hToken != null)
+ NativeMethods.RevertToSelf();
+ GC.SuppressFinalize(this);
+ }
+ ~Impersonation() { Dispose(); }
+ }
+
+ public class DriveInfo
+ {
+ public string Drive;
+ public string Path;
+ }
+
+ 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 Utils
+ {
+ private const UInt32 ERROR_SUCCESS = 0x00000000;
+ private const UInt32 ERROR_NO_MORE_ITEMS = 0x0000103;
+
+ public static void AddMappedDrive(string drive, string path, IntPtr iToken, string username = null, string password = null)
+ {
+ NativeHelpers.NETRESOURCEW resource = new NativeHelpers.NETRESOURCEW
+ {
+ dwType = NativeHelpers.ResourceType.Disk,
+ lpLocalName = drive,
+ lpRemoteName = path,
+ };
+ NativeHelpers.AddFlags dwFlags = NativeHelpers.AddFlags.UpdateProfile;
+ // While WNetAddConnection2W supports user/pass, this is only used for the first connection and the
+ // password is not remembered. We will delete the username mapping afterwards as it interferes with
+ // the implicit credential cache used in Windows
+ using (Impersonation imp = new Impersonation(iToken))
+ {
+ UInt32 res = NativeMethods.WNetAddConnection2W(resource, password, username, dwFlags);
+ if (res != ERROR_SUCCESS)
+ throw new Win32Exception((int)res, String.Format("Failed to map {0} to '{1}' with WNetAddConnection2W()", drive, path));
+ }
+ }
+
+ public static List<DriveInfo> GetMappedDrives(IntPtr iToken)
+ {
+ using (Impersonation imp = new Impersonation(iToken))
+ {
+ IntPtr enumPtr = IntPtr.Zero;
+ UInt32 res = NativeMethods.WNetOpenEnumW(NativeHelpers.ResourceScope.Remembered, NativeHelpers.ResourceType.Disk,
+ 0, IntPtr.Zero, out enumPtr);
+ if (res != ERROR_SUCCESS)
+ throw new Win32Exception((int)res, "WNetOpenEnumW()");
+
+ List<DriveInfo> resources = new List<DriveInfo>();
+ try
+ {
+ // MS recommend a buffer size of 16 KiB
+ UInt32 bufferSize = 16384;
+ int lpcCount = -1;
+
+ // keep iterating the enum until ERROR_NO_MORE_ITEMS is returned
+ do
+ {
+ using (SafeMemoryBuffer buffer = new SafeMemoryBuffer((int)bufferSize))
+ {
+ res = NativeMethods.WNetEnumResourceW(enumPtr, ref lpcCount, buffer, ref bufferSize);
+ if (res == ERROR_NO_MORE_ITEMS)
+ continue;
+ else if (res != ERROR_SUCCESS)
+ throw new Win32Exception((int)res, "WNetEnumResourceW()");
+ lpcCount = lpcCount < 0 ? 0 : lpcCount;
+
+ NativeHelpers.NETRESOURCEW[] rawResources = new NativeHelpers.NETRESOURCEW[lpcCount];
+ PtrToStructureArray(rawResources, buffer.DangerousGetHandle());
+ foreach (NativeHelpers.NETRESOURCEW resource in rawResources)
+ {
+ DriveInfo currentDrive = new DriveInfo
+ {
+ Drive = resource.lpLocalName,
+ Path = resource.lpRemoteName,
+ };
+ resources.Add(currentDrive);
+ }
+ }
+ }
+ while (res != ERROR_NO_MORE_ITEMS);
+ }
+ finally
+ {
+ NativeMethods.WNetCloseEnum(enumPtr);
+ }
+
+ return resources;
+ }
+ }
+
+ public static void RemoveMappedDrive(string drive, IntPtr iToken)
+ {
+ using (Impersonation imp = new Impersonation(iToken))
+ {
+ UInt32 res = NativeMethods.WNetCancelConnection2W(drive, NativeHelpers.CloseFlags.UpdateProfile, true);
+ if (res != ERROR_SUCCESS)
+ throw new Win32Exception((int)res, String.Format("Failed to remove mapped drive {0} with WNetCancelConnection2W()", drive));
+ }
+ }
+
+ private static void PtrToStructureArray<T>(T[] array, IntPtr ptr)
+ {
+ IntPtr ptrOffset = ptr;
+ for (int i = 0; i < array.Length; i++, ptrOffset = IntPtr.Add(ptrOffset, Marshal.SizeOf(typeof(T))))
+ array[i] = (T)Marshal.PtrToStructure(ptrOffset, typeof(T));
+ }
+ }
+}
+'@
+
+Function Get-LimitedToken {
+ $h_process = [Ansible.AccessToken.TokenUtil]::OpenProcess()
+ $h_token = [Ansible.AccessToken.TokenUtil]::OpenProcessToken($h_process, "Duplicate, Query")
+
+ try {
+ # If we don't have a Full token, we don't need to get the limited one to set a mapped drive
+ $tet = [Ansible.AccessToken.TokenUtil]::GetTokenElevationType($h_token)
+ if ($tet -ne [Ansible.AccessToken.TokenElevationType]::Full) {
+ return
+ }
+
+ foreach ($system_token in [Ansible.AccessToken.TokenUtil]::EnumerateUserTokens("S-1-5-18", "Duplicate")) {
+ # To get the TokenLinkedToken we need the SeTcbPrivilege, not all SYSTEM tokens have this assigned so
+ # we need to check before impersonating that token
+ $token_privileges = [Ansible.AccessToken.TokenUtil]::GetTokenPrivileges($system_token)
+ if ($null -eq ($token_privileges | Where-Object { $_.Name -eq "SeTcbPrivilege" })) {
+ continue
+ }
+
+ [Ansible.AccessToken.TokenUtil]::ImpersonateToken($system_token)
+ try {
+ return [Ansible.AccessToken.TokenUtil]::GetTokenLinkedToken($h_token)
+ }
+ finally {
+ [Ansible.AccessToken.TokenUtil]::RevertToSelf()
+ }
+ }
+ }
+ finally {
+ $h_token.Dispose()
+ }
+}
+
+<#
+When we run with become and UAC is enabled, the become process will most likely be the Admin/Full token. This is
+an issue with the WNetConnection APIs as the Full token is unable to add/enumerate/remove connections due to
+Windows storing the connection details on each token session ID. Unless EnabledLinkedConnections (reg key) is
+set to 1, the Full token is unable to manage connections in a persisted way whereas the Limited token is. This
+is similar to running 'net use' normally and an admin process is unable to see those and vice versa.
+
+To overcome this problem, we attempt to get a handle on the Limited token for the current logon and impersonate
+that before making any WNetConnection calls. If the token is not split, or we are already running on the Limited
+token then no impersonatoin is used/required. This allows the module to run with become (required to access the
+credential store) but still be able to manage the mapped connections.
+
+These are the following scenarios we have to handle;
+
+ 1. Run without become
+ A network logon is usually not split so GetLimitedToken() will return $null and no impersonation is needed
+ 2. Run with become on admin user with admin priv
+ We will have a Full token, GetLimitedToken() will return the limited token and impersonation is used
+ 3. Run with become on admin user without admin priv
+ We are already running with a Limited token, GetLimitedToken() return $nul and no impersonation is needed
+ 4. Run with become on standard user
+ There's no split token, GetLimitedToken() will return $null and no impersonation is needed
+#>
+$impersonation_token = Get-LimitedToken
+
+try {
+ $i_token_ptr = [System.IntPtr]::Zero
+ if ($null -ne $impersonation_token) {
+ $i_token_ptr = $impersonation_token.DangerousGetHandle()
+ }
+
+ $existing_targets = [Ansible.MappedDrive.Utils]::GetMappedDrives($i_token_ptr)
+ $existing_target = $existing_targets | Where-Object { $_.Drive -eq $letter_root }
+
+ if ($existing_target) {
+ $module.Diff.before = @{
+ letter = $letter
+ path = $existing_target.Path
+ }
+ }
+
+ if ($state -eq "absent") {
+ if ($null -ne $existing_target) {
+ if ($null -ne $path -and $existing_target.Path -ne $path) {
+ $module.FailJson("did not delete mapped drive $letter, the target path is pointing to a different location at $( $existing_target.Path )")
+ }
+ if (-not $module.CheckMode) {
+ [Ansible.MappedDrive.Utils]::RemoveMappedDrive($letter_root, $i_token_ptr)
+ }
+
+ $module.Result.changed = $true
+ }
+ }
+ else {
+ $physical_drives = Get-PSDrive -PSProvider "FileSystem"
+ if ($letter -in $physical_drives.Name) {
+ $module.FailJson("failed to create mapped drive $letter, this letter is in use and is pointing to a non UNC path")
+ }
+
+ # PowerShell converts a $null value to "" when crossing the .NET marshaler, we need to convert the input
+ # to a missing value so it uses the defaults. We also need to Invoke it with MethodInfo.Invoke so the defaults
+ # are still used
+ $input_username = $username
+ if ($null -eq $username) {
+ $input_username = [Type]::Missing
+ }
+ $input_password = $password
+ if ($null -eq $password) {
+ $input_password = [Type]::Missing
+ }
+ $add_method = [Ansible.MappedDrive.Utils].GetMethod("AddMappedDrive")
+
+ if ($null -ne $existing_target) {
+ if ($existing_target.Path -ne $path) {
+ if (-not $module.CheckMode) {
+ [Ansible.MappedDrive.Utils]::RemoveMappedDrive($letter_root, $i_token_ptr)
+ $add_method.Invoke($null, [Object[]]@($letter_root, $path, $i_token_ptr, $input_username, $input_password))
+ }
+ $module.Result.changed = $true
+ }
+ }
+ else {
+ if (-not $module.CheckMode) {
+ $add_method.Invoke($null, [Object[]]@($letter_root, $path, $i_token_ptr, $input_username, $input_password))
+ }
+
+ $module.Result.changed = $true
+ }
+
+ # If username was set and we made a change, remove the UserName value so Windows will continue to use the cred
+ # cache. If we don't do this then the drive will fail to map in the future as WNetAddConnection does not cache
+ # the password and relies on the credential store.
+ if ($null -ne $username -and $module.Result.changed -and -not $module.CheckMode) {
+ Set-ItemProperty -LiteralPath HKCU:\Network\$letter -Name UserName -Value "" -WhatIf:$module.CheckMode
+ }
+
+ $module.Diff.after = @{
+ letter = $letter
+ path = $path
+ }
+ }
+}
+finally {
+ if ($null -ne $impersonation_token) {
+ $impersonation_token.Dispose()
+ }
+}
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_mapped_drive.py b/ansible_collections/community/windows/plugins/modules/win_mapped_drive.py
new file mode 100644
index 00000000..2762e2b2
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_mapped_drive.py
@@ -0,0 +1,147 @@
+#!/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)
+
+DOCUMENTATION = r'''
+---
+module: win_mapped_drive
+short_description: Map network drives for users
+description:
+- Allows you to modify mapped network drives for individual users.
+- Also support WebDAV endpoints in the UNC form.
+options:
+ letter:
+ description:
+ - The letter of the network path to map to.
+ - This letter must not already be in use with Windows.
+ type: str
+ required: yes
+ password:
+ description:
+ - The password for C(username) that is used when testing the initial
+ connection.
+ - This is never saved with a mapped drive, use the M(community.windows.win_credential) module
+ to persist a username and password for a host.
+ type: str
+ path:
+ description:
+ - The UNC path to map the drive to.
+ - If pointing to a WebDAV location this must still be in a UNC path in the
+ format C(\\hostname\path) and not a URL, see examples for more details.
+ - To specify a C(https) WebDAV path, add C(@SSL) after the hostname. To
+ specify a custom WebDAV port add C(@<port num>) after the C(@SSL) or
+ hostname portion of the UNC path, e.g. C(\\server@SSL@1234) or
+ C(\\server@1234).
+ - This is required if C(state=present).
+ - If C(state=absent) and I(path) is not set, the module will delete the
+ mapped drive regardless of the target.
+ - If C(state=absent) and the I(path) is set, the module will throw an error
+ if path does not match the target of the mapped drive.
+ type: path
+ state:
+ description:
+ - If C(present) will ensure the mapped drive exists.
+ - If C(absent) will ensure the mapped drive does not exist.
+ type: str
+ choices: [ absent, present ]
+ default: present
+ username:
+ description:
+ - The username that is used when testing the initial connection.
+ - This is never saved with a mapped drive, the M(community.windows.win_credential) module
+ to persist a username and password for a host.
+ - This is required if the mapped drive requires authentication with
+ custom credentials and become, or CredSSP cannot be used.
+ - If become or CredSSP is used, any credentials saved with
+ M(community.windows.win_credential) will automatically be used instead.
+ type: str
+notes:
+- You cannot use this module to access a mapped drive in another Ansible task,
+ drives mapped with this module are only accessible when logging in
+ interactively with the user through the console or RDP.
+- It is recommend to run this module with become or CredSSP when the remote
+ path requires authentication.
+- When using become or CredSSP, the task will have access to any local
+ credentials stored in the user's vault.
+- If become or CredSSP is not available, the I(username) and I(password)
+ options can be used for the initial authentication but these are not
+ persisted.
+- WebDAV paths must have the WebDAV client feature installed for this module to
+ map those paths. This is installed by default on desktop Windows editions but
+ Windows Server hosts need to install the C(WebDAV-Redirector) feature using
+ M(ansible.windows.win_feature).
+seealso:
+- module: community.windows.win_credential
+author:
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = r'''
+- name: Create a mapped drive under Z
+ community.windows.win_mapped_drive:
+ letter: Z
+ path: \\domain\appdata\accounting
+
+- name: Delete any mapped drives under Z
+ community.windows.win_mapped_drive:
+ letter: Z
+ state: absent
+
+- name: Only delete the mapped drive Z if the paths match (error is thrown otherwise)
+ community.windows.win_mapped_drive:
+ letter: Z
+ path: \\domain\appdata\accounting
+ state: absent
+
+- name: Create mapped drive with credentials and save the username and password
+ block:
+ - name: Save the network credentials required for the mapped drive
+ community.windows.win_credential:
+ name: server
+ type: domain_password
+ username: username@DOMAIN
+ secret: Password01
+ state: present
+
+ - name: Create a mapped drive that requires authentication
+ community.windows.win_mapped_drive:
+ letter: M
+ path: \\SERVER\C$
+ state: present
+ vars:
+ # become is required to save and retrieve the credentials in the tasks
+ ansible_become: yes
+ ansible_become_method: runas
+ ansible_become_user: '{{ ansible_user }}'
+ ansible_become_pass: '{{ ansible_password }}'
+
+- name: Create mapped drive with credentials that do not persist on the next logon
+ community.windows.win_mapped_drive:
+ letter: M
+ path: \\SERVER\C$
+ state: present
+ username: '{{ ansible_user }}'
+ password: '{{ ansible_password }}'
+
+# This should only be required for Windows Server OS'
+- name: Ensure WebDAV client feature is installed
+ ansible.windows.win_feature:
+ name: WebDAV-Redirector
+ state: present
+ register: webdav_feature
+
+- name: Reboot after installing WebDAV client feature
+ ansible.windows.win_reboot:
+ when: webdav_feature.reboot_required
+
+- name: Map the HTTPS WebDAV location
+ community.windows.win_mapped_drive:
+ letter: W
+ path: \\live.sysinternals.com@SSL\tools # https://live.sysinternals.com/tools
+ state: present
+'''
+
+RETURN = r'''
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_msg.ps1 b/ansible_collections/community/windows/plugins/modules/win_msg.ps1
new file mode 100644
index 00000000..91763886
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_msg.ps1
@@ -0,0 +1,52 @@
+#!powershell
+
+# Copyright: (c) 2016, Jon Hawkesworth (@jhawkesworth) <jhawkesworth@protonmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+#
+$stopwatch = [system.diagnostics.stopwatch]::startNew()
+
+$params = Parse-Args $args -supports_check_mode $true
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
+
+$display_seconds = Get-AnsibleParam -obj $params -name "display_seconds" -type "int" -default "10"
+$msg = Get-AnsibleParam -obj $params -name "msg" -type "str" -default "Hello world!"
+$to = Get-AnsibleParam -obj $params -name "to" -type "str" -default "*"
+$wait = Get-AnsibleParam -obj $params -name "wait" -type "bool" -default $false
+
+$result = @{
+ changed = $false
+ display_seconds = $display_seconds
+ msg = $msg
+ wait = $wait
+}
+
+if ($msg.Length -gt 255) {
+ Fail-Json -obj $result -message "msg length must be less than 256 characters, current length: $($msg.Length)"
+}
+
+$msg_args = @($to, "/TIME:$display_seconds")
+
+if ($wait) {
+ $msg_args += "/W"
+}
+
+$msg_args += $msg
+if (-not $check_mode) {
+ $output = & msg.exe $msg_args 2>&1
+ $result.rc = $LASTEXITCODE
+}
+
+$endsend_at = Get-Date | Out-String
+$stopwatch.Stop()
+
+$result.changed = $true
+$result.runtime_seconds = $stopwatch.Elapsed.TotalSeconds
+$result.sent_localtime = $endsend_at.Trim()
+
+if ($result.rc -ne 0 ) {
+ Fail-Json -obj $result -message "$output"
+}
+
+Exit-Json $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_msg.py b/ansible_collections/community/windows/plugins/modules/win_msg.py
new file mode 100644
index 00000000..88a8beb1
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_msg.py
@@ -0,0 +1,88 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2017, Jon Hawkesworth (@jhawkesworth) <jhawkesworth@protonmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_msg
+short_description: Sends a message to logged in users on Windows hosts
+description:
+ - Wraps the msg.exe command in order to send messages to Windows hosts.
+options:
+ to:
+ description:
+ - Who to send the message to. Can be a username, sessionname or sessionid.
+ type: str
+ default: '*'
+ display_seconds:
+ description:
+ - How long to wait for receiver to acknowledge message, in seconds.
+ type: int
+ default: 10
+ wait:
+ description:
+ - Whether to wait for users to respond. Module will only wait for the number of seconds specified in display_seconds or 10 seconds if not specified.
+ However, if I(wait) is C(yes), the message is sent to each logged on user in turn, waiting for the user to either press 'ok' or for
+ the timeout to elapse before moving on to the next user.
+ type: bool
+ default: 'no'
+ msg:
+ description:
+ - The text of the message to be displayed.
+ - The message must be less than 256 characters.
+ type: str
+ default: Hello world!
+notes:
+ - This module must run on a windows host, so ensure your play targets windows
+ hosts, or delegates to a windows host.
+ - Messages are only sent to the local host where the module is run.
+ - The module does not support sending to users listed in a file.
+ - Setting wait to C(yes) can result in long run times on systems with many logged in users.
+seealso:
+- module: community.windows.win_say
+- module: community.windows.win_toast
+author:
+- Jon Hawkesworth (@jhawkesworth)
+'''
+
+EXAMPLES = r'''
+- name: Warn logged in users of impending upgrade
+ community.windows.win_msg:
+ display_seconds: 60
+ msg: Automated upgrade about to start. Please save your work and log off before {{ deployment_start_time }}
+'''
+
+RETURN = r'''
+msg:
+ description: Test of the message that was sent.
+ returned: changed
+ type: str
+ sample: Automated upgrade about to start. Please save your work and log off before 22 July 2016 18:00:00
+display_seconds:
+ description: Value of display_seconds module parameter.
+ returned: success
+ type: str
+ sample: 10
+rc:
+ description: The return code of the API call.
+ returned: always
+ type: int
+ sample: 0
+runtime_seconds:
+ description: How long the module took to run on the remote windows host.
+ returned: success
+ type: str
+ sample: 22 July 2016 17:45:51
+sent_localtime:
+ description: local time from windows host when the message was sent.
+ returned: success
+ type: str
+ sample: 22 July 2016 17:45:51
+wait:
+ description: Value of wait module parameter.
+ returned: success
+ type: bool
+ sample: false
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_net_adapter_feature.ps1 b/ansible_collections/community/windows/plugins/modules/win_net_adapter_feature.ps1
new file mode 100644
index 00000000..a7377e16
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_net_adapter_feature.ps1
@@ -0,0 +1,67 @@
+#!powershell
+
+# Copyright: (c) 2020, ライトウェルの人 <jiro.higuchi@shi-g.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+$spec = @{
+ options = @{
+ interface = @{ type = 'list'; elements = 'str'; required = $true }
+ state = @{ type = 'str'; choices = 'disabled', 'enabled'; default = 'enabled' }
+ component_id = @{ type = 'list'; elements = 'str'; required = $true }
+ }
+ supports_check_mode = $true
+}
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$interface = $module.Params.interface
+$state = $module.Params.state
+$component_id = $module.Params.component_id
+$check_mode = $module.CheckMode
+
+If ($interface -eq "*") {
+ $interface = Get-NetAdapter | Select-Object -ExpandProperty Name
+}
+Else {
+ ForEach ($Interface_name in $interface) {
+ If (@(Get-NetAdapter | Where-Object Name -eq $Interface_name).Count -eq 0) {
+ $module.FailJson("Invalid network adapter name: $Interface_name")
+ }
+ }
+}
+
+$state = $state -eq "enabled"
+
+ForEach ($componentID_name in $component_id) {
+ If (@(Get-NetAdapterBinding | Where-Object ComponentID -eq $componentID_name).Count -eq 0) {
+ $module.FailJson("Invalid componentID: $componentID_name")
+ }
+}
+
+$module.Result.changed = $false
+
+ForEach ($componentID_name in $component_id) {
+ ForEach ($Interface_name in $interface) {
+ $current_state = (Get-NetAdapterBinding | where-object { $_.Name -eq $Interface_name } | where-object { $_.ComponentID -eq $componentID_name }).Enabled
+ #Initialize the check_Idempotency flag for each interface, and for each component_id.
+ $check_Idempotency = $true
+
+ If ($current_state -eq $state) {
+ $check_Idempotency = $false
+ }
+
+ #Even Once $check_Idempotency remains $true, $module.Result.changed turns $true.
+ $module.Result.changed = $module.Result.changed -Or $check_Idempotency
+
+ If ($check_Idempotency) {
+ If ($state -eq "True") {
+ Enable-NetAdapterBinding -Name $Interface_name -ComponentID $componentID_name -WhatIf:$check_mode
+ }
+ Else {
+ Disable-NetAdapterBinding -Name $Interface_name -ComponentID $componentID_name -WhatIf:$check_mode
+ }
+ }
+ }
+}
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_net_adapter_feature.py b/ansible_collections/community/windows/plugins/modules/win_net_adapter_feature.py
new file mode 100644
index 00000000..91bb40fe
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_net_adapter_feature.py
@@ -0,0 +1,69 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2020, ライトウェルの人 <jiro.higuchi@shi-g.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+
+DOCUMENTATION = r'''
+---
+module: win_net_adapter_feature
+version_added: 1.2.0
+short_description: Enable or disable certain network adapters.
+description:
+ - Enable or disable some network components of a certain network adapter or all the network adapters.
+options:
+ interface:
+ description:
+ - Name of Network Adapter Interface. For example, C(Ethernet0) or C(*).
+ type: list
+ elements: str
+ required: yes
+ state:
+ description:
+ - Specify the state of ms_tcpip6 of interfaces.
+ type: str
+ choices: [ enabled, disabled ]
+ default: enabled
+ required: no
+ component_id:
+ description:
+ - Specify the below component_id of network adapters.
+ - component_id (DisplayName)
+ - C(ms_implat) (Microsoft Network Adapter Multiplexor Protocol)
+ - C(ms_lltdio) (Link-Layer Topology Discovery Mapper I/O Driver)
+ - C(ms_tcpip6) (Internet Protocol Version 6 (TCP/IPv6))
+ - C(ms_tcpip) (Internet Protocol Version 4 (TCP/IPv4))
+ - C(ms_lldp) (Microsoft LLDP Protocol Driver)
+ - C(ms_rspndr) (Link-Layer Topology Discovery Responder)
+ - C(ms_msclient) (Client for Microsoft Networks)
+ - C(ms_pacer) (QoS Packet Scheduler)
+ - If you'd like to set custom adapters like 'Juniper Network Service', get the I(component_id) by running the C(Get-NetAdapterBinding) cmdlet.
+ type: list
+ elements: str
+ required: yes
+
+author:
+ - ライトウェルの人 (@jirolin)
+'''
+
+
+EXAMPLES = r'''
+- name: enable multiple interfaces of multiple interfaces
+ community.windows.win_net_adapter_feature:
+ interface:
+ - 'Ethernet0'
+ - 'Ethernet1'
+ state: enabled
+ component_id:
+ - ms_tcpip6
+ - ms_server
+
+- name: Enable ms_tcpip6 of all the Interface
+ community.windows.win_net_adapter_feature:
+ interface: '*'
+ state: enabled
+ component_id:
+ - ms_tcpip6
+
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_netbios.ps1 b/ansible_collections/community/windows/plugins/modules/win_netbios.ps1
new file mode 100644
index 00000000..5995e580
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_netbios.ps1
@@ -0,0 +1,70 @@
+#!powershell
+
+# Copyright: (c) 2019, Thomas Moore (@tmmruk) <hi@tmmr.uk>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+$spec = @{
+ options = @{
+ state = @{ type = "str"; choices = "enabled", "disabled", "default"; required = $true }
+ adapter_names = @{ type = "list"; elements = "str"; required = $false }
+ }
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+$module.Result.reboot_required = $false
+
+$state = $module.Params.state
+$adapter_names = $module.Params.adapter_names
+
+switch ( $state ) {
+ 'default' { $netbiosoption = 0 }
+ enabled { $netbiosoption = 1 }
+ disabled { $netbiosoption = 2 }
+}
+
+if (-not $adapter_names) {
+ # Target all network adapters on the system
+ $get_params = @{
+ ClassName = 'Win32_NetworkAdapterConfiguration'
+ Filter = 'IPEnabled=true'
+ Property = @('MacAddress', 'TcpipNetbiosOptions')
+ }
+ $target_adapters_config = Get-CimInstance @get_params
+}
+else {
+ $get_params = @{
+ Class = 'Win32_NetworkAdapter'
+ Filter = ($adapter_names | ForEach-Object -Process { "NetConnectionId='$_'" }) -join " OR "
+ KeyOnly = $true
+ }
+ $target_adapters_config = Get-CimInstance @get_params | Get-CimAssociatedInstance -ResultClass 'Win32_NetworkAdapterConfiguration'
+ if (($target_adapters_config | Measure-Object).Count -ne $adapter_names.Count) {
+ $module.FailJson("Not all of the target adapter names could be found on the system. No configuration changes have been made. $adapter_names")
+ }
+}
+
+foreach ($adapter in $target_adapters_config) {
+ if ($adapter.TcpipNetbiosOptions -ne $netbiosoption) {
+ if (-not $module.CheckMode) {
+ $result = Invoke-CimMethod -InputObject $adapter -MethodName SetTcpipNetbios -Arguments @{TcpipNetbiosOptions = $netbiosoption }
+ switch ( $result.ReturnValue ) {
+ 0 { <# Success no reboot required #> }
+ 1 { $module.Result.reboot_required = $true }
+ 100 {
+ $msg = "DHCP not enabled on adapter $($adapter.MacAddress). Unable to set default. Try using disabled or enabled options instead."
+ $module.Warn($msg)
+ }
+ default {
+ $msg = "An error occurred while setting TcpipNetbios options on adapter $($adapter.MacAddress). Return code $($result.ReturnValue)."
+ $module.FailJson($msg)
+ }
+ }
+ }
+ $module.Result.changed = $true
+ }
+}
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_netbios.py b/ansible_collections/community/windows/plugins/modules/win_netbios.py
new file mode 100644
index 00000000..80e0fbec
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_netbios.py
@@ -0,0 +1,69 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2019, Thomas Moore (@tmmruk)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_netbios
+short_description: Manage NetBIOS over TCP/IP settings on Windows.
+description:
+ - Enables or disables NetBIOS on Windows network adapters.
+ - Can be used to protect a system against NBT-NS poisoning and avoid NBNS broadcast storms.
+ - Settings can be applied system wide or per adapter.
+options:
+ state:
+ description:
+ - Whether NetBIOS should be enabled, disabled, or default (use setting from DHCP server or if static IP address is assigned enable NetBIOS).
+ choices:
+ - enabled
+ - disabled
+ - default
+ required: yes
+ type: str
+ adapter_names:
+ description:
+ - List of adapter names for which to manage NetBIOS settings. If this option is omitted then configuration is applied to all adapters on the system.
+ - The adapter name used is the connection caption in the Network Control Panel or via C(Get-NetAdapter), eg C(Ethernet 2).
+ type: list
+ elements: str
+ required: no
+
+author:
+ - Thomas Moore (@tmmruk)
+notes:
+ - Changing NetBIOS settings does not usually require a reboot and will take effect immediately.
+ - UDP port 137/138/139 will no longer be listening once NetBIOS is disabled.
+'''
+
+EXAMPLES = r'''
+- name: Disable NetBIOS system wide
+ community.windows.win_netbios:
+ state: disabled
+
+- name: Disable NetBIOS on Ethernet2
+ community.windows.win_netbios:
+ state: disabled
+ adapter_names:
+ - Ethernet2
+
+- name: Enable NetBIOS on Public and Backup adapters
+ community.windows.win_netbios:
+ state: enabled
+ adapter_names:
+ - Public
+ - Backup
+
+- name: Set NetBIOS to system default on all adapters
+ community.windows.win_netbios:
+ state: default
+'''
+
+RETURN = r'''
+reboot_required:
+ description: Boolean value stating whether a system reboot is required.
+ returned: always
+ type: bool
+ sample: true
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_nssm.ps1 b/ansible_collections/community/windows/plugins/modules/win_nssm.ps1
new file mode 100644
index 00000000..3b951cf1
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_nssm.ps1
@@ -0,0 +1,604 @@
+#!powershell
+
+# Copyright: (c) 2015, George Frank <george@georgefrank.net>
+# Copyright: (c) 2015, Adam Keech <akeech@chathamfinancial.com>
+# Copyright: (c) 2015, Hans-Joachim Kliemeck <git@kliemeck.de>
+# Copyright: (c) 2019, Kevin Subileau (@ksubileau)
+# 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.ArgvParser
+#Requires -Module Ansible.ModuleUtils.CommandUtil
+
+$ErrorActionPreference = "Stop"
+
+$start_modes_map = @{
+ "auto" = "SERVICE_AUTO_START"
+ "delayed" = "SERVICE_DELAYED_AUTO_START"
+ "manual" = "SERVICE_DEMAND_START"
+ "disabled" = "SERVICE_DISABLED"
+}
+
+$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
+
+$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
+$state_options = "present", "absent", "started", "stopped", "restarted"
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset $state_options -resultobj $result
+$display_name = Get-AnsibleParam -obj $params -name 'display_name' -type 'str'
+$description = Get-AnsibleParam -obj $params -name 'description' -type 'str'
+
+$application = Get-AnsibleParam -obj $params -name "application" -type "path"
+$appDirectory = Get-AnsibleParam -obj $params -name "working_directory" -aliases "app_directory", "chdir" -type "path"
+$appParameters = Get-AnsibleParam -obj $params -name "app_parameters"
+$appArguments = Get-AnsibleParam -obj $params -name "arguments" -aliases "app_parameters_free_form"
+
+$stdoutFile = Get-AnsibleParam -obj $params -name "stdout_file" -type "path"
+$stderrFile = Get-AnsibleParam -obj $params -name "stderr_file" -type "path"
+
+$executable = Get-AnsibleParam -obj $params -name "executable" -type "path" -default "nssm.exe"
+
+$app_env = Get-AnsibleParam -obj $params -name "app_environment" -type "dict"
+
+$app_rotate_bytes = Get-AnsibleParam -obj $params -name "app_rotate_bytes" -type "int" -default 104858
+$app_rotate_online = Get-AnsibleParam -obj $params -name "app_rotate_online" -type "int" -default 0 -validateset 0, 1
+$app_stop_method_console = Get-AnsibleParam -obj $params -name "app_stop_method_console" -type "int"
+$app_stop_method_skip = Get-AnsibleParam -obj $params -name "app_stop_method_skip" -type "int" -validateset 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
+
+# Deprecated options, will be removed in a major release after 2021-07-01.
+$startMode = Get-AnsibleParam -obj $params -name "start_mode" -type "str" -default "auto" -validateset $start_modes_map.Keys -resultobj $result
+$dependencies = Get-AnsibleParam -obj $params -name "dependencies" -type "list"
+$user = Get-AnsibleParam -obj $params -name "username" -type "str" -aliases "user"
+$password = Get-AnsibleParam -obj $params -name "password" -type "str"
+
+$result = @{
+ changed = $false
+}
+$diff_text = $null
+
+function Invoke-NssmCommand {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)]
+ [string[]]$arguments
+ )
+
+ $command = Argv-ToString -arguments (@($executable) + $arguments)
+ $result = Run-Command -command $command
+
+ $result.arguments = $command
+
+ return $result
+}
+
+function Get-NssmServiceStatus {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$service
+ )
+
+ return Invoke-NssmCommand -arguments @("status", $service)
+}
+
+function Get-NssmServiceParameter {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$service,
+ [Parameter(Mandatory = $true)]
+ [Alias("param")]
+ [string]$parameter,
+ [Parameter(Mandatory = $false)]
+ [string]$subparameter
+ )
+
+ $arguments = @("get", $service, $parameter)
+ if ($subparameter -ne "") {
+ $arguments += $subparameter
+ }
+ return Invoke-NssmCommand -arguments $arguments
+}
+
+function Set-NssmServiceParameter {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$service,
+ [Parameter(Mandatory = $true)]
+ [string]$parameter,
+ [Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)]
+ [Alias("value")]
+ [string[]]$arguments
+ )
+
+ return Invoke-NssmCommand -arguments (@("set", $service, $parameter) + $arguments)
+}
+
+function Reset-NssmServiceParameter {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$service,
+ [Parameter(Mandatory = $true)]
+ [Alias("param")]
+ [string]$parameter
+ )
+
+ return Invoke-NssmCommand -arguments @("reset", $service, $parameter)
+}
+
+function Update-NssmServiceParameter {
+ <#
+ .SYNOPSIS
+ A generic cmdlet to idempotently set a nssm service parameter.
+ .PARAMETER service
+ [String] The service name
+ .PARAMETER parameter
+ [String] The name of the nssm parameter to set.
+ .PARAMETER arguments
+ [String[]] Target value (or list of value) or array of arguments to pass to the 'nssm set' command.
+ .PARAMETER compare
+ [scriptblock] An optionnal idempotency check scriptblock that must return true when
+ the current value is equal to the desired value. Usefull when 'nssm get' doesn't return
+ the same value as 'nssm set' takes in argument, like for the ObjectName parameter.
+ #>
+ [CmdletBinding(SupportsShouldProcess = $true)]
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$service,
+
+ [Parameter(Mandatory = $true)]
+ [string]$parameter,
+
+ [Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)]
+ [AllowEmptyString()]
+ [AllowNull()]
+ [Alias("value")]
+ [string[]]$arguments,
+
+ [Parameter()]
+ [scriptblock]$compare = { param($actual, $expected) @(Compare-Object -ReferenceObject $actual -DifferenceObject $expected).Length -eq 0 }
+ )
+
+ if ($null -eq $arguments) { return }
+ $arguments = @($arguments | Where-Object { $_ -ne '' })
+
+ $nssm_result = Get-NssmServiceParameter -service $service -parameter $parameter
+
+ if ($nssm_result.rc -ne 0) {
+ $result.nssm_error_cmd = $nssm_result.arguments
+ $result.nssm_error_log = $nssm_result.stderr
+ Fail-Json -obj $result -message "Error retrieving $parameter for service ""$service"""
+ }
+
+ $current_values = @($nssm_result.stdout.split("`n`r") | Where-Object { $_ -ne '' })
+
+ if (-not $compare.Invoke($current_values, $arguments)) {
+ if ($PSCmdlet.ShouldProcess($service, "Update '$parameter' parameter")) {
+ if ($arguments.Count -gt 0) {
+ $nssm_result = Set-NssmServiceParameter -service $service -parameter $parameter -arguments $arguments
+ }
+ else {
+ $nssm_result = Reset-NssmServiceParameter -service $service -parameter $parameter
+ }
+
+ if ($nssm_result.rc -ne 0) {
+ $result.nssm_error_cmd = $nssm_result.arguments
+ $result.nssm_error_log = $nssm_result.stderr
+ Fail-Json -obj $result -message "Error setting $parameter for service ""$service"""
+ }
+ }
+
+ $script:diff_text += "-$parameter = $($current_values -join ', ')`n+$parameter = $($arguments -join ', ')`n"
+ $result.changed_by = $parameter
+ $result.changed = $true
+ }
+}
+
+function Test-NssmServiceExist {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$service
+ )
+
+ return [bool](Get-Service -Name $service -ErrorAction SilentlyContinue)
+}
+
+function Invoke-NssmStart {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$service
+ )
+
+ $nssm_result = Invoke-NssmCommand -arguments @("start", $service)
+
+ if ($nssm_result.rc -ne 0) {
+ $result.nssm_error_cmd = $nssm_result.arguments
+ $result.nssm_error_log = $nssm_result.stderr
+ Fail-Json -obj $result -message "Error starting service ""$service"""
+ }
+}
+
+function Invoke-NssmStop {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$service
+ )
+
+ $nssm_result = Invoke-NssmCommand -arguments @("stop", $service)
+
+ if ($nssm_result.rc -ne 0) {
+ $result.nssm_error_cmd = $nssm_result.arguments
+ $result.nssm_error_log = $nssm_result.stderr
+ Fail-Json -obj $result -message "Error stopping service ""$service"""
+ }
+}
+
+function Start-NssmService {
+ [CmdletBinding(SupportsShouldProcess = $true)]
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$service
+ )
+
+ $currentStatus = Get-NssmServiceStatus -service $service
+
+ if ($currentStatus.rc -ne 0) {
+ $result.nssm_error_cmd = $currentStatus.arguments
+ $result.nssm_error_log = $currentStatus.stderr
+ Fail-Json -obj $result -message "Error starting service ""$service"""
+ }
+
+ if ($currentStatus.stdout -notlike "*SERVICE_RUNNING*") {
+ if ($PSCmdlet.ShouldProcess($service, "Start service")) {
+ switch -wildcard ($currentStatus.stdout) {
+ "*SERVICE_STOPPED*" { Invoke-NssmStart -service $service }
+
+ "*SERVICE_CONTINUE_PENDING*" { Invoke-NssmStop -service $service; Invoke-NssmStart -service $service }
+ "*SERVICE_PAUSE_PENDING*" { Invoke-NssmStop -service $service; Invoke-NssmStart -service $service }
+ "*SERVICE_PAUSED*" { Invoke-NssmStop -service $service; Invoke-NssmStart -service $service }
+ "*SERVICE_START_PENDING*" { Invoke-NssmStop -service $service; Invoke-NssmStart -service $service }
+ "*SERVICE_STOP_PENDING*" { Invoke-NssmStop -service $service; Invoke-NssmStart -service $service }
+ }
+ }
+
+ $result.changed_by = "start_service"
+ $result.changed = $true
+ }
+}
+
+function Stop-NssmService {
+ [CmdletBinding(SupportsShouldProcess = $true)]
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$service
+ )
+
+ $currentStatus = Get-NssmServiceStatus -service $service
+
+ if ($currentStatus.rc -ne 0) {
+ $result.nssm_error_cmd = $currentStatus.arguments
+ $result.nssm_error_log = $currentStatus.stderr
+ Fail-Json -obj $result -message "Error stopping service ""$service"""
+ }
+
+ if ($currentStatus.stdout -notlike "*SERVICE_STOPPED*") {
+ if ($PSCmdlet.ShouldProcess($service, "Stop service")) {
+ Invoke-NssmStop -service $service
+ }
+
+ $result.changed_by = "stop_service"
+ $result.changed = $true
+ }
+}
+
+function Add-DepByDate {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory = $true)]
+ [String]$Message,
+
+ [Parameter(Mandatory = $true)]
+ [String]$Date
+ )
+
+ # Legacy doesn't natively support deprecate by date, need to do this manually until we use Ansible.Basic
+ if (-not $result.ContainsKey('deprecations')) {
+ $result.deprecations = @()
+ }
+ $result.deprecations += @{
+ msg = $Message
+ date = $Date
+ collection_name = "community.windows"
+ }
+}
+
+Function ConvertTo-NormalizedUser {
+ [CmdletBinding()]
+ param (
+ [Parameter(Mandatory = $true)]
+ [String]$InputObject
+ )
+
+ $systemSid = [System.Security.Principal.SecurityIdentifier]'S-1-5-18'
+
+ # Try to get the SID from the raw value or with LocalSystem (what services consider to be SYSTEM).
+ try {
+ $sid = New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList $InputObject
+ }
+ catch [ArgumentException] {
+ if ($InputObject -eq "LocalSystem") {
+ $sid = $systemSid
+ }
+ }
+
+ if (-not $sid) {
+ $candidates = @(if ($InputObject.Contains('\')) {
+ $nameSplit = $InputObject.Split('\', 2)
+
+ if ($nameSplit[0] -eq '.') {
+ # If the domain portion is . try using the hostname then falling back to just the username.
+ # Usually the hostname just works except when running on a DC where it's a domain account
+ # where looking up just the username should work.
+ , @($env:COMPUTERNAME, $nameSplit[1])
+ $nameSplit[1]
+ }
+ else {
+ , $nameSplit
+ }
+ }
+ else {
+ $InputObject
+ })
+
+ $sid = for ($i = 0; $i -lt $candidates.Length; $i++) {
+ $candidate = $candidates[$i]
+ $ntAccount = New-Object -TypeName System.Security.Principal.NTAccount -ArgumentList $candidate
+ try {
+ $ntAccount.Translate([System.Security.Principal.SecurityIdentifier])
+ break
+ }
+ catch [System.Security.Principal.IdentityNotMappedException] {
+ if ($i -eq ($candidates.Length - 1)) {
+ throw
+ }
+ continue
+ }
+ }
+ }
+
+ if ($sid -eq $systemSid) {
+ "LocalSystem"
+ }
+ else {
+ $sid.Translate([System.Security.Principal.NTAccount]).Value
+ }
+}
+
+if (($null -ne $appParameters) -and ($null -ne $appArguments)) {
+ Fail-Json $result "'app_parameters' and 'arguments' are mutually exclusive but have both been set."
+}
+
+# Backward compatibility for old parameters style. Remove the block bellow in 2.12
+if ($null -ne $appParameters) {
+ $dep = @{
+ Message = "The parameter 'app_parameters' will be removed soon, use 'arguments' instead"
+ Date = "2022-07-01"
+ }
+ Add-DepByDate @dep
+
+ if ($appParameters -isnot [string]) {
+ Fail-Json -obj $result -message "The app_parameters parameter must be a string representing a dictionary."
+ }
+
+ # Convert dict-as-string form to list
+ $escapedAppParameters = $appParameters.TrimStart("@").TrimStart("{").TrimEnd("}").Replace("; ", "`n").Replace("\", "\\")
+ $appParametersHash = ConvertFrom-StringData -StringData $escapedAppParameters
+
+ $appParamsArray = @()
+ $appParametersHash.GetEnumerator() | Foreach-Object {
+ if ($_.Name -ne "_") {
+ $appParamsArray += $_.Name
+ }
+ $appParamsArray += $_.Value
+ }
+ $appArguments = @($appParamsArray)
+
+ # The rest of the code should use only the new $appArguments variable
+}
+
+if ($state -ne 'absent') {
+ if ($null -eq $application) {
+ Fail-Json -obj $result -message "The application parameter must be defined when the state is not absent."
+ }
+
+ if (-not (Test-Path -LiteralPath $application -PathType Leaf)) {
+ Fail-Json -obj $result -message "The application specified ""$application"" does not exist on the host."
+ }
+
+ if ($null -eq $appDirectory) {
+ $appDirectory = (Get-Item -LiteralPath $application).DirectoryName
+ }
+
+ if ($user) {
+ $user = ConvertTo-NormalizedUser -InputObject $user
+ if (
+ $user -in @(
+ (ConvertTo-NormalizedUser -InputObject 'S-1-5-18'), # SYSTEM
+ (ConvertTo-NormalizedUser -InputObject 'S-1-5-19'), # LOCAL SERVICE
+ (ConvertTo-NormalizedUser -InputObject 'S-1-5-20') # NETWORK SERVICE
+ )
+ ) {
+ # These accounts have no password (NSSM expects nothing)
+ $password = ""
+ }
+ elseif ($user.EndsWith('$')) {
+ # While a gMSA doesn't have a password NSSM will fail with no password so we set a dummy value. The service
+ # still starts up properly with this so SCManager handles this nicely.
+ $password = "gsma_password"
+ }
+ elseif (-not $password) {
+ # Any other account requires a password here.
+ Fail-Json -obj $result -message "User without password is informed for service ""$name"""
+ }
+ }
+}
+
+
+$service_exists = Test-NssmServiceExist -service $name
+
+if ($state -eq 'absent') {
+ if ($service_exists) {
+ if (-not $check_mode) {
+ if ((Get-Service -Name $name).Status -ne "Stopped") {
+ $nssm_result = Invoke-NssmStop -service $name
+ }
+
+ $nssm_result = Invoke-NssmCommand -arguments @("remove", $name, "confirm")
+
+ if ($nssm_result.rc -ne 0) {
+ $result.nssm_error_cmd = $nssm_result.arguments
+ $result.nssm_error_log = $nssm_result.stderr
+ Fail-Json -obj $result -message "Error removing service ""$name"""
+ }
+ }
+
+ $diff_text += "-[$name]"
+ $result.changed_by = "remove_service"
+ $result.changed = $true
+ }
+}
+else {
+ $diff_text_added_prefix = ''
+ if (-not $service_exists) {
+ if (-not $check_mode) {
+ $nssm_result = Invoke-NssmCommand -arguments @("install", $name, $application)
+
+ if ($nssm_result.rc -ne 0) {
+ $result.nssm_error_cmd = $nssm_result.arguments
+ $result.nssm_error_log = $nssm_result.stderr
+ Fail-Json -obj $result -message "Error installing service ""$name"""
+ }
+ $service_exists = $true
+ }
+
+ $diff_text_added_prefix = '+'
+ $result.changed_by = "install_service"
+ $result.changed = $true
+ }
+
+ $diff_text += "$diff_text_added_prefix[$name]`n"
+
+ # We cannot configure a service that was created above in check mode as it won't actually exist
+ if ($service_exists) {
+ $common_params = @{
+ service = $name
+ WhatIf = $check_mode
+ }
+
+ Update-NssmServiceParameter -parameter "Application" -value $application @common_params
+ Update-NssmServiceParameter -parameter "DisplayName" -value $display_name @common_params
+ Update-NssmServiceParameter -parameter "Description" -value $description @common_params
+
+ Update-NssmServiceParameter -parameter "AppDirectory" -value $appDirectory @common_params
+
+
+ if ($null -ne $appArguments) {
+ $singleLineParams = ""
+ if ($appArguments -is [array]) {
+ $singleLineParams = Argv-ToString -arguments $appArguments
+ }
+ else {
+ $singleLineParams = $appArguments.ToString()
+ }
+
+ $result.nssm_app_parameters = $appArguments
+ $result.nssm_single_line_app_parameters = $singleLineParams
+
+ Update-NssmServiceParameter -parameter "AppParameters" -value $singleLineParams @common_params
+ }
+
+
+ Update-NssmServiceParameter -parameter "AppStdout" -value $stdoutFile @common_params
+ Update-NssmServiceParameter -parameter "AppStderr" -value $stderrFile @common_params
+
+ # set app environment, only do this for now when explicitly requested by caller to
+ # avoid breaking playbooks which use another / custom scheme for configuring app_env
+ if ($null -ne $app_env) {
+ # note: convert app_env dictionary to list of strings in the form key=value and pass that a long as value
+ $app_env_str = $app_env.GetEnumerator() | ForEach-Object { "$($_.Name)=$($_.Value)" }
+
+ # note: this is important here to make an empty envvar set working properly (in the sense that appenv is reset)
+ if ($null -eq $app_env_str) {
+ $app_env_str = ''
+ }
+
+ Update-NssmServiceParameter -parameter "AppEnvironmentExtra" -value $app_env_str @common_params
+ }
+
+ ###
+ # Setup file rotation so we don't accidentally consume too much disk
+ ###
+
+ #set files to overwrite
+ Update-NssmServiceParameter -parameter "AppStdoutCreationDisposition" -value 2 @common_params
+ Update-NssmServiceParameter -parameter "AppStderrCreationDisposition" -value 2 @common_params
+
+ #enable file rotation
+ Update-NssmServiceParameter -parameter "AppRotateFiles" -value 1 @common_params
+
+ #don't rotate until the service restarts
+ Update-NssmServiceParameter -parameter "AppRotateOnline" -value $app_rotate_online @common_params
+
+ #both of the below conditions must be met before rotation will happen
+ #minimum age before rotating
+ Update-NssmServiceParameter -parameter "AppRotateSeconds" -value 86400 @common_params
+
+ #minimum size before rotating
+ Update-NssmServiceParameter -parameter "AppRotateBytes" -value $app_rotate_bytes @common_params
+
+ Update-NssmServiceParameter -parameter "DependOnService" -arguments $dependencies @common_params
+ if ($user) {
+ # Use custom compare callback to test only the username (and not the password)
+ Update-NssmServiceParameter -parameter "ObjectName" -arguments @($user, $password) -compare {
+ param($actual, $expected)
+
+ $actualUser = ConvertTo-NormalizedUser -InputObject $actual[0]
+ $expectedUser = ConvertTo-NormalizedUser -InputObject $expected[0]
+
+ $actualUser -eq $expectedUser
+ } @common_params
+ }
+ $mappedMode = $start_modes_map.$startMode
+ Update-NssmServiceParameter -parameter "Start" -value $mappedMode @common_params
+ if ($state -in "stopped", "restarted") {
+ Stop-NssmService @common_params
+ }
+
+ if ($state -in "started", "restarted") {
+ Start-NssmService @common_params
+ }
+
+ # Added per users` requests
+ if ($null -ne $app_stop_method_console) {
+ Update-NssmServiceParameter -parameter "AppStopMethodConsole" -value $app_stop_method_console @common_params
+ }
+
+ if ($null -ne $app_stop_method_skip) {
+ Update-NssmServiceParameter -parameter "AppStopMethodSkip" -value $app_stop_method_skip @common_params
+ }
+ }
+}
+
+if ($diff_mode -and $result.changed -eq $true) {
+ $result.diff = @{
+ prepared = $diff_text
+ }
+}
+
+Exit-Json $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_nssm.py b/ansible_collections/community/windows/plugins/modules/win_nssm.py
new file mode 100644
index 00000000..d79f638b
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_nssm.py
@@ -0,0 +1,216 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2015, Heyo
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_nssm
+short_description: Install a service using NSSM
+description:
+ - Install a Windows service using the NSSM wrapper.
+ - NSSM is a service helper which doesn't suck. See U(https://nssm.cc/) for more information.
+requirements:
+ - "nssm >= 2.24.0 # (install via M(chocolatey.chocolatey.win_chocolatey)) C(win_chocolatey: name=nssm)"
+options:
+ name:
+ description:
+ - Name of the service to operate on.
+ type: str
+ required: true
+ state:
+ description:
+ - State of the service on the system.
+ type: str
+ choices: [ absent, present, started, stopped, restarted ]
+ default: present
+ application:
+ description:
+ - The application binary to run as a service
+ - Required when I(state) is C(present), C(started), C(stopped), or C(restarted).
+ type: path
+ executable:
+ description:
+ - The location of the NSSM utility (in case it is not located in your PATH).
+ type: path
+ default: nssm.exe
+ description:
+ description:
+ - The description to set for the service.
+ type: str
+ display_name:
+ description:
+ - The display name to set for the service.
+ type: str
+ working_directory:
+ description:
+ - The working directory to run the service executable from (defaults to the directory containing the application binary)
+ type: path
+ aliases: [ app_directory, chdir ]
+ stdout_file:
+ description:
+ - Path to receive output.
+ type: path
+ stderr_file:
+ description:
+ - Path to receive error output.
+ type: path
+ app_parameters:
+ description:
+ - A string representing a dictionary of parameters to be passed to the application when it starts.
+ - DEPRECATED since v2.8, please use I(arguments) instead.
+ - This is mutually exclusive with I(arguments).
+ type: str
+ arguments:
+ description:
+ - Parameters to be passed to the application when it starts.
+ - This can be either a simple string or a list.
+ - This is mutually exclusive with I(app_parameters).
+ aliases: [ app_parameters_free_form ]
+ type: str
+ dependencies:
+ description:
+ - Service dependencies that has to be started to trigger startup, separated by comma.
+ type: list
+ elements: str
+ username:
+ description:
+ - User to be used for service startup.
+ - Group managed service accounts must end with C($).
+ - Before C(1.8.0), this parameter was just C(user).
+ type: str
+ aliases:
+ - user
+ password:
+ description:
+ - Password to be used for service startup.
+ - This is not required for the well known service accounts and group managed service accounts.
+ type: str
+ start_mode:
+ description:
+ - If C(auto) is selected, the service will start at bootup.
+ - C(delayed) causes a delayed but automatic start after boot.
+ - C(manual) means that the service will start only when another service needs it.
+ - C(disabled) means that the service will stay off, regardless if it is needed or not.
+ type: str
+ choices: [ auto, delayed, disabled, manual ]
+ default: auto
+ app_environment:
+ description:
+ - Key/Value pairs which will be added to the environment of the service application.
+ type: dict
+ version_added: 1.2.0
+ app_rotate_bytes:
+ description:
+ - NSSM will not rotate any file which is smaller than the configured number of bytes.
+ type: int
+ default: 104858
+ app_rotate_online:
+ description:
+ - If set to 1, nssm can rotate files which grow to the configured file size limit while the service is running.
+ type: int
+ choices:
+ - 0
+ - 1
+ default: 0
+ app_stop_method_console:
+ description:
+ - Time to wait after sending Control-C.
+ type: int
+ app_stop_method_skip:
+ description:
+ - To disable service shutdown methods, set to the sum of one or more of the numbers
+ - 1 - Don't send Control-C to the console.
+ - 2 - Don't send WM_CLOSE to windows.
+ - 4 - Don't send WM_QUIT to threads.
+ - 8 - Don't call TerminateProcess().
+ type: int
+ choices:
+ - 1
+ - 2
+ - 3
+ - 4
+ - 5
+ - 6
+ - 7
+ - 8
+ - 9
+ - 10
+ - 11
+ - 12
+ - 13
+ - 14
+ - 15
+seealso:
+ - module: ansible.windows.win_service
+notes:
+ - The service will NOT be started after its creation when C(state=present).
+ - Once the service is created, you can use the M(ansible.windows.win_service) module to start it or configure
+ some additionals properties, such as its startup type, dependencies, service account, and so on.
+author:
+ - Adam Keech (@smadam813)
+ - George Frank (@georgefrank)
+ - Hans-Joachim Kliemeck (@h0nIg)
+ - Michael Wild (@themiwi)
+ - Kevin Subileau (@ksubileau)
+ - Shachaf Goldstein (@Shachaf92)
+'''
+
+EXAMPLES = r'''
+- name: Install the foo service
+ community.windows.win_nssm:
+ name: foo
+ application: C:\windows\foo.exe
+
+# This will yield the following command: C:\windows\foo.exe bar "true"
+- name: Install the Consul service with a list of parameters
+ community.windows.win_nssm:
+ name: Consul
+ application: C:\consul\consul.exe
+ arguments:
+ - agent
+ - -config-dir=C:\consul\config
+
+# This is strictly equivalent to the previous example
+- name: Install the Consul service with an arbitrary string of parameters
+ community.windows.win_nssm:
+ name: Consul
+ application: C:\consul\consul.exe
+ arguments: agent -config-dir=C:\consul\config
+
+
+# Install the foo service, and then configure and start it with win_service
+- name: Install the foo service, redirecting stdout and stderr to the same file
+ community.windows.win_nssm:
+ name: foo
+ application: C:\windows\foo.exe
+ stdout_file: C:\windows\foo.log
+ stderr_file: C:\windows\foo.log
+
+- name: Configure and start the foo service using win_service
+ ansible.windows.win_service:
+ name: foo
+ dependencies: [ adf, tcpip ]
+ username: foouser
+ password: secret
+ start_mode: manual
+ state: started
+
+- name: Install a script based service and define custom environment variables
+ community.windows.win_nssm:
+ name: <ServiceName>
+ application: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
+ arguments:
+ - <path-to-script>
+ - <script arg>
+ app_environment:
+ AUTH_TOKEN: <token value>
+ SERVER_URL: https://example.com
+ PATH: "<path-prepends>;{{ ansible_env.PATH }};<path-appends>"
+
+- name: Remove the foo service
+ community.windows.win_nssm:
+ name: foo
+ state: absent
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_pagefile.ps1 b/ansible_collections/community/windows/plugins/modules/win_pagefile.ps1
new file mode 100644
index 00000000..627d4e46
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_pagefile.ps1
@@ -0,0 +1,243 @@
+#!powershell
+
+# Copyright: (c) 2017, Liran Nisanov <lirannis@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
+
+########
+
+Function Remove-Pagefile {
+ [CmdletBinding(SupportsShouldProcess)]
+ param(
+ $path
+ )
+ Get-CIMInstance Win32_PageFileSetting | Where-Object { $_.Name -eq $path } | ForEach-Object {
+ if ($PSCmdlet.ShouldProcess($Path, "remove pagefile")) {
+ $_ | Remove-CIMInstance
+ }
+ }
+}
+
+Function Get-Pagefile($path) {
+ Get-CIMInstance Win32_PageFileSetting | Where-Object { $_.Name -eq $path }
+}
+
+########
+
+$params = Parse-Args $args -supports_check_mode $true
+$check_mode = Get-AnsibleParam -obj $params -name '_ansible_check_mode' -type 'bool' -default $false
+
+$automatic = Get-AnsibleParam -obj $params -name "automatic" -type "bool"
+$drive = Get-AnsibleParam -obj $params -name "drive" -type "str"
+$fullPath = $drive + ":\pagefile.sys"
+$initialSize = Get-AnsibleParam -obj $params -name "initial_size" -type "int"
+$maximumSize = Get-AnsibleParam -obj $params -name "maximum_size" -type "int"
+$override = Get-AnsibleParam -obj $params -name "override" -type "bool" -default $true
+$removeAll = Get-AnsibleParam -obj $params -name "remove_all" -type "bool" -default $false
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "query" -validateset "present", "absent", "query"
+$systemManaged = Get-AnsibleParam -obj $params -name "system_managed" -type "bool" -default $false
+$test_path = Get-AnsibleParam -obj $params -name "test_path" -type "bool" -default $true
+
+$result = @{
+ changed = $false
+}
+
+if ($removeAll) {
+ $currentPageFiles = Get-CIMInstance Win32_PageFileSetting
+ if ($null -ne $currentPageFiles) {
+ $currentPageFiles | Remove-CIMInstance -WhatIf:$check_mode > $null
+ $result.changed = $true
+ }
+}
+
+$regPath = "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management"
+
+if ($null -ne $automatic) {
+ # change autmoatic managed pagefile
+ try {
+ $computerSystem = Get-CIMInstance -Class win32_computersystem
+ }
+ catch {
+ Fail-Json $result "Failed to query WMI computer system object $($_.Exception.Message)"
+ }
+ if ($computerSystem.AutomaticManagedPagefile -ne $automatic) {
+ if (-not $check_mode) {
+ try {
+ $computerSystem | Set-CimInstance -Property @{automaticmanagedpagefile = "$automatic" } > $null
+ }
+ catch {
+ Fail-Json $result "Failed to set AutomaticManagedPagefile $($_.Exception.Message)"
+ }
+ }
+ $result.changed = $true
+ }
+}
+
+if ($state -eq "absent") {
+ # Remove pagefile
+ if ($null -ne (Get-Pagefile $fullPath)) {
+ try {
+ Remove-Pagefile $fullPath -whatif:$check_mode
+ }
+ catch {
+ Fail-Json $result "Failed to remove pagefile $($_.Exception.Message)"
+ }
+ $result.changed = $true
+ }
+}
+elseif ($state -eq "present") {
+ # Remove current pagefile
+ if ($override) {
+ if ($null -ne (Get-Pagefile $fullPath)) {
+ try {
+ Remove-Pagefile $fullPath -whatif:$check_mode
+ }
+ catch {
+ Fail-Json $result "Failed to remove current pagefile $($_.Exception.Message)"
+ }
+ $result.changed = $true
+ }
+ }
+
+ # Make sure drive is accessible
+ if (($test_path) -and (-not (Test-Path -LiteralPath "${drive}:"))) {
+ Fail-Json $result "Unable to access '${drive}:' drive"
+ }
+
+ $curPagefile = Get-Pagefile $fullPath
+
+ # Set pagefile
+ if ($null -eq $curPagefile) {
+ try {
+ $pagefile = New-CIMInstance -Class Win32_PageFileSetting -Arguments @{name = $fullPath; } -WhatIf:$check_mode
+ }
+ catch {
+ Fail-Json $result "Failed to create pagefile $($_.Exception.Message)"
+ }
+ if (-not ($systemManaged -or $check_mode)) {
+ try {
+ $pagefile | Set-CimInstance -Property @{ InitialSize = $initialSize; MaximumSize = $maximumSize }
+ }
+ catch {
+ $originalExceptionMessage = $($_.Exception.Message)
+ # Try workaround before failing
+ try {
+ Remove-Pagefile $fullPath -whatif:$check_mode
+ }
+ catch {
+ Fail-Json $result "Failed to remove pagefile before workaround $($_.Exception.Message) Original exception: $originalExceptionMessage"
+ }
+ try {
+ $pagingFilesValues = (Get-ItemProperty -LiteralPath $regPath).PagingFiles
+ }
+ catch {
+ $msg = -join @(
+ "Failed to get pagefile settings from the registry for workaround $($_.Exception.Message) "
+ "Original exception: $originalExceptionMessage"
+ )
+ Fail-Json $result $msg
+ }
+ $pagingFilesValues += "$fullPath $initialSize $maximumSize"
+ try {
+ Set-ItemProperty -LiteralPath $regPath "PagingFiles" $pagingFilesValues
+ }
+ catch {
+ $msg = -join @(
+ "Failed to set pagefile settings to the registry for workaround $($_.Exception.Message) "
+ "Original exception: $originalExceptionMessage"
+ )
+ Fail-Json $result $msg
+ }
+ }
+ }
+ $result.changed = $true
+ }
+ else {
+ if ((-not $check_mode) -and
+ -not ($systemManaged) -and
+ -not ( ($curPagefile.InitialSize -eq 0) -and ($curPagefile.maximumSize -eq 0) ) -and
+ ( ($curPagefile.InitialSize -ne $initialSize) -or ($curPagefile.maximumSize -ne $maximumSize) )
+ ) {
+ $curPagefile.InitialSize = $initialSize
+ $curPagefile.MaximumSize = $maximumSize
+ try {
+ $curPagefile.Put() | out-null
+ }
+ catch {
+ $originalExceptionMessage = $($_.Exception.Message)
+ # Try workaround before failing
+ try {
+ Remove-Pagefile $fullPath -whatif:$check_mode
+ }
+ catch {
+ Fail-Json $result "Failed to remove pagefile before workaround $($_.Exception.Message) Original exception: $originalExceptionMessage"
+ }
+ try {
+ $pagingFilesValues = (Get-ItemProperty -LiteralPath $regPath).PagingFiles
+ }
+ catch {
+ $msg = -join @(
+ "Failed to get pagefile settings from the registry for workaround $($_.Exception.Message) "
+ "Original exception: $originalExceptionMessage"
+ )
+ Fail-Json $result $msg
+ }
+ $pagingFilesValues += "$fullPath $initialSize $maximumSize"
+ try {
+
+ Set-ItemProperty -LiteralPath $regPath -Name "PagingFiles" -Value $pagingFilesValues
+ }
+ catch {
+ $msg = -join @(
+ "Failed to set pagefile settings to the registry for workaround $($_.Exception.Message) "
+ "Original exception: $originalExceptionMessage"
+ )
+ Fail-Json $result $msg
+ }
+ }
+ $result.changed = $true
+ }
+ }
+}
+elseif ($state -eq "query") {
+ $result.pagefiles = @()
+
+ if ($null -eq $drive) {
+ try {
+ $pagefiles = Get-CIMInstance Win32_PageFileSetting
+ }
+ catch {
+ Fail-Json $result "Failed to query all pagefiles $($_.Exception.Message)"
+ }
+ }
+ else {
+ try {
+ $pagefiles = Get-Pagefile $fullPath
+ }
+ catch {
+ Fail-Json $result "Failed to query specific pagefile $($_.Exception.Message)"
+ }
+ }
+
+ # Get all pagefiles
+ foreach ($currentPagefile in $pagefiles) {
+ $currentPagefileObject = @{
+ name = $currentPagefile.Name
+ initial_size = $currentPagefile.InitialSize
+ maximum_size = $currentPagefile.MaximumSize
+ caption = $currentPagefile.Caption
+ description = $currentPagefile.Description
+ }
+ $result.pagefiles += , $currentPagefileObject
+ }
+
+ # Get automatic managed pagefile state
+ try {
+ $result.automatic_managed_pagefiles = (Get-CIMInstance -Class win32_computersystem).AutomaticManagedPagefile
+ }
+ catch {
+ Fail-Json $result "Failed to query automatic managed pagefile state $($_.Exception.Message)"
+ }
+}
+Exit-Json $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_pagefile.py b/ansible_collections/community/windows/plugins/modules/win_pagefile.py
new file mode 100644
index 00000000..c8b75220
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_pagefile.py
@@ -0,0 +1,131 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2017, Liran Nisanov <lirannis@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_pagefile
+short_description: Query or change pagefile configuration
+description:
+ - Query current pagefile configuration.
+ - Enable/Disable AutomaticManagedPagefile.
+ - Create new or override pagefile configuration.
+options:
+ drive:
+ description:
+ - The drive of the pagefile.
+ type: str
+ initial_size:
+ description:
+ - The initial size of the pagefile in megabytes.
+ type: int
+ maximum_size:
+ description:
+ - The maximum size of the pagefile in megabytes.
+ type: int
+ override:
+ description:
+ - Override the current pagefile on the drive.
+ type: bool
+ default: yes
+ system_managed:
+ description:
+ - Configures current pagefile to be managed by the system.
+ type: bool
+ default: no
+ automatic:
+ description:
+ - Configures AutomaticManagedPagefile for the entire system.
+ type: bool
+ remove_all:
+ description:
+ - Remove all pagefiles in the system, not including automatic managed.
+ type: bool
+ default: no
+ test_path:
+ description:
+ - Use Test-Path on the drive to make sure the drive is accessible before creating the pagefile.
+ type: bool
+ default: yes
+ state:
+ description:
+ - State of the pagefile.
+ type: str
+ choices: [ absent, present, query ]
+ default: query
+notes:
+- There is difference between automatic managed pagefiles that configured once for the entire system and system managed pagefile that configured per pagefile.
+- InitialSize 0 and MaximumSize 0 means the pagefile is managed by the system.
+- Value out of range exception may be caused by several different issues, two common problems - No such drive, Pagefile size is too small.
+- Setting a pagefile when AutomaticManagedPagefile is on will disable the AutomaticManagedPagefile.
+author:
+- Liran Nisanov (@LiranNis)
+'''
+
+EXAMPLES = r'''
+- name: Query pagefiles configuration
+ community.windows.win_pagefile:
+
+- name: Query C pagefile
+ community.windows.win_pagefile:
+ drive: C
+
+- name: Set C pagefile, don't override if exists
+ community.windows.win_pagefile:
+ drive: C
+ initial_size: 1024
+ maximum_size: 1024
+ override: no
+ state: present
+
+- name: Set C pagefile, override if exists
+ community.windows.win_pagefile:
+ drive: C
+ initial_size: 1024
+ maximum_size: 1024
+ state: present
+
+- name: Remove C pagefile
+ community.windows.win_pagefile:
+ drive: C
+ state: absent
+
+- name: Remove all current pagefiles, enable AutomaticManagedPagefile and query at the end
+ community.windows.win_pagefile:
+ remove_all: yes
+ automatic: yes
+
+- name: Remove all pagefiles disable AutomaticManagedPagefile and set C pagefile
+ community.windows.win_pagefile:
+ drive: C
+ initial_size: 2048
+ maximum_size: 2048
+ remove_all: yes
+ automatic: no
+ state: present
+
+- name: Set D pagefile, override if exists
+ community.windows.win_pagefile:
+ drive: d
+ initial_size: 1024
+ maximum_size: 1024
+ state: present
+'''
+
+RETURN = r'''
+automatic_managed_pagefiles:
+ description: Whether the pagefiles is automatically managed.
+ returned: When state is query.
+ type: bool
+ sample: true
+pagefiles:
+ description: Contains caption, description, initial_size, maximum_size and name for each pagefile in the system.
+ returned: When state is query.
+ type: list
+ sample:
+ [{"caption": "c:\\ 'pagefile.sys'", "description": "'pagefile.sys' @ c:\\", "initial_size": 2048, "maximum_size": 2048, "name": "c:\\pagefile.sys"},
+ {"caption": "d:\\ 'pagefile.sys'", "description": "'pagefile.sys' @ d:\\", "initial_size": 1024, "maximum_size": 1024, "name": "d:\\pagefile.sys"}]
+
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_partition.ps1 b/ansible_collections/community/windows/plugins/modules/win_partition.ps1
new file mode 100644
index 00000000..6e451769
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_partition.ps1
@@ -0,0 +1,371 @@
+#!powershell
+
+# Copyright: (c) 2018, Varun Chopra (@chopraaa) <v@chopraaa.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#AnsibleRequires -OSVersion 6.2
+
+Set-StrictMode -Version 2
+
+$ErrorActionPreference = "Stop"
+
+$spec = @{
+ options = @{
+ state = @{ type = "str"; choices = "absent", "present"; default = "present" }
+ drive_letter = @{ type = "str" }
+ disk_number = @{ type = "int" }
+ partition_number = @{ type = "int" }
+ partition_size = @{ type = "str" }
+ read_only = @{ type = "bool" }
+ active = @{ type = "bool" }
+ hidden = @{ type = "bool" }
+ offline = @{ type = "bool" }
+ mbr_type = @{ type = "str"; choices = "fat12", "fat16", "extended", "huge", "ifs", "fat32" }
+ gpt_type = @{ type = "str"; choices = "system_partition", "microsoft_reserved", "basic_data", "microsoft_recovery" }
+ }
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$state = $module.Params.state
+$drive_letter = $module.Params.drive_letter
+$disk_number = $module.Params.disk_number
+$partition_number = $module.Params.partition_number
+$partition_size = $module.Params.partition_size
+$read_only = $module.Params.read_only
+$active = $module.Params.active
+$hidden = $module.Params.hidden
+$offline = $module.Params.offline
+$mbr_type = $module.Params.mbr_type
+$gpt_type = $module.Params.gpt_type
+
+$size_is_maximum = $false
+$ansible_partition = $false
+$ansible_partition_size = $null
+$partition_style = $null
+
+$gpt_styles = @{
+ system_partition = "{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}"
+ microsoft_reserved = "{e3c9e316-0b5c-4db8-817d-f92df00215ae}"
+ basic_data = "{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}"
+ microsoft_recovery = "{de94bba4-06d1-4d40-a16a-bfd50179d6ac}"
+}
+
+$mbr_styles = @{
+ fat12 = 1
+ fat16 = 4
+ extended = 5
+ huge = 6
+ ifs = 7
+ fat32 = 12
+}
+
+function Convert-SizeToByte {
+ param(
+ $Size,
+ $Units
+ )
+
+ switch ($Units) {
+ "B" { return 1 * $Size }
+ "KB" { return 1000 * $Size }
+ "KiB" { return 1024 * $Size }
+ "MB" { return [Math]::Pow(1000, 2) * $Size }
+ "MiB" { return [Math]::Pow(1024, 2) * $Size }
+ "GB" { return [Math]::Pow(1000, 3) * $Size }
+ "GiB" { return [Math]::Pow(1024, 3) * $Size }
+ "TB" { return [Math]::Pow(1000, 4) * $Size }
+ "TiB" { return [Math]::Pow(1024, 4) * $Size }
+ }
+}
+
+if ($null -ne $partition_size) {
+ if ($partition_size -eq -1) {
+ $size_is_maximum = $true
+ }
+ elseif ($partition_size -match '^(?<Size>[0-9]+)[ ]*(?<Units>b|kb|kib|mb|mib|gb|gib|tb|tib)$') {
+ $ansible_partition_size = Convert-SizeToByte -Size $Matches.Size -Units $Matches.Units
+ }
+ else {
+ $module.FailJson("Invalid partition size. B, KB, KiB, MB, MiB, GB, GiB, TB, TiB are valid partition size units")
+ }
+}
+
+# If partition_exists, we can change or delete it; otherwise we only need the disk to create a new partition
+if ($null -ne $disk_number -and $null -ne $partition_number) {
+ $ansible_partition = Get-Partition -DiskNumber $disk_number -PartitionNumber $partition_number -ErrorAction SilentlyContinue
+}
+# Check if drive_letter is either auto-assigned or a character from A-Z
+elseif ($drive_letter -and $drive_letter -ne "auto" -and -not ($disk_number -and $partition_number)) {
+ if ($drive_letter -match "^[a-zA-Z]$") {
+ $ansible_partition = Get-Partition -DriveLetter $drive_letter -ErrorAction SilentlyContinue
+ }
+ else {
+ $module.FailJson("Incorrect usage of drive_letter: specify a drive letter from A-Z or use 'auto' to automatically assign a drive letter")
+ }
+}
+elseif ($disk_number) {
+ try {
+ Get-Disk -Number $disk_number | Out-Null
+ }
+ catch {
+ $module.FailJson("Specified disk does not exist")
+ }
+}
+else {
+ $module.FailJson("You must provide disk_number, partition_number")
+}
+
+# Partition can't have two partition styles
+if ($null -ne $gpt_type -and $null -ne $mbr_type) {
+ $module.FailJson("Cannot specify both GPT and MBR partition styles. Check which partition style is supported by the disk")
+}
+
+function New-AnsiblePartition {
+ param(
+ $DiskNumber,
+ $Letter,
+ $SizeMax,
+ $Size,
+ $MbrType,
+ $GptType,
+ $Style
+ )
+
+ $parameters = @{
+ DiskNumber = $DiskNumber
+ }
+
+ if ($null -ne $Letter) {
+ switch ($Letter) {
+ "auto" {
+ $parameters.Add("AssignDriveLetter", $True)
+ }
+ default {
+ $parameters.Add("DriveLetter", $Letter)
+ }
+ }
+ }
+
+ if ($null -ne $Size) {
+ $parameters.Add("Size", $Size)
+ }
+
+ if ($null -ne $MbrType) {
+ $parameters.Add("MbrType", $Style)
+ }
+
+ if ($null -ne $GptType) {
+ $parameters.Add("GptType", $Style)
+ }
+
+ try {
+ $new_partition = New-Partition @parameters
+ }
+ catch {
+ $module.FailJson("Unable to create a new partition: $($_.Exception.Message)", $_)
+ }
+
+ return $new_partition
+}
+
+
+function Set-AnsiblePartitionState {
+ param(
+ $hidden,
+ $read_only,
+ $active,
+ $partition
+ )
+
+ $parameters = @{
+ DiskNumber = $partition.DiskNumber
+ PartitionNumber = $partition.PartitionNumber
+ }
+
+ if ($hidden -NotIn ($null, $partition.IsHidden)) {
+ $parameters.Add("IsHidden", $hidden)
+ }
+
+ if ($read_only -NotIn ($null, $partition.IsReadOnly)) {
+ $parameters.Add("IsReadOnly", $read_only)
+ }
+
+ if ($active -NotIn ($null, $partition.IsActive)) {
+ $parameters.Add("IsActive", $active)
+ }
+
+ try {
+ Set-Partition @parameters
+ }
+ catch {
+ $module.FailJson("Error changing state of partition: $($_.Exception.Message)", $_)
+ }
+}
+
+
+if ($ansible_partition) {
+ if ($state -eq "absent") {
+ try {
+ $remove_params = @{
+ DiskNumber = $ansible_partition.DiskNumber
+ PartitionNumber = $ansible_partition.PartitionNumber
+ Confirm = $false
+ WhatIf = $module.CheckMode
+ }
+ Remove-Partition @remove_params
+ }
+ catch {
+ $module.FailJson("There was an error removing the partition: $($_.Exception.Message)", $_)
+ }
+ $module.Result.changed = $true
+ }
+ else {
+
+ if ($null -ne $gpt_type -and $gpt_styles.$gpt_type -ne $ansible_partition.GptType) {
+ $module.FailJson("gpt_type is not a valid parameter for existing partitions")
+ }
+ if ($null -ne $mbr_type -and $mbr_styles.$mbr_type -ne $ansible_partition.MbrType) {
+ $module.FailJson("mbr_type is not a valid parameter for existing partitions")
+ }
+
+ if ($partition_size) {
+ try {
+ $get_params = @{
+ DiskNumber = $ansible_partition.DiskNumber
+ PartitionNumber = $ansible_partition.PartitionNumber
+ }
+ $max_supported_size = (Get-PartitionSupportedSize @get_params).SizeMax
+ }
+ catch {
+ $module.FailJson("Unable to get maximum supported partition size: $($_.Exception.Message)", $_)
+ }
+ if ($size_is_maximum) {
+ $ansible_partition_size = $max_supported_size
+ }
+ if (
+ $ansible_partition_size -ne $ansible_partition.Size -and
+ ($ansible_partition_size - $ansible_partition.Size -gt 1049000 -or $ansible_partition.Size - $ansible_partition_size -gt 1049000)
+ ) {
+ if ($ansible_partition.IsReadOnly) {
+ $module.FailJson("Unable to resize partition: Partition is read only")
+ }
+ else {
+ try {
+ $resize_params = @{
+ DiskNumber = $ansible_partition.DiskNumber
+ PartitionNumber = $ansible_partition.PartitionNumber
+ Size = $ansible_partition_size
+ WhatIf = $module.CheckMode
+ }
+ Resize-Partition @resize_params
+ }
+ catch {
+ $module.FailJson("Unable to change partition size: $($_.Exception.Message)", $_)
+ }
+ $module.Result.changed = $true
+ }
+ }
+ elseif ($ansible_partition_size -gt $max_supported_size) {
+ $module.FailJson("Specified partition size exceeds size supported by the partition")
+ }
+ }
+
+ if ($drive_letter -NotIn ("auto", $null, $ansible_partition.DriveLetter)) {
+ if (-not $module.CheckMode) {
+ try {
+ Set-Partition -DiskNumber $ansible_partition.DiskNumber -PartitionNumber $ansible_partition.PartitionNumber -NewDriveLetter $drive_letter
+ }
+ catch {
+ $module.FailJson("Unable to change drive letter: $($_.Exception.Message)", $_)
+ }
+ }
+ $module.Result.changed = $true
+ }
+ }
+}
+else {
+ if ($state -eq "present") {
+ if ($null -eq $disk_number) {
+ $module.FailJson("Missing required parameter: disk_number")
+ }
+ if ($null -eq $ansible_partition_size -and -not $size_is_maximum) {
+ $module.FailJson("Missing required parameter: partition_size")
+ }
+ if (-not $size_is_maximum) {
+ try {
+ $max_supported_size = (Get-Disk -Number $disk_number).LargestFreeExtent
+ }
+ catch {
+ $module.FailJson("Unable to get maximum size supported by disk: $($_.Exception.Message)", $_)
+ }
+
+ if ($ansible_partition_size -gt $max_supported_size) {
+ $module.FailJson("Partition size is not supported by disk. Use partition_size: -1 to get maximum size")
+ }
+ }
+ else {
+ $ansible_partition_size = (Get-Disk -Number $disk_number).LargestFreeExtent
+ }
+
+ $supp_part_type = (Get-Disk -Number $disk_number).PartitionStyle
+ if ($null -ne $mbr_type) {
+ if ($supp_part_type -eq "MBR" -and $mbr_styles.ContainsKey($mbr_type)) {
+ $partition_style = $mbr_styles.$mbr_type
+ }
+ else {
+ $module.FailJson("Incorrect partition style specified")
+ }
+ }
+ if ($null -ne $gpt_type) {
+ if ($supp_part_type -eq "GPT" -and $gpt_styles.ContainsKey($gpt_type)) {
+ $partition_style = $gpt_styles.$gpt_type
+ }
+ else {
+ $module.FailJson("Incorrect partition style specified")
+ }
+ }
+
+ if (-not $module.CheckMode) {
+ $new_params = @{
+ DiskNumber = $disk_number
+ Letter = $drive_letter
+ Size = $ansible_partition_size
+ MbrType = $mbr_type
+ GptType = $gpt_type
+ Style = $partition_style
+ }
+ $ansible_partition = New-AnsiblePartition @new_params
+ }
+ $module.Result.changed = $true
+ }
+}
+
+if ($state -eq "present" -and $ansible_partition) {
+ if ($offline -NotIn ($null, $ansible_partition.IsOffline)) {
+ if (-not $module.CheckMode) {
+ try {
+ Set-Partition -DiskNumber $ansible_partition.DiskNumber -PartitionNumber $ansible_partition.PartitionNumber -IsOffline $offline
+ }
+ catch {
+ $module.FailJson("Error setting partition offline: $($_.Exception.Message)", $_)
+ }
+ }
+ $module.Result.changed = $true
+ }
+
+ if (
+ $hidden -NotIn ($null, $ansible_partition.IsHidden) -or
+ $read_only -NotIn ($null, $ansible_partition.IsReadOnly) -or
+ $active -NotIn ($null, $ansible_partition.IsActive)
+ ) {
+ if (-not $module.CheckMode) {
+ Set-AnsiblePartitionState -hidden $hidden -read_only $read_only -active $active -partition $ansible_partition
+ }
+ $module.Result.changed = $true
+ }
+}
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_partition.py b/ansible_collections/community/windows/plugins/modules/win_partition.py
new file mode 100644
index 00000000..5f8a46f6
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_partition.py
@@ -0,0 +1,110 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Varun Chopra (@chopraaa) <v@chopraaa.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_partition
+short_description: Creates, changes and removes partitions on Windows Server
+description:
+ - The M(community.windows.win_partition) module can create, modify or delete a partition on a disk
+options:
+ state:
+ description:
+ - Used to specify the state of the partition. Use C(absent) to specify if a partition should be removed
+ and C(present) to specify if the partition should be created or updated.
+ type: str
+ choices: [ absent, present]
+ default: present
+ drive_letter:
+ description:
+ - Used for accessing partitions if I(disk_number) and I(partition_number) are not provided.
+ - Use C(auto) for automatically assigning a drive letter, or a letter A-Z for manually assigning a drive letter to a new partition.
+ If not specified, no drive letter is assigned when creating a new partition.
+ type: str
+ disk_number:
+ description:
+ - Disk number is mandatory for creating new partitions.
+ - A combination of I(disk_number) and I(partition_number) can be used to specify the partition instead of I(drive_letter) if required.
+ type: int
+ partition_number:
+ description:
+ - Used in conjunction with I(disk_number) to uniquely identify a partition.
+ type: int
+ partition_size:
+ description:
+ - Specify size of the partition in B, KB, KiB, MB, MiB, GB, GiB, TB or TiB. Use -1 to specify maximum supported size.
+ - Partition size is mandatory for creating a new partition but not for updating or deleting a partition.
+ - The decimal SI prefixes kilo, mega, giga, tera, etc., are powers of 10^3 = 1000. The binary prefixes kibi, mebi, gibi, tebi, etc.
+ respectively refer to the corresponding power of 2^10 = 1024.
+ Thus, a gigabyte (GB) is 1000000000 (1000^3) bytes while 1 gibibyte (GiB) is 1073741824 (1024^3) bytes.
+ type: str
+ read_only:
+ description:
+ - Make the partition read only, restricting changes from being made to the partition.
+ type: bool
+ active:
+ description:
+ - Specifies if the partition is active and can be used to start the system. This property is only valid when the disk's partition style is MBR.
+ type: bool
+ hidden:
+ description:
+ - Hides the target partition, making it undetectable by the mount manager.
+ type: bool
+ offline:
+ description:
+ - Sets the partition offline.
+ - Adding a mount point (such as a drive letter) will cause the partition to go online again.
+ type: bool
+ required: no
+ mbr_type:
+ description:
+ - Specify the partition's MBR type if the disk's partition style is MBR.
+ - This only applies to new partitions.
+ - This does not relate to the partitions file system formatting.
+ type: str
+ choices: [ fat12, fat16, extended, huge, ifs, fat32 ]
+ gpt_type:
+ description:
+ - Specify the partition's GPT type if the disk's partition style is GPT.
+ - This only applies to new partitions.
+ - This does not relate to the partitions file system formatting.
+ type: str
+ choices: [ system_partition, microsoft_reserved, basic_data, microsoft_recovery ]
+
+notes:
+ - A minimum Operating System Version of 6.2 is required to use this module. To check if your OS is compatible, see
+ U(https://docs.microsoft.com/en-us/windows/desktop/sysinfo/operating-system-version).
+ - This module cannot be used for removing the drive letter associated with a partition, initializing a disk or, file system formatting.
+ - Idempotence works only if you're specifying a drive letter or other unique attributes such as a combination of disk number and partition number.
+ - For more information, see U(https://msdn.microsoft.com/en-us/library/windows/desktop/hh830524.aspx).
+author:
+ - Varun Chopra (@chopraaa) <v@chopraaa.com>
+'''
+
+EXAMPLES = r'''
+- name: Create a partition with drive letter D and size 5 GiB
+ community.windows.win_partition:
+ drive_letter: D
+ partition_size: 5 GiB
+ disk_number: 1
+
+- name: Resize previously created partition to it's maximum size and change it's drive letter to E
+ community.windows.win_partition:
+ drive_letter: E
+ partition_size: -1
+ partition_number: 1
+ disk_number: 1
+
+- name: Delete partition
+ community.windows.win_partition:
+ disk_number: 1
+ partition_number: 1
+ state: absent
+'''
+
+RETURN = r'''
+#
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_pester.ps1 b/ansible_collections/community/windows/plugins/modules/win_pester.ps1
new file mode 100644
index 00000000..5ffcf5b7
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_pester.ps1
@@ -0,0 +1,125 @@
+#!powershell
+
+# Copyright: (c) 2017, Erwan Quelin (@equelin) <erwan.quelin@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+$spec = @{
+ options = @{
+ output_file = @{ type = "str" }
+ output_format = @{ type = "str"; default = "NunitXML" }
+ path = @{ type = "str"; required = $true }
+ tags = @{ type = "list"; elements = "str" }
+ test_parameters = @{ type = "dict" }
+ version = @{ type = "str"; aliases = @(, "minimum_version") }
+ }
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$output_file = $module.Params.output_file
+$output_format = $module.Params.output_format
+$path = $module.Params.path
+$tags = $module.Params.tags
+$test_parameters = $module.Params.test_parameters
+$version = $module.Params.version
+
+Try {
+ $version = [version]$version
+}
+Catch {
+ $module.FailJson("Value '$version' for parameter 'minimum_version' is not a valid version format")
+}
+
+# Make sure path is a real path
+Try {
+ $path = $path.TrimEnd("\")
+ $path = (Get-item -LiteralPath $path).FullName
+}
+Catch {
+ $module.FailJson("Cannot find file or directory: '$path' as it does not exist")
+}
+
+# Import Pester module if available
+$Pester = 'Pester'
+
+If (-not (Get-Module -Name $Pester -ErrorAction SilentlyContinue)) {
+ If (Get-Module -Name $Pester -ListAvailable -ErrorAction SilentlyContinue) {
+ Import-Module $Pester
+ }
+ else {
+ $msg = -join @(
+ "Cannot find module: $Pester. Check if pester is installed, and if it is not, "
+ "install using win_psmodule or chocolatey.chocolatey.win_chocolatey."
+ )
+ $module.FailJson($msg)
+ }
+}
+
+# Add actual pester's module version in the ansible's result variable
+$Pester_version = (Get-Module -Name $Pester).Version.ToString()
+$module.Result.pester_version = $Pester_version
+
+# Test if the Pester module is available with a version greater or equal than the one specified in the $version parameter
+If ((-not (Get-Module -Name $Pester -ErrorAction SilentlyContinue | Where-Object { $_.Version -ge $version })) -and ($version)) {
+ $module.FailJson("$Pester version is not greater or equal to $version")
+}
+
+#Prepare Invoke-Pester parameters depending of the Pester's version.
+#Invoke-Pester output deactivation behave differently depending on the Pester's version
+If ($module.Result.pester_version -ge "4.0.0") {
+ $Parameters = @{
+ "show" = "none"
+ "PassThru" = $True
+ }
+}
+else {
+ $Parameters = @{
+ "quiet" = $True
+ "PassThru" = $True
+ }
+}
+
+if ($tags.count) {
+ $Parameters.Tag = $tags
+}
+
+if ($output_file) {
+ $Parameters.OutputFile = $output_file
+ $Parameters.OutputFormat = $output_format
+}
+
+# Run Pester tests
+If (Test-Path -LiteralPath $path -PathType Leaf) {
+ $test_parameters_check_mode_msg = ''
+ if ($test_parameters.keys.count) {
+ $Parameters.Script = @{Path = $Path ; Parameters = $test_parameters }
+ $test_parameters_check_mode_msg = " with $($test_parameters.keys -join ',') parameters"
+ }
+ else {
+ $Parameters.Script = $Path
+ }
+
+ if ($module.CheckMode) {
+ $module.Result.output = "Run pester test in the file: $path$test_parameters_check_mode_msg"
+ }
+ else {
+ $module.Result.output = Invoke-Pester @Parameters
+ }
+}
+else {
+ $Parameters.Script = $path
+
+ if ($module.CheckMode) {
+ $module.Result.output = "Run Pester test(s): $path"
+ }
+ else {
+ $module.Result.output = Invoke-Pester @Parameters
+ }
+}
+
+$module.Result.changed = $true
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_pester.py b/ansible_collections/community/windows/plugins/modules/win_pester.py
new file mode 100644
index 00000000..83cc74fa
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_pester.py
@@ -0,0 +1,104 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_pester
+short_description: Run Pester tests on Windows hosts
+description:
+ - Run Pester tests on Windows hosts.
+ - Test files have to be available on the remote host.
+requirements:
+ - Pester
+options:
+ path:
+ description:
+ - Path to a pester test file or a folder where tests can be found.
+ - If the path is a folder, the module will consider all ps1 files as Pester tests.
+ type: str
+ required: true
+ tags:
+ description:
+ - Runs only tests in Describe blocks with specified Tags values.
+ - Accepts multiple comma separated tags.
+ type: list
+ elements: str
+ output_file:
+ description:
+ - Generates an output test report.
+ type: str
+ output_format:
+ description:
+ - Format of the test report to be generated.
+ - This parameter is to be used with output_file option.
+ type: str
+ default: NunitXML
+ test_parameters:
+ description:
+ - Allows to specify parameters to the test script.
+ type: dict
+ version:
+ description:
+ - Minimum version of the pester module that has to be available on the remote host.
+ type: str
+ aliases:
+ - minimum_version
+author:
+ - Erwan Quelin (@equelin)
+ - Prasoon Karunan V (@prasoonkarunan)
+'''
+
+EXAMPLES = r'''
+- name: Get facts
+ ansible.windows.setup:
+
+- name: Add Pester module
+ action:
+ module_name: "{{ 'community.windows.win_psmodule' if ansible_powershell_version >= 5 else 'chocolatey.chocolatey.win_chocolatey' }}"
+ name: Pester
+ state: present
+
+- name: Run the pester test provided in the path parameter.
+ community.windows.win_pester:
+ path: C:\Pester
+
+- name: Run the pester tests only for the tags specified.
+ community.windows.win_pester:
+ path: C:\Pester\TestScript.tests
+ tags: CI,UnitTests
+
+# Run pesters tests files that are present in the specified folder
+# ensure that the pester module version available is greater or equal to the version parameter.
+- name: Run the pester test present in a folder and check the Pester module version.
+ community.windows.win_pester:
+ path: C:\Pester\test01.test.ps1
+ version: 4.1.0
+
+- name: Run the pester test present in a folder with given script parameters.
+ community.windows.win_pester:
+ path: C:\Pester\test04.test.ps1
+ test_parameters:
+ Process: lsass
+ Service: bits
+
+- name: Run the pester test present in a folder and generate NunitXML test result..
+ community.windows.win_pester:
+ path: C:\Pester\test04.test.ps1
+ output_file: c:\Pester\resullt\testresult.xml
+'''
+
+RETURN = r'''
+pester_version:
+ description: Version of the pester module found on the remote host.
+ returned: always
+ type: str
+ sample: 4.3.1
+output:
+ description: Results of the Pester tests.
+ returned: success
+ type: list
+ sample: false
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_power_plan.ps1 b/ansible_collections/community/windows/plugins/modules/win_power_plan.ps1
new file mode 100644
index 00000000..0ebad630
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_power_plan.ps1
@@ -0,0 +1,231 @@
+#!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.AddType
+
+$spec = @{
+ options = @{
+ name = @{ type = "str"; }
+ guid = @{ type = "str"; }
+ }
+ required_one_of = @(
+ , @('name', 'guid')
+ )
+ supports_check_mode = $true
+}
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$name = $module.Params.name
+$guid = $module.Params.guid
+$module.Result.power_plan_name = $name
+$module.Result.power_plan_enabled = $null
+$module.Result.all_available_plans = $null
+
+Add-CSharpType -References @"
+using System;
+using System.Runtime.InteropServices;
+
+namespace Ansible.WinPowerPlan
+{
+ public enum AccessFlags : uint
+ {
+ AccessScheme = 16,
+ AccessSubgroup = 17,
+ AccessIndividualSetting = 18
+ }
+
+ public class NativeMethods
+ {
+ [DllImport("Kernel32.dll", SetLastError = true)]
+ public static extern IntPtr LocalFree(
+ IntPtr hMen);
+
+ [DllImport("PowrProf.dll")]
+ public static extern UInt32 PowerEnumerate(
+ IntPtr RootPowerKey,
+ IntPtr SchemeGuid,
+ IntPtr SubGroupOfPowerSettingsGuid,
+ AccessFlags AccessFlags,
+ UInt32 Index,
+ IntPtr Buffer,
+ ref UInt32 BufferSize);
+
+ [DllImport("PowrProf.dll")]
+ public static extern UInt32 PowerGetActiveScheme(
+ IntPtr UserRootPowerKey,
+ out IntPtr ActivePolicyGuid);
+
+ [DllImport("PowrProf.dll")]
+ public static extern UInt32 PowerReadFriendlyName(
+ IntPtr RootPowerKey,
+ Guid SchemeGuid,
+ IntPtr SubGroupOfPowerSettingsGuid,
+ IntPtr PowerSettingGuid,
+ IntPtr Buffer,
+ ref UInt32 BufferSize);
+
+ [DllImport("PowrProf.dll")]
+ public static extern UInt32 PowerSetActiveScheme(
+ IntPtr UserRootPowerKey,
+ Guid SchemeGuid);
+ }
+}
+"@
+
+Function Get-LastWin32ErrorMessage {
+ param([Int]$ErrorCode)
+ $exp = New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList $ErrorCode
+ $error_msg = "{0} - (Win32 Error Code {1} - 0x{1:X8})" -f $exp.Message, $ErrorCode
+ return $error_msg
+}
+
+Function Get-PlanName {
+ param([Guid]$Plan)
+
+ $buffer_size = 0
+ $buffer = [IntPtr]::Zero
+ [Ansible.WinPowerPlan.NativeMethods]::PowerReadFriendlyName([IntPtr]::Zero, $Plan, [IntPtr]::Zero, [IntPtr]::Zero,
+ $buffer, [ref]$buffer_size) > $null
+
+ $buffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($buffer_size)
+ try {
+ $res = [Ansible.WinPowerPlan.NativeMethods]::PowerReadFriendlyName([IntPtr]::Zero, $Plan, [IntPtr]::Zero,
+ [IntPtr]::Zero, $buffer, [ref]$buffer_size)
+
+ if ($res -ne 0) {
+ $err_msg = Get-LastWin32ErrorMessage -ErrorCode $res
+ $module.FailJson("Failed to get name for power scheme $Plan - $err_msg")
+ }
+
+ return [System.Runtime.InteropServices.Marshal]::PtrToStringUni($buffer)
+ }
+ finally {
+ [System.Runtime.InteropServices.Marshal]::FreeHGlobal($buffer)
+ }
+}
+
+Function Get-PowerPlan {
+ $plans = @{}
+
+ $i = 0
+ while ($true) {
+ $buffer_size = 0
+ $buffer = [IntPtr]::Zero
+ $res = [Ansible.WinPowerPlan.NativeMethods]::PowerEnumerate([IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero,
+ [Ansible.WinPowerPlan.AccessFlags]::AccessScheme, $i, $buffer, [ref]$buffer_size)
+
+ if ($res -eq 259) {
+ # 259 == ERROR_NO_MORE_ITEMS, there are no more power plans to enumerate
+ break
+ }
+ elseif ($res -notin @(0, 234)) {
+ # 0 == ERROR_SUCCESS and 234 == ERROR_MORE_DATA
+ $err_msg = Get-LastWin32ErrorMessage -ErrorCode $res
+ $module.FailJson("Failed to get buffer size on local power schemes at index $i - $err_msg")
+ }
+
+ $buffer = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($buffer_size)
+ try {
+ $res = [Ansible.WinPowerPlan.NativeMethods]::PowerEnumerate([IntPtr]::Zero, [IntPtr]::Zero, [IntPtr]::Zero,
+ [Ansible.WinPowerPlan.AccessFlags]::AccessScheme, $i, $buffer, [ref]$buffer_size)
+
+ if ($res -eq 259) {
+ # Server 2008 does not return 259 in the first call above so we do an additional check here
+ break
+ }
+ elseif ($res -notin @(0, 234, 259)) {
+ $err_msg = Get-LastWin32ErrorMessage -ErrorCode $res
+ $module.FailJson("Failed to enumerate local power schemes at index $i - $err_msg")
+ }
+ $scheme_guid = [System.Runtime.InteropServices.Marshal]::PtrToStructure($buffer, [Type][Guid])
+ }
+ finally {
+ [System.Runtime.InteropServices.Marshal]::FreeHGlobal($buffer)
+ }
+ $scheme_name = Get-PlanName -Plan $scheme_guid
+ $plans.$scheme_name = $scheme_guid
+
+ $i += 1
+ }
+
+ return $plans
+}
+
+Function Get-ActivePowerPlan {
+ $buffer = [IntPtr]::Zero
+ $res = [Ansible.WinPowerPlan.NativeMethods]::PowerGetActiveScheme([IntPtr]::Zero, [ref]$buffer)
+ if ($res -ne 0) {
+ $err_msg = Get-LastWin32ErrorMessage -ErrorCode $res
+ $module.FailJson("Failed to get the active power plan - $err_msg")
+ }
+
+ try {
+ $active_guid = [System.Runtime.InteropServices.Marshal]::PtrToStructure($buffer, [Type][Guid])
+ }
+ finally {
+ [Ansible.WinPowerPlan.NativeMethods]::LocalFree($buffer) > $null
+ }
+
+ return $active_guid
+}
+
+Function Set-ActivePowerPlan {
+ [CmdletBinding(SupportsShouldProcess = $true)]
+ param([Guid]$Plan)
+
+ $res = 0
+ if ($PSCmdlet.ShouldProcess($Plan, "Set Power Plan")) {
+ $res = [Ansible.WinPowerPlan.NativeMethods]::PowerSetActiveScheme([IntPtr]::Zero, $Plan)
+ }
+
+ if ($res -ne 0) {
+ $err_msg = Get-LastWin32ErrorMessage -ErrorCode $res
+ $module.FailJson("Failed to set the active power plan to $Plan - $err_msg")
+ }
+}
+
+# Get all local power plans and the current active plan
+$plans = Get-PowerPlan
+$active_plan = Get-ActivePowerPlan
+$module.Result.all_available_plans = @{}
+foreach ($plan_info in $plans.GetEnumerator()) {
+ $module.Result.all_available_plans.($plan_info.Key) = $plan_info.Value -eq $active_plan
+}
+
+if ($null -ne $name -and $name -notin $plans.Keys) {
+ $module.FailJson("Defined power_plan: ($name) is not available")
+}
+if ($null -ne $guid -and $guid -notin $plans.Values) {
+ $module.FailJson("Defined power_plan: ($guid) is not available")
+}
+if ($null -ne $name) {
+ $plan_guid = $plans.$name
+ $is_active = $active_plan -eq $plans.$name
+}
+if ($null -ne $guid) {
+ $plan_guid = $guid
+ $name = $plans.GetEnumerator() | ForEach-Object {
+ $name = $_.Key
+ if ($Plans.Item($name) -eq $plan_guid) {
+ $name
+ }
+ }
+ $is_active = $active_plan -eq $plans.$name
+}
+
+$module.Result.power_plan_enabled = $is_active
+
+if (-not $is_active) {
+ Set-ActivePowerPlan -Plan $plan_guid -WhatIf:$module.CheckMode
+ $module.Result.changed = $true
+ $module.Result.power_plan_enabled = $true
+ foreach ($plan_info in $plans.GetEnumerator()) {
+ $is_active = $plan_info.Value -eq $plan_guid
+ $module.Result.all_available_plans.($plan_info.Key) = $is_active
+ }
+}
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_power_plan.py b/ansible_collections/community/windows/plugins/modules/win_power_plan.py
new file mode 100644
index 00000000..a4d07c7a
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_power_plan.py
@@ -0,0 +1,68 @@
+#!/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)
+
+DOCUMENTATION = r'''
+---
+module: win_power_plan
+short_description: Changes the power plan of a Windows system
+description:
+ - This module will change the power plan of a Windows system to the defined string.
+ - Windows defaults to C(balanced) which will cause CPU throttling. In some cases it can be preferable
+ to change the mode to C(high performance) to increase CPU performance.
+ - One of I(name) or I(guid) must be provided.
+options:
+ name:
+ description:
+ - String value that indicates the desired power plan by name.
+ - The power plan must already be present on the system.
+ - Commonly there will be options for C(balanced) and C(high performance).
+ type: str
+ required: false
+ guid:
+ description:
+ - String value that indicates the desired power plan by guid.
+ - The power plan must already be present on the system.
+ - For out of box guids see U(https://docs.microsoft.com/en-us/windows/win32/power/power-policy-settings).
+ type: str
+ required: false
+ version_added: 1.9.0
+
+author:
+ - Noah Sparks (@nwsparks)
+'''
+
+EXAMPLES = r'''
+- name: Change power plan to high performance
+ community.windows.win_power_plan:
+ name: high performance
+
+- name: Change power plan to high performance
+ community.windows.win_power_plan:
+ guid: 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c
+'''
+
+RETURN = r'''
+power_plan_name:
+ description: Value of the intended power plan.
+ returned: always
+ type: str
+ sample: balanced
+power_plan_enabled:
+ description: State of the intended power plan.
+ returned: success
+ type: bool
+ sample: true
+all_available_plans:
+ description: The name and enabled state of all power plans.
+ returned: always
+ type: dict
+ sample: |
+ {
+ "High performance": false,
+ "Balanced": true,
+ "Power saver": false
+ }
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_product_facts.ps1 b/ansible_collections/community/windows/plugins/modules/win_product_facts.ps1
new file mode 100644
index 00000000..18519af8
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_product_facts.ps1
@@ -0,0 +1,105 @@
+#!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
+
+# This modules does not accept any options
+$spec = @{
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+# First try to find the product key from ACPI
+try {
+ $product_key = (Get-CimInstance -Class SoftwareLicensingService).OA3xOriginalProductKey
+}
+catch {
+ $product_key = $null
+}
+
+if (-not $product_key) {
+ # Else try to get it from the registry instead
+ try {
+ $data = Get-ItemPropertyValue -LiteralPath "HKLM:\Software\Microsoft\Windows NT\CurrentVersion" -Name DigitalProductId
+ }
+ catch {
+ $data = $null
+ }
+
+ # And for Windows 2008 R2
+ if (-not $data) {
+ try {
+ $data = Get-ItemPropertyValue -LiteralPath "HKLM:\Software\Microsoft\Windows NT\CurrentVersion" -Name DigitalProductId4
+ }
+ catch {
+ $data = $null
+ }
+ }
+
+ if ($data) {
+ $product_key = $null
+ $isWin8 = [int]($data[66] / 6) -band 1
+ $HF7 = 0xF7
+ $data[66] = ($data[66] -band $HF7) -bOr (($isWin8 -band 2) * 4)
+ $hexdata = $data[52..66]
+ $chardata = "B", "C", "D", "F", "G", "H", "J", "K", "M", "P", "Q", "R", "T", "V", "W", "X", "Y", "2", "3", "4", "6", "7", "8", "9"
+
+ # Decode base24 binary data
+ for ($i = 24; $i -ge 0; $i--) {
+ $k = 0
+ for ($j = 14; $j -ge 0; $j--) {
+ $k = $k * 256 -bxor $hexdata[$j]
+ $hexdata[$j] = [math]::truncate($k / 24)
+ $k = $k % 24
+ }
+ $product_key_output = $chardata[$k] + $product_key_output
+ $last = $k
+ }
+
+ $product_key_tmp1 = $product_key_output.SubString(1, $last)
+ $product_key_tmp2 = $product_key_output.SubString(1, $product_key_output.Length - 1)
+ if ($last -eq 0) {
+ $product_key_output = "N" + $product_key_tmp2
+ }
+ else {
+ $product_key_output = $product_key_tmp2.Insert($product_key_tmp2.IndexOf($product_key_tmp1) + $product_key_tmp1.Length, "N")
+ }
+ $num = 0
+ $product_key_split = @()
+ for ($i = 0; $i -le 4; $i++) {
+ $product_key_split += $product_key_output.SubString($num, 5)
+ $num += 5
+ }
+ $product_key = $product_key_split -join "-"
+ }
+}
+
+# Retrieve license information
+$license_info = Get-CimInstance SoftwareLicensingProduct | Where-Object PartialProductKey
+
+$winlicense_status = switch ($license_info.LicenseStatus) {
+ 0 { "Unlicensed" }
+ 1 { "Licensed" }
+ 2 { "OOBGrace" }
+ 3 { "OOTGrace" }
+ 4 { "NonGenuineGrace" }
+ 5 { "Notification" }
+ 6 { "ExtendedGrace" }
+ default { $null }
+}
+
+$winlicense_edition = $license_info.Name
+$winlicense_channel = $license_info.ProductKeyChannel
+
+$module.Result.ansible_facts = @{
+ ansible_os_product_id = (Get-CimInstance Win32_OperatingSystem).SerialNumber
+ ansible_os_product_key = $product_key
+ ansible_os_license_edition = $winlicense_edition
+ ansible_os_license_channel = $winlicense_channel
+ ansible_os_license_status = $winlicense_status
+}
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_product_facts.py b/ansible_collections/community/windows/plugins/modules/win_product_facts.py
new file mode 100644
index 00000000..5353ce55
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_product_facts.py
@@ -0,0 +1,61 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# 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)
+
+DOCUMENTATION = r'''
+---
+module: win_product_facts
+short_description: Provides Windows product and license information
+description:
+- Provides Windows product and license information.
+author:
+- Dag Wieers (@dagwieers)
+'''
+
+EXAMPLES = r'''
+- name: Get product id and product key
+ community.windows.win_product_facts:
+
+- name: Display Windows edition
+ debug:
+ var: ansible_os_license_edition
+
+- name: Display Windows license status
+ debug:
+ var: ansible_os_license_status
+'''
+
+RETURN = r'''
+ansible_facts:
+ description: Dictionary containing all the detailed information about the Windows product and license.
+ returned: always
+ type: complex
+ contains:
+ ansible_os_license_channel:
+ description: The Windows license channel.
+ returned: always
+ type: str
+ sample: Volume:MAK
+ ansible_os_license_edition:
+ description: The Windows license edition.
+ returned: always
+ type: str
+ sample: Windows(R) ServerStandard edition
+ ansible_os_license_status:
+ description: The Windows license status.
+ returned: always
+ type: str
+ sample: Licensed
+ ansible_os_product_id:
+ description: The Windows product ID.
+ returned: always
+ type: str
+ sample: 00326-10000-00000-AA698
+ ansible_os_product_key:
+ description: The Windows product key.
+ returned: always
+ type: str
+ sample: T49TD-6VFBW-VV7HY-B2PXY-MY47H
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_psexec.ps1 b/ansible_collections/community/windows/plugins/modules/win_psexec.ps1
new file mode 100644
index 00000000..76c5ff8a
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_psexec.ps1
@@ -0,0 +1,161 @@
+#!powershell
+
+# Copyright: (c) 2017, Dag Wieers (@dagwieers) <dag@wieers.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#Requires -Module Ansible.ModuleUtils.ArgvParser
+#Requires -Module Ansible.ModuleUtils.CommandUtil
+
+# See also: https://technet.microsoft.com/en-us/sysinternals/pxexec.aspx
+
+$spec = @{
+ options = @{
+ command = @{ type = 'str'; required = $true }
+ executable = @{ type = 'path'; default = 'psexec.exe' }
+ hostnames = @{ type = 'list'; elements = 'str' }
+ username = @{ type = 'str' }
+ password = @{ type = 'str'; no_log = $true }
+ chdir = @{ type = 'path' }
+ wait = @{ type = 'bool'; default = $true }
+ nobanner = @{ type = 'bool'; default = $false }
+ noprofile = @{ type = 'bool'; default = $false }
+ elevated = @{ type = 'bool'; default = $false }
+ limited = @{ type = 'bool'; default = $false }
+ system = @{ type = 'bool'; default = $false }
+ interactive = @{ type = 'bool'; default = $false }
+ session = @{ type = 'int' }
+ priority = @{ type = 'str'; choices = @( 'background', 'low', 'belownormal', 'abovenormal', 'high', 'realtime' ) }
+ timeout = @{ type = 'int' }
+ }
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$command = $module.Params.command
+$executable = $module.Params.executable
+$hostnames = $module.Params.hostnames
+$username = $module.Params.username
+$password = $module.Params.password
+$chdir = $module.Params.chdir
+$wait = $module.Params.wait
+$nobanner = $module.Params.nobanner
+$noprofile = $module.Params.noprofile
+$elevated = $module.Params.elevated
+$limited = $module.Params.limited
+$system = $module.Params.system
+$interactive = $module.Params.interactive
+$session = $module.Params.session
+$priority = $module.Params.Priority
+$timeout = $module.Params.timeout
+
+$module.Result.changed = $true
+
+If (-Not (Get-Command $executable -ErrorAction SilentlyContinue)) {
+ $module.FailJson("Executable '$executable' was not found.")
+}
+
+$arguments = [System.Collections.Generic.List`1[String]]@($executable)
+
+If ($nobanner -eq $true) {
+ $arguments.Add("-nobanner")
+}
+
+# Support running on local system if no hostname is specified
+If ($hostnames) {
+ $hostname_argument = ($hostnames | Sort-Object -Unique) -join ','
+ $arguments.Add("\\$hostname_argument")
+}
+
+# Username is optional
+If ($null -ne $username) {
+ $arguments.Add("-u")
+ $arguments.Add($username)
+}
+
+# Password is optional
+If ($null -ne $password) {
+ $arguments.Add("-p")
+ $arguments.Add($password)
+}
+
+If ($null -ne $chdir) {
+ $arguments.Add("-w")
+ $arguments.Add($chdir)
+}
+
+If ($wait -eq $false) {
+ $arguments.Add("-d")
+}
+
+If ($noprofile -eq $true) {
+ $arguments.Add("-e")
+}
+
+If ($elevated -eq $true) {
+ $arguments.Add("-h")
+}
+
+If ($system -eq $true) {
+ $arguments.Add("-s")
+}
+
+If ($interactive -eq $true) {
+ $arguments.Add("-i")
+ If ($null -ne $session) {
+ $arguments.Add($session)
+ }
+}
+
+If ($limited -eq $true) {
+ $arguments.Add("-l")
+}
+
+If ($null -ne $priority) {
+ $arguments.Add("-$priority")
+}
+
+If ($null -ne $timeout) {
+ $arguments.Add("-n")
+ $arguments.Add($timeout)
+}
+
+$arguments.Add("-accepteula")
+
+$argument_string = Argv-ToString -arguments $arguments
+
+# Add the command at the end of the argument string, we don't want to escape
+# that as psexec doesn't expect it to be one arg
+$argument_string += " $command"
+
+$start_datetime = [DateTime]::UtcNow
+
+# Replace password with *PASSWORD_REPLACED* to avoid disclosing sensitive data
+$toLog = $argument_string
+if ($password) {
+ $maskedPassword = Argv-ToString $password
+ $toLog = $toLog.Replace($maskedPassword, "*PASSWORD_REPLACED*")
+}
+
+$module.Result.psexec_command = $toLog
+
+$command_result = Run-Command -command $argument_string
+
+$end_datetime = [DateTime]::UtcNow
+
+$module.Result.stdout = $command_result.stdout
+$module.Result.stderr = $command_result.stderr
+
+If ($wait -eq $true) {
+ $module.Result.rc = $command_result.rc
+}
+else {
+ $module.Result.rc = 0
+ $module.Result.pid = $command_result.rc
+}
+
+$module.Result.start = $start_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff")
+$module.Result.end = $end_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff")
+$module.Result.delta = $($end_datetime - $start_datetime).ToString("h\:mm\:ss\.ffffff")
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_psexec.py b/ansible_collections/community/windows/plugins/modules/win_psexec.py
new file mode 100644
index 00000000..44d37654
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_psexec.py
@@ -0,0 +1,167 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: 2017, Dag Wieers (@dagwieers) <dag@wieers.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_psexec
+short_description: Runs commands (remotely) as another (privileged) user
+description:
+- Run commands (remotely) through the PsExec service.
+- Run commands as another (domain) user (with elevated privileges).
+requirements:
+- Microsoft PsExec
+options:
+ command:
+ description:
+ - The command line to run through PsExec (limited to 260 characters).
+ type: str
+ required: yes
+ executable:
+ description:
+ - The location of the PsExec utility (in case it is not located in your PATH).
+ type: path
+ default: psexec.exe
+ hostnames:
+ description:
+ - The hostnames to run the command.
+ - If not provided, the command is run locally.
+ type: list
+ elements: str
+ username:
+ description:
+ - The (remote) user to run the command as.
+ - If not provided, the current user is used.
+ type: str
+ password:
+ description:
+ - The password for the (remote) user to run the command as.
+ - This is mandatory in order authenticate yourself.
+ type: str
+ chdir:
+ description:
+ - Run the command from this (remote) directory.
+ type: path
+ nobanner:
+ description:
+ - Do not display the startup banner and copyright message.
+ - This only works for specific versions of the PsExec binary.
+ type: bool
+ default: no
+ noprofile:
+ description:
+ - Run the command without loading the account's profile.
+ type: bool
+ default: no
+ elevated:
+ description:
+ - Run the command with elevated privileges.
+ type: bool
+ default: no
+ interactive:
+ description:
+ - Run the program so that it interacts with the desktop on the remote system.
+ type: bool
+ default: no
+ session:
+ description:
+ - Specifies the session ID to use.
+ - This parameter works in conjunction with I(interactive).
+ - It has no effect when I(interactive) is set to C(no).
+ type: int
+ limited:
+ description:
+ - Run the command as limited user (strips the Administrators group and allows only privileges assigned to the Users group).
+ type: bool
+ default: no
+ system:
+ description:
+ - Run the remote command in the System account.
+ type: bool
+ default: no
+ priority:
+ description:
+ - Used to run the command at a different priority.
+ choices: [ abovenormal, background, belownormal, high, low, realtime ]
+ type: str
+ timeout:
+ description:
+ - The connection timeout in seconds
+ type: int
+ wait:
+ description:
+ - Wait for the application to terminate.
+ - Only use for non-interactive applications.
+ type: bool
+ default: yes
+notes:
+- More information related to Microsoft PsExec is available from
+ U(https://technet.microsoft.com/en-us/sysinternals/bb897553.aspx)
+seealso:
+- module: community.windows.psexec
+- module: ansible.builtin.raw
+- module: ansible.windows.win_command
+- module: ansible.windows.win_shell
+author:
+- Dag Wieers (@dagwieers)
+'''
+
+EXAMPLES = r'''
+- name: Test the PsExec connection to the local system (target node) with your user
+ community.windows.win_psexec:
+ command: whoami.exe
+
+- name: Run regedit.exe locally (on target node) as SYSTEM and interactively
+ community.windows.win_psexec:
+ command: regedit.exe
+ interactive: yes
+ system: yes
+
+- name: Run the setup.exe installer on multiple servers using the Domain Administrator
+ community.windows.win_psexec:
+ command: E:\setup.exe /i /IACCEPTEULA
+ hostnames:
+ - remote_server1
+ - remote_server2
+ username: DOMAIN\Administrator
+ password: some_password
+ priority: high
+
+- name: Run PsExec from custom location C:\Program Files\sysinternals\
+ community.windows.win_psexec:
+ command: netsh advfirewall set allprofiles state off
+ executable: C:\Program Files\sysinternals\psexec.exe
+ hostnames: [ remote_server ]
+ password: some_password
+ priority: low
+'''
+
+RETURN = r'''
+cmd:
+ description: The complete command line used by the module, including PsExec call and additional options.
+ returned: always
+ type: str
+ sample: psexec.exe -nobanner \\remote_server -u "DOMAIN\Administrator" -p "some_password" -accepteula E:\setup.exe
+pid:
+ description: The PID of the async process created by PsExec.
+ returned: when C(wait=False)
+ type: int
+ sample: 1532
+rc:
+ description: The return code for the command.
+ returned: always
+ type: int
+ sample: 0
+stdout:
+ description: The standard output from the command.
+ returned: always
+ type: str
+ sample: Success.
+stderr:
+ description: The error output from the command.
+ returned: always
+ type: str
+ sample: Error 15 running E:\setup.exe
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_psmodule.ps1 b/ansible_collections/community/windows/plugins/modules/win_psmodule.ps1
new file mode 100644
index 00000000..47515952
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_psmodule.ps1
@@ -0,0 +1,549 @@
+#!powershell
+
+# Copyright: (c) 2018, Wojciech Sciesinski <wojciech[at]sciesinski[dot]net>
+# Copyright: (c) 2017, Daniele Lazzari <lazzari@mailup.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+# win_psmodule (Windows PowerShell modules Additions/Removals/Updates)
+
+$params = Parse-Args -arguments $args -supports_check_mode $true
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
+
+$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
+$required_version = Get-AnsibleParam -obj $params -name "required_version" -type "str"
+$minimum_version = Get-AnsibleParam -obj $params -name "minimum_version" -type "str"
+$maximum_version = Get-AnsibleParam -obj $params -name "maximum_version" -type "str"
+$repo = Get-AnsibleParam -obj $params -name "repository" -type "str"
+$repo_user = Get-AnsibleParam -obj $params -name "username" -type "str"
+$repo_pass = Get-AnsibleParam -obj $params -name "password" -type "str"
+$url = Get-AnsibleParam -obj $params -name "url" -type str
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present", "absent", "latest"
+$allow_clobber = Get-AnsibleParam -obj $params -name "allow_clobber" -type "bool" -default $false
+$skip_publisher_check = Get-AnsibleParam -obj $params -name "skip_publisher_check" -type "bool" -default $false
+$allow_prerelease = Get-AnsibleParam -obj $params -name "allow_prerelease" -type "bool" -default $false
+$accept_license = Get-AnsibleParam -obj $params -name "accept_license" -type "bool" -default $false
+
+$result = @{changed = $false
+ output = ""
+ nuget_changed = $false
+ repository_changed = $false
+}
+
+
+# Enable TLS1.1/TLS1.2 if they're available but disabled (eg. .NET 4.5)
+$security_protocols = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::SystemDefault
+if ([System.Net.SecurityProtocolType].GetMember("Tls11").Count -gt 0) {
+ $security_protocols = $security_protocols -bor [System.Net.SecurityProtocolType]::Tls11
+}
+if ([System.Net.SecurityProtocolType].GetMember("Tls12").Count -gt 0) {
+ $security_protocols = $security_protocols -bor [System.Net.SecurityProtocolType]::Tls12
+}
+[System.Net.ServicePointManager]::SecurityProtocol = $security_protocols
+
+
+Function Install-NugetProvider {
+ Param(
+ [Bool]$CheckMode
+ )
+ $PackageProvider = Get-PackageProvider -ListAvailable | Where-Object { ($_.name -eq 'Nuget') -and ($_.version -ge "2.8.5.201") }
+ if (-not($PackageProvider)) {
+ try {
+ Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -WhatIf:$CheckMode | out-null
+ $result.changed = $true
+ $result.nuget_changed = $true
+ }
+ catch [ System.Exception ] {
+ $ErrorMessage = "Problems adding package provider: $($_.Exception.Message)"
+ Fail-Json $result $ErrorMessage
+ }
+ }
+}
+
+Function Install-PrereqModule {
+ Param(
+ [Switch]$TestInstallationOnly,
+ [bool]$AllowClobber,
+ [Bool]$CheckMode,
+ [bool]$AcceptLicense,
+ [string]$Repository
+ )
+
+ # Those are minimum required versions of modules.
+ $PrereqModules = @{
+ PackageManagement = '1.1.7'
+ PowerShellGet = '1.6.0'
+ }
+
+ [Bool]$PrereqModulesInstalled = $true
+
+ ForEach ( $Name in $PrereqModules.Keys ) {
+
+ $ExistingPrereqModule = Get-Module -ListAvailable | Where-Object { ($_.name -eq $Name) -and ($_.version -ge $PrereqModules[$Name]) }
+
+ if ( -not $ExistingPrereqModule ) {
+ if ( $TestInstallationOnly ) {
+ $PrereqModulesInstalled = $false
+ }
+ else {
+ try {
+ $install_params = @{
+ Name = $Name
+ MinimumVersion = $PrereqModules[$Name]
+ Force = $true
+ WhatIf = $CheckMode
+ }
+ $installCmd = Get-Command -Name Install-Module
+ if ($installCmd.Parameters.ContainsKey('SkipPublisherCheck')) {
+ $install_params.SkipPublisherCheck = $true
+ }
+ if ($installCmd.Parameters.ContainsKey('AllowClobber')) {
+ $install_params.AllowClobber = $AllowClobber
+ }
+ if ($installCmd.Parameters.ContainsKey('AcceptLicense')) {
+ $install_params.AcceptLicense = $AcceptLicense
+ }
+ if ($Repository) {
+ $install_params.Repository = $Repository
+ }
+
+ Install-Module @install_params > $null
+
+ if ( $Name -eq 'PowerShellGet' ) {
+ # An order has to be reverted due to dependency
+ Remove-Module -Name PowerShellGet, PackageManagement -Force
+ Import-Module -Name PowerShellGet, PackageManagement -Force
+ }
+
+ $result.changed = $true
+ }
+ catch [ System.Exception ] {
+ $ErrorMessage = "Problems adding a prerequisite module $Name $($_.Exception.Message)"
+ Fail-Json $result $ErrorMessage
+ }
+ }
+ }
+ }
+
+ if ( $TestInstallationOnly ) {
+ $PrereqModulesInstalled
+ }
+}
+
+Function Get-PsModule {
+ Param(
+ [Parameter(Mandatory = $true)]
+ [String]$Name,
+ [String]$RequiredVersion,
+ [String]$MinimumVersion,
+ [String]$MaximumVersion
+ )
+
+ $ExistingModule = @{
+ Exists = $false
+ Version = ""
+ }
+
+ $ExistingModules = Get-Module -Listavailable | Where-Object { ($_.name -eq $Name) }
+ $ExistingModulesCount = $($ExistingModules | Measure-Object).Count
+
+ if ( $ExistingModulesCount -gt 0 ) {
+
+ $ExistingModules | Add-Member -MemberType ScriptProperty -Name FullVersion -Value {
+ if ( $null -ne ( $this.PrivateData ) ) {
+ [String]"$($this.Version)-$(($this | Select-Object -ExpandProperty PrivateData).PSData.Prerelease)".TrimEnd('-')
+ }
+ else {
+ [String]"$($this.Version)"
+ }
+ }
+
+ if ( -not ($RequiredVersion -or
+ $MinimumVersion -or
+ $MaximumVersion) ) {
+
+ $ReturnedModule = $ExistingModules | Select-Object -First 1
+ }
+ elseif ( $RequiredVersion ) {
+ $ReturnedModule = $ExistingModules | Where-Object -FilterScript { $_.FullVersion -eq $RequiredVersion }
+ }
+ elseif ( $MinimumVersion -and $MaximumVersion ) {
+ $ReturnedModule = $ExistingModules |
+ Where-Object -FilterScript { $MinimumVersion -le $_.Version -and $MaximumVersion -ge $_.Version } |
+ Select-Object -First 1
+ }
+ elseif ( $MinimumVersion ) {
+ $ReturnedModule = $ExistingModules | Where-Object -FilterScript { $MinimumVersion -le $_.Version } | Select-Object -First 1
+ }
+ elseif ( $MaximumVersion ) {
+ $ReturnedModule = $ExistingModules | Where-Object -FilterScript { $MaximumVersion -ge $_.Version } | Select-Object -First 1
+ }
+ }
+
+ $ReturnedModuleCount = ($ReturnedModule | Measure-Object).Count
+
+ if ( $ReturnedModuleCount -eq 1 ) {
+ $ExistingModule.Exists = $true
+ $ExistingModule.Version = $ReturnedModule.FullVersion
+ }
+
+ $ExistingModule
+}
+
+Function Add-DefinedParameter {
+ Param (
+ [Parameter(Mandatory = $true)]
+ [Hashtable]$Hashtable,
+ [Parameter(Mandatory = $true)]
+ [String[]]$ParametersNames
+ )
+
+ ForEach ($ParameterName in $ParametersNames) {
+ $ParameterVariable = Get-Variable -Name $ParameterName -ErrorAction SilentlyContinue
+ if ( $ParameterVariable.Value -and $Hashtable.Keys -notcontains $ParameterName ) {
+ $Hashtable.Add($ParameterName, $ParameterVariable.Value)
+ }
+ }
+
+ $Hashtable
+}
+
+Function Install-PsModule {
+ Param(
+ [Parameter(Mandatory = $true)]
+ [String]$Name,
+ [String]$RequiredVersion,
+ [String]$MinimumVersion,
+ [String]$MaximumVersion,
+ [String]$Repository,
+ [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty,
+ [Bool]$AllowClobber,
+ [Bool]$SkipPublisherCheck,
+ [Bool]$AllowPrerelease,
+ [Bool]$CheckMode,
+ [Bool]$AcceptLicense
+ )
+
+ $getParams = @{
+ Name = $Name
+ RequiredVersion = $RequiredVersion
+ MinimumVersion = $MinimumVersion
+ MaximumVersion = $MaximumVersion
+ }
+ $ExistingModuleBefore = Get-PsModule @getParams
+
+ if ( -not $ExistingModuleBefore.Exists ) {
+ try {
+ # Install NuGet provider if needed.
+ Install-NugetProvider -CheckMode $CheckMode
+
+ $ht = @{
+ Name = $Name
+ WhatIf = $CheckMode
+ Force = $true
+ AcceptLicense = $AcceptLicense
+ }
+
+ [String[]]$ParametersNames = @("RequiredVersion", "MinimumVersion", "MaximumVersion", "AllowPrerelease",
+ "AllowClobber", "SkipPublisherCheck", "Repository", "Credential")
+
+ $ht = Add-DefinedParameter -Hashtable $ht -ParametersNames $ParametersNames
+
+ Install-Module @ht -ErrorVariable ErrorDetails | out-null
+
+ $result.changed = $true
+ $result.output = "Module $($Name) installed"
+ }
+ catch [ System.Exception ] {
+ $ErrorMessage = "Problems installing $($Name) module: $($_.Exception.Message)"
+ Fail-Json $result $ErrorMessage
+ }
+ }
+ else {
+ $result.output = "Module $($Name) already present"
+ }
+}
+
+Function Remove-PsModule {
+ Param(
+ [Parameter(Mandatory = $true)]
+ [String]$Name,
+ [String]$RequiredVersion,
+ [String]$MinimumVersion,
+ [String]$MaximumVersion,
+ [Bool]$CheckMode
+ )
+ # If module is present, uninstalls it.
+ if (Get-Module -Listavailable | Where-Object { $_.name -eq $Name }) {
+ try {
+ $ht = @{
+ Name = $Name
+ Confirm = $false
+ Force = $true
+ }
+
+ $ExistingModuleBefore = Get-PsModule -Name $Name -RequiredVersion $RequiredVersion -MinimumVersion $MinimumVersion -MaximumVersion $MaximumVersion
+
+ [String[]]$ParametersNames = @("RequiredVersion", "MinimumVersion", "MaximumVersion")
+
+ $ht = Add-DefinedParameter -Hashtable $ht -ParametersNames $ParametersNames
+
+ if ( -not ( $RequiredVersion -or $MinimumVersion -or $MaximumVersion ) ) {
+ $ht.Add("AllVersions", $true)
+ }
+
+ if ( $ExistingModuleBefore.Exists) {
+ # The Force parameter overwrite the WhatIf parameter
+ if ( -not $CheckMode ) {
+ Uninstall-Module @ht -ErrorVariable ErrorDetails | out-null
+ }
+ $result.changed = $true
+ $result.output = "Module $($Name) removed"
+ }
+ }
+ catch [ System.Exception ] {
+ $ErrorMessage = "Problems uninstalling $($Name) module: $($_.Exception.Message)"
+ Fail-Json $result $ErrorMessage
+ }
+ }
+ else {
+ $result.output = "Module $($Name) removed"
+ }
+}
+
+Function Find-LatestPsModule {
+ Param(
+ [Parameter(Mandatory = $true)]
+ [String]$Name,
+ [String]$Repository,
+ [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty,
+ [Bool]$AllowPrerelease,
+ [Bool]$CheckMode
+ )
+
+ try {
+ $ht = @{
+ Name = $Name
+ }
+
+ [String[]]$ParametersNames = @("AllowPrerelease", "Repository", "Credential")
+
+ $ht = Add-DefinedParameter -Hashtable $ht -ParametersNames $ParametersNames
+
+ $LatestModule = Find-Module @ht
+ $LatestModuleVersion = $LatestModule.Version
+ }
+ catch [ System.Exception ] {
+ $ErrorMessage = "Cant find the module $($Name): $($_.Exception.Message)"
+ Fail-Json $result $ErrorMessage
+ }
+
+ $LatestModuleVersion
+}
+
+Function Install-Repository {
+ Param(
+ [Parameter(Mandatory = $true)]
+ [string]$Name,
+ [Parameter(Mandatory = $true)]
+ [string]$Url,
+ [bool]$CheckMode
+ )
+ # Legacy doesn't natively support deprecate by date, need to do this manually until we use Ansible.Basic
+ if (-not $result.ContainsKey('deprecations')) {
+ $result.deprecations = @()
+ }
+ $msg = -join @(
+ "Adding a repo with this module is deprecated, the repository parameter should only be used to select a repo. "
+ "Use community.windows.win_psrepository to manage repos"
+ )
+ $result.deprecations += @{
+ msg = $msg
+ date = "2021-07-01"
+ collection_name = "community.windows"
+ }
+ # Install NuGet provider if needed.
+ Install-NugetProvider -CheckMode $CheckMode
+
+ $Repos = (Get-PSRepository).SourceLocation
+
+ # If repository isn't already present, try to register it as trusted.
+ if ($Repos -notcontains $Url) {
+ try {
+ if ( -not ($CheckMode) ) {
+ Register-PSRepository -Name $Name -SourceLocation $Url -InstallationPolicy Trusted -ErrorAction Stop
+ }
+ $result.changed = $true
+ $result.repository_changed = $true
+ }
+ catch {
+ $ErrorMessage = "Problems registering $($Name) repository: $($_.Exception.Message)"
+ Fail-Json $result $ErrorMessage
+ }
+ }
+}
+
+Function Remove-Repository {
+ Param(
+ [Parameter(Mandatory = $true)]
+ [string]$Name,
+ [bool]$CheckMode
+ )
+ # Legacy doesn't natively support deprecate by date, need to do this manually until we use Ansible.Basic
+ if (-not $result.ContainsKey('deprecations')) {
+ $result.deprecations = @()
+ }
+ $result.deprecations += @{
+ msg = "Removing a repo with this module is deprecated, use community.windows.win_psrepository to manage repos"
+ date = "2021-07-01"
+ collection_name = "community.windows"
+ }
+
+ $Repo = (Get-PSRepository).Name
+
+ # Try to remove the repository
+ if ($Repo -contains $Name) {
+ try {
+ if ( -not ($CheckMode) ) {
+ Unregister-PSRepository -Name $Name -ErrorAction Stop
+ }
+ $result.changed = $true
+ $result.repository_changed = $true
+ }
+ catch [ System.Exception ] {
+ $ErrorMessage = "Problems unregistering $($Name)repository: $($_.Exception.Message)"
+ Fail-Json $result $ErrorMessage
+ }
+ }
+}
+
+# Check PowerShell version, fail if < 5.0 and required modules are not installed
+$PsVersion = $PSVersionTable.PSVersion
+if ($PsVersion.Major -lt 5 ) {
+ $PrereqModulesInstalled = Install-PrereqModule -TestInstallationOnly
+ if ( -not $PrereqModulesInstalled ) {
+ $ErrorMessage = -join @(
+ "Modules PowerShellGet and PackageManagement in versions 1.6.0 and 1.1.7 respectively "
+ "have to be installed before using the win_psmodule."
+ )
+ Fail-Json $result $ErrorMessage
+ }
+}
+
+if ( $required_version -and ( $minimum_version -or $maximum_version ) ) {
+ $ErrorMessage = "Parameters required_version and minimum/maximum_version are mutually exclusive."
+ Fail-Json $result $ErrorMessage
+}
+
+if ( $allow_prerelease -and ( $minimum_version -or $maximum_version ) ) {
+ $ErrorMessage = "Parameters minimum_version, maximum_version can't be used with the parameter allow_prerelease."
+ Fail-Json $result $ErrorMessage
+}
+
+if ( $allow_prerelease -and $state -eq "absent" ) {
+ $ErrorMessage = "The parameter allow_prerelease can't be used with state set to 'absent'."
+ Fail-Json $result $ErrorMessage
+}
+
+if ( ($state -eq "latest") -and
+ ( $required_version -or $minimum_version -or $maximum_version ) ) {
+ $ErrorMessage = "When the parameter state is equal 'latest' you can use any of required_version, minimum_version, maximum_version."
+ Fail-Json $result $ErrorMessage
+}
+
+if ( $repo -and (-not $url) ) {
+ $RepositoryExists = Get-PSRepository -Name $repo -ErrorAction SilentlyContinue
+ if ( $null -eq $RepositoryExists) {
+ $ErrorMessage = "The repository $repo doesn't exist."
+ Fail-Json $result $ErrorMessage
+ }
+
+}
+
+if ($repo_user -and $repo_pass ) {
+ $repo_credential = New-Object -TypeName PSCredential ($repo_user, ($repo_pass | ConvertTo-SecureString -AsPlainText -Force))
+}
+
+if ( ($allow_clobber -or $allow_prerelease -or $skip_publisher_check -or
+ $required_version -or $minimum_version -or $maximum_version -or $accept_license) ) {
+ # Update the PowerShellGet and PackageManagement modules.
+ # It's required to support AllowClobber, AllowPrerelease parameters.
+ Install-PrereqModule -AllowClobber $allow_clobber -CheckMode $check_mode -AcceptLicense $accept_license -Repository $repo
+}
+
+Import-Module -Name PackageManagement, PowerShellGet -Force
+
+if ($state -eq "present") {
+ if (($repo) -and ($url)) {
+ Install-Repository -Name $repo -Url $url -CheckMode $check_mode
+ }
+ else {
+ $ErrorMessage = "Repository Name and Url are mandatory if you want to add a new repository"
+ }
+
+ if ($name) {
+ $ht = @{
+ Name = $name
+ RequiredVersion = $required_version
+ MinimumVersion = $minimum_version
+ MaximumVersion = $maximum_version
+ Repository = $repo
+ AllowClobber = $allow_clobber
+ SkipPublisherCheck = $skip_publisher_check
+ AllowPrerelease = $allow_prerelease
+ CheckMode = $check_mode
+ Credential = $repo_credential
+ AcceptLicense = $accept_license
+ }
+ Install-PsModule @ht
+ }
+}
+elseif ($state -eq "absent") {
+ if ($repo) {
+ Remove-Repository -Name $repo -CheckMode $check_mode
+ }
+
+ if ($name) {
+ $ht = @{
+ Name = $Name
+ CheckMode = $check_mode
+ RequiredVersion = $required_version
+ MinimumVersion = $minimum_version
+ MaximumVersion = $maximum_version
+ }
+ Remove-PsModule @ht
+ }
+}
+elseif ( $state -eq "latest") {
+
+ $ht = @{
+ Name = $Name
+ AllowPrerelease = $allow_prerelease
+ Repository = $repo
+ CheckMode = $check_mode
+ Credential = $repo_credential
+ }
+
+ $LatestVersion = Find-LatestPsModule @ht
+
+ $ExistingModule = Get-PsModule $Name
+
+ if ( $LatestVersion.Version -ne $ExistingModule.Version ) {
+
+ $ht = @{
+ Name = $Name
+ RequiredVersion = $LatestVersion
+ Repository = $repo
+ AllowClobber = $allow_clobber
+ SkipPublisherCheck = $skip_publisher_check
+ AllowPrerelease = $allow_prerelease
+ CheckMode = $check_mode
+ Credential = $repo_credential
+ AcceptLicense = $accept_license
+ }
+ Install-PsModule @ht
+ }
+}
+
+Exit-Json $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_psmodule.py b/ansible_collections/community/windows/plugins/modules/win_psmodule.py
new file mode 100644
index 00000000..9808199e
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_psmodule.py
@@ -0,0 +1,174 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Wojciech Sciesinski <wojciech[at]sciesinski[dot]net>
+# Copyright: (c) 2017, Daniele Lazzari <lazzari@mailup.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_psmodule
+short_description: Adds or removes a Windows PowerShell module
+description:
+ - This module helps to install Windows PowerShell modules and register custom modules repository on Windows-based systems.
+options:
+ name:
+ description:
+ - Name of the Windows PowerShell module that has to be installed.
+ type: str
+ required: yes
+ state:
+ description:
+ - If C(present) a new module is installed.
+ - If C(absent) a module is removed.
+ - If C(latest) a module is updated to the newest version.
+ type: str
+ choices: [ absent, latest, present ]
+ default: present
+ required_version:
+ description:
+ - The exact version of the PowerShell module that has to be installed.
+ type: str
+ minimum_version:
+ description:
+ - The minimum version of the PowerShell module that has to be installed.
+ type: str
+ maximum_version:
+ description:
+ - The maximum version of the PowerShell module that has to be installed.
+ type: str
+ allow_clobber:
+ description:
+ - If C(yes) allows install modules that contains commands those have the same names as commands that already exists.
+ type: bool
+ default: no
+ skip_publisher_check:
+ description:
+ - If C(yes), allows you to install a different version of a module that already exists on your computer in the case when a different one
+ is not digitally signed by a trusted publisher and the newest existing module is digitally signed by a trusted publisher.
+ type: bool
+ default: no
+ allow_prerelease:
+ description:
+ - If C(yes) installs modules marked as prereleases.
+ - It doesn't work with the parameters C(minimum_version) and/or C(maximum_version).
+ - It doesn't work with the C(state) set to absent.
+ type: bool
+ default: no
+ repository:
+ description:
+ - Name of the custom repository to use.
+ type: str
+ username:
+ description:
+ - Username to authenticate against private repository.
+ type: str
+ required: no
+ version_added: '1.10.0'
+ password:
+ description:
+ - Password to authenticate against private repository.
+ type: str
+ required: no
+ version_added: '1.10.0'
+ accept_license:
+ description:
+ - Accepts the module's license.
+ - Required for modules that require license acceptance, since interactively answering the prompt is not possible.
+ - Corresponds to the C(-AcceptLicense) parameter of C(Install-Module).
+ - >-
+ Installation of a module or a dependency that requires license acceptance cannot be detected in check mode,
+ but will cause a failure at runtime unless I(accept_license=true).
+ type: bool
+ required: no
+ default: false
+ version_added: '1.11.0'
+ url:
+ description:
+ - URL of the custom repository to register.
+ - DEPRECATED, will be removed in a major release after C(2021-07-01), please use the
+ M(community.windows.win_psrepository) module instead.
+ type: str
+notes:
+ - PowerShell modules needed
+ - PowerShellGet >= 1.6.0
+ - PackageManagement >= 1.1.7
+ - PowerShell package provider needed
+ - NuGet >= 2.8.5.201
+ - On PowerShell 5.x required modules and a package provider will be updated under the first run of the win_psmodule module.
+ - On PowerShell 3.x and 4.x you have to install them before using the win_psmodule.
+seealso:
+- module: community.windows.win_psrepository
+author:
+- Wojciech Sciesinski (@it-praktyk)
+- Daniele Lazzari (@dlazz)
+'''
+
+EXAMPLES = r'''
+---
+- name: Add a PowerShell module
+ community.windows.win_psmodule:
+ name: PowerShellModule
+ state: present
+
+- name: Add an exact version of PowerShell module
+ community.windows.win_psmodule:
+ name: PowerShellModule
+ required_version: "4.0.2"
+ state: present
+
+- name: Install or update an existing PowerShell module to the newest version
+ community.windows.win_psmodule:
+ name: PowerShellModule
+ state: latest
+
+- name: Install newer version of built-in Windows module
+ community.windows.win_psmodule:
+ name: Pester
+ skip_publisher_check: yes
+ state: present
+
+- name: Add a PowerShell module and register a repository
+ community.windows.win_psmodule:
+ name: MyCustomModule
+ repository: MyRepository
+ state: present
+
+- name: Add a PowerShell module from a specific repository
+ community.windows.win_psmodule:
+ name: PowerShellModule
+ repository: MyRepository
+ state: present
+
+- name: Add a PowerShell module from a specific repository with credentials
+ win_psmodule:
+ name: PowerShellModule
+ repository: MyRepository
+ username: repo_username
+ password: repo_password
+ state: present
+
+- name: Remove a PowerShell module
+ community.windows.win_psmodule:
+ name: PowerShellModule
+ state: absent
+'''
+
+RETURN = r'''
+---
+output:
+ description: A message describing the task result.
+ returned: always
+ sample: "Module PowerShellCookbook installed"
+ type: str
+nuget_changed:
+ description: True when Nuget package provider is installed.
+ returned: always
+ type: bool
+ sample: true
+repository_changed:
+ description: True when a custom repository is installed or removed.
+ returned: always
+ type: bool
+ sample: true
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_psmodule_info.ps1 b/ansible_collections/community/windows/plugins/modules/win_psmodule_info.ps1
new file mode 100644
index 00000000..b0932a79
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_psmodule_info.ps1
@@ -0,0 +1,305 @@
+#!powershell
+
+# Copyright: (c) 2020, Brian Scholer <@briantist>
+# 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.CamelConversion
+#Requires -Module PowerShellGet
+
+$spec = @{
+ options = @{
+ name = @{ type = 'str' ; default = '*' }
+ repository = @{ type = 'str' }
+ }
+ supports_check_mode = $true
+}
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+# We need to remove this type data so that arrays don't get serialized weirdly.
+# In some cases, an array gets serialized as an object with a Count and Value property where the value is the actual array.
+# See: https://stackoverflow.com/a/48858780/3905079
+Remove-TypeData System.Array
+
+function Convert-ObjectToSnakeCase {
+ <#
+ .SYNOPSIS
+ Converts an object with CamelCase properties to a dictionary with snake_case keys.
+ Works in the spirit of and depends on the existing CamelConversion module util.
+ #>
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
+ [Object]
+ $InputObject ,
+
+ [Parameter()]
+ [Switch]
+ $NoRecurse ,
+
+ [Parameter()]
+ [Switch]
+ $OmitNull
+ )
+
+ Process {
+ $result = [Ordered]@{}
+ foreach ($property in $InputObject.PSObject.Properties) {
+ $value = $property.Value
+ if (-not $NoRecurse -and $value -is [System.Collections.IDictionary]) {
+ $value = Convert-DictToSnakeCase -dict $value
+ }
+ elseif (-not $NoRecurse -and ($value -is [Array] -or $value -is [System.Collections.ArrayList])) {
+ $value = Convert-ListToSnakeCase -list $value
+ }
+ elseif ($null -eq $value) {
+ if ($OmitNull) {
+ continue
+ }
+ }
+ elseif (-not $NoRecurse -and $value -isnot [System.ValueType] -and $value -isnot [string]) {
+ $value = Convert-ObjectToSnakeCase -InputObject $value
+ }
+
+ $name = Convert-StringToSnakeCase -string $property.Name
+ $result[$name] = $value
+ }
+ $result
+ }
+}
+
+function ConvertTo-SerializableModuleInfo {
+ <#
+ .SYNOPSIS
+ Transforms some members of a ModuleInfo object to be more serialization-friendly and prevent infinite recursion.
+
+ .DESCRIPTION
+ Stringifies version properties so we don't get serialized [System.Version] objects which aren't very useful.
+
+ ExportedCommands and some other Exported* members are problematic in a few ways.
+ For one, they contain a reference to the full ModuleInfo object they are found in. This is probably useful
+ for nested modules and such, but in most cases they are just a reference to the current module, and this will
+ recurse infinitely.
+
+ Further, the rest of the properties of the exported commands aren't needed for this module. A low depth on JSON
+ conversion doesn't fully work because some other module fields need it deeper than 1, and since this is a
+ dictionary we get a JSOn object whose property names and values are both just the name of the command.
+
+ So instead we just make an array of the names, as that's good enough for what we want to return.
+
+ NestedModules and RequiredModules are full ModuleInfo objects so that becomes a recursive call.
+ Limiting depth or reference counting shouldn't be necessary because the entries aren't references; they
+ have to actually exist.
+
+ ModuleList gets recursed as well even though it's a tiny subset of fields, to transform versions and to
+ convert to snake_case.
+
+ PrivateData can contain any data but a module manifest is a static file that can't contain references or
+ problematic types like [Type]. Unfortunately some module types like CIM (and presumably binary?) seem to be
+ able to populate that with whatever they want.
+
+ As a precaution then, for module type that is not Script or Manifest, we limit PrivateData to a shallow depth.
+ #>
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
+ [Object]
+ $InputObject ,
+
+ [Parameter()]
+ [string[]]
+ $ExcludeProperty = @(
+ <#
+ Definition is the script that makes up the script module.
+ Big to return from this module, but also it doesn't ever seem to be filled in
+ in the data from Get-Module -ListAvailable.
+ #>
+ 'Definition',
+ <#
+ OnRemove is a script that gets run before the module is removed.
+ Not necessary for return.
+ #>
+ 'OnRemove',
+ <#
+ For binary modules, ImplementingAssembly is a reference to the assembly.
+ No use returning it.
+ #>
+ 'ImplementingAssembly',
+ <#
+ The session state instance of a loaded module. It's a runtime property only returned
+ without -ListAvailable and has no value serialized.
+ #>
+ 'SessionState'
+ )
+ )
+
+ Process {
+ $properties = foreach ($p in $InputObject.PSObject.Properties) {
+ $pName = $p.Name
+ $pValue = $p.Value
+
+ switch -Regex ($pName) {
+ '^PrivateData$' {
+ if (
+ $InputObject.ModuleType -notin @(
+ [System.Management.Automation.ModuleType]::Script,
+ [System.Management.Automation.ModuleType]::Manifest
+ )
+ ) {
+ $safeVal = $pValue | ConvertTo-Json -Depth 1 | ConvertFrom-Json
+ @{
+ Name = $pName
+ Expression = { $safeVal }.GetNewClosure()
+ }
+ }
+ else {
+ $pName
+ }
+ break
+ }
+ '^(?:NestedModules|ModuleList|RequiredModules)$' {
+ # Nested and Required modules are full moduleinfo objects
+ # ModuleList isn't but its simplified fields need much of the same treatment
+ if ($pValue) {
+ @{
+ Name = $pName
+ Expression = {
+ @(, ($pValue | ConvertTo-SerializableModuleInfo | Convert-ObjectToSnakeCase -NoRecurse))
+ }.GetNewClosure()
+ }
+ }
+ else {
+ $pName
+ }
+ break
+ }
+ 'Version$' {
+ @{
+ Name = $pName
+ Expression = { $pValue.ToString() }.GetNewClosure()
+ }
+ break
+ }
+ '^Exported' {
+ @{
+ Name = $pName
+ Expression = { @(, $pValue.Keys) }.GetNewClosure()
+ }
+ break
+ }
+ default {
+ if ($pValue -is [datetime]) {
+ @{
+ Name = $pName
+ Expression = { $pValue.ToString('o') }.GetNewClosure()
+ }
+ }
+ elseif ($pValue -is [enum] -or $pValue -is [type]) {
+ @{
+ Name = $pName
+ Expression = { $pValue.ToString() }.GetNewClosure()
+ }
+ }
+ else {
+ $pName
+ }
+ }
+ }
+ }
+
+ $InputObject | Select-Object -Property $properties -ExcludeProperty $ExcludeProperty
+ }
+}
+
+function Add-ModuleRepositoryInfo {
+ <#
+ .SYNOPSIS
+ Takes a ModuleInfo object and adds some info that came from PowerShellGet
+
+ .DESCRIPTION
+ Checks if there's information from Get-InstalledModule about the current module,
+ and if so adds to it to ModuleInfo object. The fields are always added, with null
+ values if need be.
+ #>
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
+ [System.Object]
+ $InputObject ,
+
+ [Parameter()]
+ [String]
+ $RepositoryName
+ )
+
+ Begin {
+ $wantedProperties = @(
+ 'PublishedDate',
+ 'InstalledDate',
+ 'UpdatedDate',
+ 'Dependencies',
+ 'Repository',
+ 'PackageManagementProvider',
+ 'InstalledLocation',
+ 'RepositorySourceLocation',
+ 'Tags',
+ 'CompatiblePSEditions',
+ 'LicenseUri',
+ 'ProjectUri',
+ 'IconUri',
+ 'ReleaseNotes',
+ 'ExportedDscResources', # ExportedDscResources is not returned here, this is a hack for Windows 2012/R2 to ensure the field is present
+ 'Prefix' # Prefix is not actually returned here, this is a hack for Windows 2012 just to ensure the field is present
+ )
+
+ # Get all the installed modules at once. This prevents us from having to make an expensive individual call for every
+ # local module, the vast majority of which didn't come from PowerShellGet.
+ # The results here won't contain every version, but that's ok because we'll still have a fast signal as to whether
+ # it makes sense to make a version-specific call for the individual module.
+ $installedModules = Get-InstalledModule -ErrorAction SilentlyContinue | Group-Object -Property Name -AsHashTable -AsString
+ }
+
+ Process {
+ $moduleName = $InputObject.Name
+
+ $installed = if ($installedModules.Contains($moduleName)) {
+ # we know at least one version of this module was installed from PowerShellGet
+ # if the version of this local modle matches what we got it in the initial installed module list
+ # use it
+ if ($installedModules[$moduleName].Version -eq $InputObject.Version) {
+ $installedModules[$moduleName]
+ }
+ else {
+ # make an individual call to see if this specific version of the local module was installed from PowerShellGet
+ $im = Get-InstalledModule -Name $InputObject.Name -RequiredVersion $InputObject.Version -AllowPrerelease -ErrorAction SilentlyContinue
+ if ($im) {
+ $im
+ }
+ }
+ }
+
+ if ($RepositoryName -and $installed.Repository -ne $RepositoryName) {
+ return
+ }
+
+ $members = @{}
+ $wantedProperties | ForEach-Object -Process {
+ if (-not $InputObject.$_) {
+ # if the fields are present in both places, let's prefer what's sent in
+ $members[$_] = $installed.$_
+ }
+ }
+
+ $InputObject | Add-Member -NotePropertyMembers $members -Force -PassThru
+ }
+}
+
+$module.Result.modules = @(
+ Get-Module -ListAvailable -Name $module.Params.name |
+ Add-ModuleRepositoryInfo -RepositoryName $module.Params.repository |
+ ConvertTo-SerializableModuleInfo |
+ Convert-ObjectToSnakeCase -NoRecurse
+)
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_psmodule_info.py b/ansible_collections/community/windows/plugins/modules/win_psmodule_info.py
new file mode 100644
index 00000000..4fe7c0c6
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_psmodule_info.py
@@ -0,0 +1,435 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2020, Brian Scholer <@briantist>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_psmodule_info
+short_description: Gather information about PowerShell Modules
+description:
+ - Gather information about PowerShell Modules including information from PowerShellGet.
+options:
+ name:
+ description:
+ - The name of the module to retrieve.
+ - Supports any wildcard pattern supported by C(Get-Module).
+ - If omitted then all modules will returned.
+ type: str
+ default: '*'
+ repository:
+ description:
+ - The name of the PSRepository the modules were installed from.
+ - This acts as a filter against the modules that would be returned based on the I(name) option.
+ - Modules that were not installed from a repository will not be returned if this option is set.
+ - Only modules installed from a registered repository will be returned.
+ - If the repository was re-registered after module installation with a new C(SourceLocation), this will not match.
+ type: str
+requirements:
+ - C(PowerShellGet) module
+seealso:
+ - module: community.windows.win_psrepository_info
+ - module: community.windows.win_psscript_info
+author:
+ - Brian Scholer (@briantist)
+'''
+
+EXAMPLES = r'''
+- name: Get info about all modules on the system
+ community.windows.win_psmodule_info:
+
+- name: Get info about the ScheduledTasks module
+ community.windows.win_psmodule_info:
+ name: ScheduledTasks
+
+- name: Get info about networking modules
+ community.windows.win_psmodule_info:
+ name: Net*
+
+- name: Get info about all modules installed from the PSGallery repository
+ community.windows.win_psmodule_info:
+ repository: PSGallery
+ register: gallery_modules
+
+- name: Update all modules retrieved from above example
+ community.windows.win_psmodule:
+ name: "{{ item }}"
+ state: latest
+ loop: "{{ gallery_modules.modules | map(attribute=name) }}"
+
+- name: Get info about all modules on the system
+ community.windows.win_psmodule_info:
+ register: all_modules
+
+- name: Find modules installed from a repository that isn't registered now
+ set_fact:
+ missing_repository_modules: "{{
+ all_modules
+ | json_query('modules[?repository!=null && repository==repository_source_location].{name: name, version: version, repository: repository}')
+ | list
+ }}"
+
+- debug:
+ var: missing_repository_modules
+'''
+
+RETURN = r'''
+modules:
+ description:
+ - A list of modules (or an empty list is there are none).
+ returned: always
+ type: list
+ elements: dict
+ contains:
+ name:
+ description:
+ - The name of the module.
+ type: str
+ sample: PSReadLine
+ version:
+ description:
+ - The module version.
+ type: str
+ sample: 1.2.3
+ guid:
+ description:
+ - The GUID of the module.
+ type: str
+ sample: 74c9fd30-734b-4c89-a8ae-7727ad21d1d5
+ path:
+ description:
+ - The path to the module.
+ type: str
+ sample: 'C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules\PKI\PKI.psd1'
+ module_base:
+ description:
+ - The path that contains the module's files.
+ type: str
+ sample: 'C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules\PKI'
+ installed_location:
+ description:
+ - The path where the module is installed.
+ - This should have the same value as C(module_base) but only has a value when the module was installed via PowerShellGet.
+ type: str
+ sample: 'C:\Program Files\WindowsPowerShell\Modules\posh-git\0.7.1'
+ exported_aliases:
+ description:
+ - The aliases exported from the module.
+ type: list
+ elements: str
+ sample:
+ - glu
+ - slu
+ exported_cmdlets:
+ description:
+ - The cmdlets exported from the module.
+ type: list
+ elements: str
+ sample:
+ - Get-Certificate
+ - Get-PfxData
+ exported_commands:
+ description:
+ - All of the commands exported from the module. Includes functions, cmdlets, and aliases.
+ type: list
+ elements: str
+ sample:
+ - glu
+ - Get-LocalUser
+ exported_dsc_resources:
+ description:
+ - The DSC resources exported from the module.
+ type: list
+ elements: str
+ sample:
+ - xWebAppPool
+ - xWebSite
+ exported_format_files:
+ description:
+ - The format files exported from the module.
+ type: list
+ elements: path
+ sample:
+ - 'C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules\DnsClient\DnsCmdlets.Format.ps1xml'
+ - 'C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules\DnsClient\DnsConfig.Format.ps1xml'
+ - 'C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules\DnsClient\DnsClientPSProvider.Format.ps1xml'
+ exported_functions:
+ description:
+ - The functions exported from the module.
+ type: list
+ elements: str
+ sample:
+ - New-VirtualDisk
+ - New-Volume
+ exported_type_files:
+ description:
+ - The type files exported from the module.
+ type: list
+ elements: path
+ sample:
+ - 'C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules\DnsClient\DnsCmdlets.Types.ps1xml'
+ - 'C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules\DnsClient\DnsConfig.Types.ps1xml'
+ - 'C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules\DnsClient\DnsClientPSProvider.Types.ps1xml'
+ exported_variables:
+ description:
+ - The variables exported from the module.
+ type: list
+ elements: str
+ sample:
+ - GitPromptScriptBlock
+ exported_workflows:
+ description:
+ - The workflows exported from the module.
+ type: list
+ elements: str
+ access_mode:
+ description:
+ - The module's access mode. See U(https://docs.microsoft.com/en-us/dotnet/api/system.management.automation.moduleaccessmode)
+ type: str
+ sample: ReadWrite
+ module_type:
+ description:
+ - The module's type. See U(https://docs.microsoft.com/en-us/dotnet/api/system.management.automation.moduletype)
+ type: str
+ sample: Script
+ procoessor_architecture:
+ description:
+ - The module's processor architecture. See U(https://docs.microsoft.com/en-us/dotnet/api/system.reflection.processorarchitecture)
+ type: str
+ sample: Amd64
+ author:
+ description:
+ - The author of the module.
+ type: str
+ sample: Warren Frame
+ copyright:
+ description:
+ - The copyright of the module.
+ type: str
+ sample: '(c) 2016 Warren F. All rights reserved.'
+ company_name:
+ description:
+ - The company name of the module.
+ type: str
+ sample: Microsoft Corporation
+ description:
+ description:
+ - The description of the module.
+ type: str
+ sample: Provides cmdlets to work with local users and local groups
+ clr_version:
+ description:
+ - The CLR version of the module.
+ type: str
+ sample: '4.0'
+ compatible_ps_editions:
+ description:
+ - The PS Editions the module is compatible with.
+ type: list
+ elements: str
+ sample:
+ - Desktop
+ dependencies:
+ description:
+ - The modules required by this module.
+ type: list
+ elements: str
+ dot_net_framework_version:
+ description:
+ - The .Net Framework version of the module.
+ type: str
+ sample: '4.6.1'
+ file_list:
+ description:
+ - The files included in the module.
+ type: list
+ elements: path
+ sample:
+ - 'C:\Program Files\WindowsPowerShell\Modules\PowerShellGet\1.6.0\PSModule.psm1'
+ - 'C:\Program Files\WindowsPowerShell\Modules\PowerShellGet\1.6.0\PSGet.Format.ps1xml'
+ - 'C:\Program Files\WindowsPowerShell\Modules\PowerShellGet\1.6.0\PSGet.Resource.psd1'
+ help_info_uri:
+ description:
+ - The help info address of the module.
+ type: str
+ sample: 'https://go.microsoft.com/fwlink/?linkid=390823'
+ icon_uri:
+ description:
+ - The address of the icon of the module.
+ type: str
+ sample: 'https://raw.githubusercontent.com/powershell/psscriptanalyzer/master/logo.png'
+ license_uri:
+ description:
+ - The address of the license for the module.
+ type: str
+ sample: 'https://github.com/PowerShell/xPendingReboot/blob/master/LICENSE'
+ project_uri:
+ description:
+ - The address of the module's project.
+ type: str
+ sample: 'https://github.com/psake/psake'
+ repository_source_location:
+ description:
+ - The source location of the repository where the module was installed from.
+ type: str
+ sample: 'https://www.powershellgallery.com/api/v2'
+ repository:
+ description:
+ - The PSRepository where the module was installed from.
+ - This value is not historical. It depends on the PSRepositories that are registered now for the current user.
+ - The C(repository_source_location) must match the current source location of a registered repository to get a repository name.
+ - If there is no match, then this value will match C(repository_source_location).
+ type: str
+ sample: PSGallery
+ release_notes:
+ description:
+ - The module's release notes. This is a free text field and no specific format should be assumed.
+ type: str
+ sample: |
+ ## 1.4.6
+ - Update `HelpInfoUri` to point to the latest content
+
+ ## 1.4.5
+ - Bug fix for deadlock when getting parameters in an event
+
+ ## 1.4.4
+ - Bug fix when installing modules from private feeds
+ installed_date:
+ description:
+ - The date the module was installed.
+ type: str
+ sample: '2018-02-14T17:55:34.9620740-05:00'
+ published_date:
+ description:
+ - The date the module was published.
+ type: str
+ sample: '2017-03-15T04:18:09.0000000'
+ updated_date:
+ description:
+ - The date the module was last updated.
+ type: str
+ sample: '2019-12-31T09:20:02.0000000'
+ log_pipeline_execution_details:
+ description:
+ - Determines whether pipeline execution detail events should be logged.
+ type: bool
+ module_list:
+ description:
+ - A list of modules packaged with this module.
+ - This value is not often returned and the modules are not automatically processed.
+ type: list
+ elements: dict
+ contains:
+ name:
+ description:
+ - The name of the module.
+ - This may also be a path to the module file.
+ type: str
+ sample: '.\WindowsUpdateLog.psm1'
+ guid:
+ description:
+ - The GUID of the module.
+ type: str
+ sample: 82fdb72c-ecc5-4dfd-b9d5-83cf6eb9067f
+ version:
+ description:
+ - The minimum version of the module.
+ type: str
+ sample: '2.0'
+ maximum_version:
+ description:
+ - The maximum version of the module.
+ type: str
+ sample: '2.9'
+ required_version:
+ description:
+ - The exact version of the module required.
+ type: str
+ sample: '3.1.4'
+ nested_modules:
+ description:
+ - A list of modules nested with and loaded into the scope of this module.
+ - This list contains full module objects, so each item can have all of the properties listed here, including C(nested_modules).
+ type: list
+ elements: dict
+ required_modules:
+ description:
+ - A list of modules required by this module.
+ - This list contains full module objects, so each item can have all of the properties listed here, including C(required_modules).
+ - These module objects may not contain full information however, so you may see different results than if you had directly queried the module.
+ type: list
+ elements: dict
+ required_assemblies:
+ description:
+ - A list of assemblies that the module requires.
+ - The values may be a simple name or a full path.
+ type: str
+ sample:
+ - Microsoft.Management.Infrastructure.CimCmdlets.dll
+ - Microsoft.Management.Infrastructure.Dll
+ package_management_provider:
+ description:
+ - If the module was installed from PowerShellGet, this is the package management provider used.
+ type: str
+ sample: NuGet
+ power_shell_host_name:
+ description:
+ - The name of the PowerShell host that the module requires.
+ type: str
+ sample: Windows PowerShell ISE Host
+ power_shell_host_version:
+ description:
+ - The version of the PowerShell host that the module requires.
+ type: str
+ sample: '1.1'
+ power_shell_version:
+ description:
+ - The minimum version of PowerShell that the module requires.
+ type: str
+ sample: '5.1'
+ prefix:
+ description:
+ - The default prefix applied to C(Verb-Noun) commands exported from the module, resulting in C(Verb-PrefixNoun) names.
+ type: str
+ private_data:
+ description:
+ - Arbitrary private data used by the module. This is typically defined in the module manifest.
+ - This module limits the depth of the data returned for module types other than C(Script) and C(Manifest).
+ - The C(PSData) is commonly supplied and provides metadata for PowerShellGet but those fields are surfaced in top-level properties as well.
+ type: dict
+ sample:
+ PSData:
+ LicenseUri: https://example.com/module/LICENSE
+ ProjectUri: https://example.com/module/
+ ReleaseNotes: |
+ v2 - Fixed some bugs
+ v1 - First release
+ Tags:
+ - networking
+ - serialization
+ root_module:
+ description:
+ - The root module as defined in the manifest.
+ - This may be a module name, filename, or full path.
+ type: str
+ sample: WindowsErrorReporting.psm1
+ scripts:
+ description:
+ - A list of scripts (C(.ps1) files) that run in the caller's session state when the module is imported.
+ - This value comes from the C(ScriptsToProcess) field in the module's manifest.
+ type: list
+ sample:
+ - PrepareEnvironment.ps1
+ - InitializeData.ps1
+ tags:
+ description:
+ - The tags defined in the module's C(PSData) metadata.
+ type: list
+ elements: str
+ sample:
+ - networking
+ - serialization
+ - git
+ - dsc
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_psrepository.ps1 b/ansible_collections/community/windows/plugins/modules/win_psrepository.ps1
new file mode 100644
index 00000000..6e3319fd
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_psrepository.ps1
@@ -0,0 +1,179 @@
+#!powershell
+
+# Copyright: (c) 2020, Brian Scholer <@briantist>
+# Copyright: (c) 2018, Wojciech Sciesinski <wojciech[at]sciesinski[dot]net>
+# Copyright: (c) 2017, Daniele Lazzari <lazzari@mailup.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '',
+ Justification = 'Some vars are referenced via Get-Variable')]
+param() # param() is needed for attribute to take effect.
+
+# win_psrepository (Windows PowerShell repositories Additions/Removals/Updates)
+
+$params = Parse-Args -arguments $args -supports_check_mode $true
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
+
+$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
+$source_location = Get-AnsibleParam -obj $params -name "source_location" -aliases "source" -type "str"
+$script_source_location = Get-AnsibleParam -obj $params -name "script_source_location" -type "str"
+$publish_location = Get-AnsibleParam -obj $params -name "publish_location" -type "str"
+$script_publish_location = Get-AnsibleParam -obj $params -name "script_publish_location" -type "str"
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present", "absent"
+$installation_policy = Get-AnsibleParam -obj $params -name "installation_policy" -type "str" -validateset "trusted", "untrusted"
+$force = Get-AnsibleParam -obj $params -name "force" -type "bool" -default $false
+$proxy = Get-AnsibleParam -obj $params -name "proxy" -type "str" -failifempty $false
+$repo_user = Get-AnsibleParam -obj $params -name "username" -type "str"
+$repo_pass = Get-AnsibleParam -obj $params -name "password" -type "str"
+
+$result = @{"changed" = $false }
+
+# Enable TLS1.1/TLS1.2 if they're available but disabled (eg. .NET 4.5)
+$security_protocols = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::SystemDefault
+if ([System.Net.SecurityProtocolType].GetMember("Tls11").Count -gt 0) {
+ $security_protocols = $security_protocols -bor [System.Net.SecurityProtocolType]::Tls11
+}
+if ([System.Net.SecurityProtocolType].GetMember("Tls12").Count -gt 0) {
+ $security_protocols = $security_protocols -bor [System.Net.SecurityProtocolType]::Tls12
+}
+[System.Net.ServicePointManager]::SecurityProtocol = $security_protocols
+
+if (-not (Import-Module -Name PowerShellGet -MinimumVersion 1.6.0 -PassThru -ErrorAction SilentlyContinue)) {
+ Fail-Json -obj $result -Message "PowerShellGet version 1.6.0+ is required."
+}
+
+$repository_params = @{
+ Name = $name
+}
+
+$Repo = Get-PSRepository @repository_params -ErrorAction SilentlyContinue
+
+if ($installation_policy) {
+ $repository_params.InstallationPolicy = $installation_policy
+}
+
+if ($proxy) {
+ $repository_params.Proxy = $Proxy
+}
+
+if ($repo_user -and $repo_pass ) {
+ $repository_params.Credential = New-Object -TypeName PSCredential ($repo_user, ($repo_pass | ConvertTo-SecureString -AsPlainText -Force))
+}
+
+function Resolve-LocationParameter {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory = $true)]
+ [string[]]
+ $Name ,
+
+ [Parameter(Mandatory = $true)]
+ [hashtable]
+ $Splatter
+ )
+
+ End {
+ foreach ($param in $Name) {
+ $val = Get-Variable -Name $param -ValueOnly -ErrorAction SilentlyContinue
+ if ($val) {
+ if ($val -as [uri]) {
+ $Splatter[$param.Replace('_', '')] = $val -as [uri]
+ }
+ else {
+ Fail-Json -obj $result -Message "'$param' must be a valid URI."
+ }
+ }
+ }
+ }
+}
+
+Resolve-LocationParameter -Name source_location, publish_location, script_source_location, script_publish_location -Splatter $repository_params
+
+if (-not $repository_params.SourceLocation -and $state -eq 'present' -and ($force -or -not $Repo)) {
+ Fail-Json -obj $result -message "'source_location' is required when registering a new repository or using force with 'state' == 'present'."
+}
+Function Update-NuGetPackageProvider {
+ $PackageProvider = Get-PackageProvider -ListAvailable | Where-Object { ($_.name -eq 'Nuget') -and ($_.version -ge "2.8.5.201") }
+ if ($null -eq $PackageProvider) {
+ Find-PackageProvider -Name Nuget -ForceBootstrap -IncludeDependencies -Force | Out-Null
+ }
+}
+
+if ($Repo) {
+ $changed_properties = @{}
+
+ if ($force -and -not $repository_params.InstallationPolicy) {
+ $repository_params.InstallationPolicy = 'trusted'
+ }
+
+ if ($repository_params.InstallationPolicy) {
+ if ($Repo.InstallationPolicy -ne $repository_params.InstallationPolicy) {
+ $changed_properties.InstallationPolicy = $repository_params.InstallationPolicy
+ }
+ }
+
+ if ($repository_params.SourceLocation) {
+ # force check not needed here because it's done earlier; source_location is required with force
+ if ($repository_params.SourceLocation -ne $Repo.SourceLocation) {
+ $changed_properties.SourceLocation = $repository_params.SourceLocation
+ }
+ }
+
+ if ($force -or $repository_params.ScriptSourceLocation) {
+ if ($repository_params.ScriptSourceLocation -ne $Repo.ScriptSourceLocation) {
+ $changed_properties.ScriptSourceLocation = $repository_params.ScriptSourceLocation
+ }
+ }
+
+ if ($force -or $repository_params.PublishLocation) {
+ if ($repository_params.PublishLocation -ne $Repo.PublishLocation) {
+ $changed_properties.PublishLocation = $repository_params.PublishLocation
+ }
+ }
+
+ if ($force -or $repository_params.ScriptPublishLocation) {
+ if ($repository_params.ScriptPublishLocation -ne $Repo.ScriptPublishLocation) {
+ $changed_properties.ScriptPublishLocation = $repository_params.ScriptPublishLocation
+ }
+ }
+
+ if ($changed_properties.Count -gt 0) {
+ if ($repository_params.Credential) {
+ $changed_properties.Credential = $repository_params.Credential
+ }
+ }
+}
+
+if ($Repo -and ($state -eq "absent" -or ($force -and $changed_properties.Count -gt 0))) {
+ if (-not $check_mode) {
+ Update-NuGetPackageProvider
+ Unregister-PSRepository -Name $name
+ }
+ $result.changed = $true
+}
+
+if ($state -eq "present") {
+ if (-not $Repo -or ($force -and $changed_properties.Count -gt 0)) {
+ if (-not $repository_params.InstallationPolicy) {
+ $repository_params.InstallationPolicy = "trusted"
+ }
+ if (-not $check_mode) {
+ Update-NuGetPackageProvider
+ Register-PSRepository @repository_params
+ }
+ $result.changed = $true
+ }
+ else {
+ if ($changed_properties.Count -gt 0) {
+ if (-not $check_mode) {
+ Update-NuGetPackageProvider
+ Set-PSRepository -Name $name @changed_properties
+ }
+ $result.changed = $true
+ }
+ }
+}
+
+Exit-Json -obj $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_psrepository.py b/ansible_collections/community/windows/plugins/modules/win_psrepository.py
new file mode 100644
index 00000000..6c37b204
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_psrepository.py
@@ -0,0 +1,153 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2020, Brian Scholer <@briantist>
+# Copyright: (c) 2018, Wojciech Sciesinski <wojciech[at]sciesinski[dot]net>
+# Copyright: (c) 2017, Daniele Lazzari <lazzari@mailup.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_psrepository
+short_description: Adds, removes or updates a Windows PowerShell repository.
+description:
+ - This module helps to add, remove and update Windows PowerShell repository on Windows-based systems.
+options:
+ name:
+ description:
+ - Name of the repository to work with.
+ type: str
+ required: yes
+ source_location:
+ description:
+ - Specifies the URI for discovering and installing modules from this repository.
+ - A URI can be a NuGet server feed (most common situation), HTTP, HTTPS, FTP or file location.
+ - Required when registering a new repository or using I(force=True).
+ type: str
+ aliases:
+ - source
+ script_source_location:
+ description:
+ - Specifies the URI for discovering and installing scripts from this repository.
+ type: str
+ publish_location:
+ description:
+ - Specifies the URI for publishing modules to this repository.
+ type: str
+ script_publish_location:
+ description:
+ - Specifies the URI for publishing scripts to this repository.
+ type: str
+ state:
+ description:
+ - If C(present) a new repository is added or updated.
+ - If C(absent) a repository is removed.
+ type: str
+ choices: [ absent, present ]
+ default: present
+ installation_policy:
+ description:
+ - Sets the C(InstallationPolicy) of a repository.
+ - Will default to C(trusted) when creating a new repository or used with I(force=True).
+ type: str
+ choices: [ trusted, untrusted ]
+ force:
+ description:
+ - If C(True), any differences from the desired state will result in the repository being unregistered, and then re-registered.
+ - I(force) has no effect when I(state=absent). See notes for additional context.
+ type: bool
+ default: False
+ proxy:
+ description:
+ - Proxy to use for repository.
+ type: str
+ required: no
+ version_added: 1.1.0
+ username:
+ description:
+ - Username to authenticate against private repository.
+ type: str
+ required: no
+ version_added: '1.10.0'
+ password:
+ description:
+ - Password to authenticate against private repository.
+ type: str
+ required: no
+ version_added: '1.10.0'
+requirements:
+ - PowerShell Module L(PowerShellGet >= 1.6.0,https://www.powershellgallery.com/packages/PowerShellGet/)
+ - PowerShell Module L(PackageManagement >= 1.1.7,https://www.powershellgallery.com/packages/PackageManagement/)
+ - PowerShell Package Provider C(NuGet) >= 2.8.5.201
+notes:
+ - See the examples on how to update the NuGet package provider.
+ - You can not use C(win_psrepository) to re-register (add) removed PSGallery, use the command C(Register-PSRepository -Default) instead.
+ - When registering or setting I(source_location), PowerShellGet will transform the location according to internal rules, such as following HTTP/S redirects.
+ - This can result in a C(CHANGED) status on each run as the values will never match and will be "reset" each time.
+ - To work around that, find the true destination value with M(community.windows.win_psrepository_info) or C(Get-PSRepository) and update the playbook to
+ match.
+ - When updating an existing repository, all options except I(name) are optional. Only supplied options will be updated. Use I(force=True) to exactly match.
+ - I(script_location), I(publish_location), and I(script_publish_location) are optional but once set can only be cleared with I(force=True).
+ - Using I(force=True) will unregister and re-register the repository if there are any changes, so that it exactly matches the options specified.
+seealso:
+ - module: community.windows.win_psrepository_info
+ - module: community.windows.win_psmodule
+author:
+ - Wojciech Sciesinski (@it-praktyk)
+ - Brian Scholer (@briantist)
+'''
+
+EXAMPLES = r'''
+---
+- name: Ensure the required NuGet package provider version is installed
+ ansible.windows.win_shell: Find-PackageProvider -Name Nuget -ForceBootstrap -IncludeDependencies -Force
+
+- name: Register a PowerShell repository
+ community.windows.win_psrepository:
+ name: MyRepository
+ source_location: https://myrepo.com
+ state: present
+
+- name: Remove a PowerShell repository
+ community.windows.win_psrepository:
+ name: MyRepository
+ state: absent
+
+- name: Add an untrusted repository
+ community.windows.win_psrepository:
+ name: MyRepository
+ installation_policy: untrusted
+
+- name: Add a repository with different locations
+ community.windows.win_psrepository:
+ name: NewRepo
+ source_location: https://myrepo.example/module/feed
+ script_source_location: https://myrepo.example/script/feed
+ publish_location: https://myrepo.example/api/module/publish
+ script_publish_location: https://myrepo.example/api/script/publish
+
+- name: Update only two properties on the above repository
+ community.windows.win_psrepository:
+ name: NewRepo
+ installation_policy: untrusted
+ script_publish_location: https://scriptprocessor.example/publish
+
+- name: Clear script locations from the above repository by re-registering it
+ community.windows.win_psrepository:
+ name: NewRepo
+ installation_policy: untrusted
+ source_location: https://myrepo.example/module/feed
+ publish_location: https://myrepo.example/api/module/publish
+ force: True
+
+- name: Register a PowerShell repository with credentials
+ community.windows.win_psrepository:
+ name: MyRepository
+ source_location: https://myrepo.com
+ state: present
+ username: repo_username
+ password: repo_password
+'''
+
+RETURN = '''
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_psrepository_copy.ps1 b/ansible_collections/community/windows/plugins/modules/win_psrepository_copy.ps1
new file mode 100644
index 00000000..47c6cece
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_psrepository_copy.ps1
@@ -0,0 +1,233 @@
+#!powershell
+
+# Copyright: (c) 2020, Brian Scholer <@briantist>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+$spec = @{
+ supports_check_mode = $true
+ options = @{
+ source = @{
+ type = 'path'
+ default = '%LOCALAPPDATA%\Microsoft\Windows\PowerShell\PowerShellGet\PSRepositories.xml'
+ }
+ name = @{
+ type = 'list'
+ elements = 'str'
+ default = @(
+ '*'
+ )
+ }
+ exclude = @{
+ type = 'list'
+ elements = 'str'
+ }
+ profiles = @{
+ type = 'list'
+ elements = 'str'
+ default = @(
+ '*'
+ )
+ }
+ exclude_profiles = @{
+ type = 'list'
+ elements = 'str'
+ default = @(
+ 'systemprofile'
+ 'LocalService'
+ 'NetworkService'
+ )
+ }
+ }
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$module.Diff.before = @{}
+$module.Diff.after = @{}
+
+function Select-Wildcard {
+ <#
+ .SYNOPSIS
+ Compares a value to an Include and Exclude list of wildcards,
+ returning the input object if a match is found
+
+ .DESCRIPTION
+ If $Property is specified, that property of the input object is
+ compared rather than the object itself, but the original object
+ is returned, not the property.
+ #>
+ [CmdletBinding()]
+ [OutputType([object])]
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
+ [object]
+ $InputObject ,
+
+ [Parameter()]
+ [String]
+ $Property ,
+
+ [Parameter()]
+ [String[]]
+ $Include ,
+
+ [Parameter()]
+ [String[]]
+ $Exclude
+ )
+
+ Process {
+ $o = if ($Property) {
+ $InputObject.($Property)
+ }
+ else {
+ $InputObject
+ }
+
+ foreach ($inc in $Include) {
+ $imatch = $o -like $inc
+ if ($imatch) {
+ break
+ }
+ }
+
+ if (-not $imatch) {
+ return
+ }
+
+ foreach ($exc in $Exclude) {
+ if ($o -like $exc) {
+ return
+ }
+ }
+
+ $InputObject
+ }
+}
+
+function Get-ProfileDirectory {
+ <#
+ .SYNOPSIS
+ Returns DirectoryInfo objects for each profile on the system, as reported by the registry
+
+ .DESCRIPTION
+ The special "Default" profile, used as a template for newly created users, is explicitly
+ added to the list of possible profiles returned. Public is explicitly excluded.
+ Paths reported by the registry that don't exist on the filesystem are silently skipped.
+ #>
+ [CmdletBinding()]
+ [OutputType([System.IO.DirectoryInfo])]
+ param(
+ [Parameter()]
+ [String[]]
+ $Include ,
+
+ [Parameter()]
+ [String[]]
+ $Exclude
+ )
+
+ $regPL = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList'
+
+ # note: this is a key named "Default", not the (Default) key
+ $default = Get-ItemProperty -LiteralPath $regPL | Select-Object -ExpandProperty Default
+
+ # "ProfileImagePath" is always the local side of the profile, even if roaming profiles are used
+ # This is what we want, because PSRepositories are stored in AppData/Local and don't roam
+ $profiles = (
+ @($default) +
+ (Get-ChildItem -LiteralPath $regPL | Get-ItemProperty | Select-Object -ExpandProperty ProfileImagePath)
+ ) -as [System.IO.DirectoryInfo[]]
+
+ $profiles |
+ Where-Object -Property Exists -EQ $true |
+ Select-Wildcard -Property Name -Include $Include -Exclude $Exclude
+}
+
+function Compare-Hashtable {
+ <#
+ .SYNOPSIS
+ Attempts to naively compare two hashtables by serializing them and string comparing the serialized versions
+ #>
+ [CmdletBinding()]
+ [OutputType([bool])]
+ param(
+ [Parameter(Mandatory)]
+ [hashtable]
+ $ReferenceObject ,
+
+ [Parameter(Mandatory)]
+ [hashtable]
+ $DifferenceObject ,
+
+ [Parameter()]
+ [int]
+ $Depth
+ )
+
+ if ($PSBoundParameters.ContainsKey('Depth')) {
+ $sRef = [System.Management.Automation.PSSerializer]::Serialize($ReferenceObject, $Depth)
+ $sDif = [System.Management.Automation.PSSerializer]::Serialize($DifferenceObject, $Depth)
+ }
+ else {
+ $sRef = [System.Management.Automation.PSSerializer]::Serialize($ReferenceObject)
+ $sDif = [System.Management.Automation.PSSerializer]::Serialize($DifferenceObject)
+ }
+
+ $sRef -ceq $sDif
+}
+
+# load the repositories from the source file
+try {
+ $src = $module.Params.source
+ $src_repos = Import-Clixml -LiteralPath $src -ErrorAction Stop
+}
+catch [System.IO.FileNotFoundException] {
+ $module.FailJson("The source file '$src' was not found.", $_)
+}
+catch {
+ $module.FailJson("There was an error loading the source file '$src': $($_.Exception.Message).", $_)
+}
+
+$profiles = Get-ProfileDirectory -Include $module.Params.profiles -Exclude $module.Params.exclude_profiles
+
+foreach ($user in $profiles) {
+ $username = $user.Name
+
+ $repo_dir = $user.FullName | Join-Path -ChildPath 'AppData\Local\Microsoft\Windows\PowerShell\PowerShellGet'
+ $repo_path = $repo_dir | Join-Path -ChildPath 'PSRepositories.xml'
+
+ if (Test-Path -LiteralPath $repo_path) {
+ $cur_repos = Import-Clixml -LiteralPath $repo_path
+ }
+ else {
+ $cur_repos = @{}
+ }
+
+ $new_repos = $cur_repos.Clone()
+ $updated = $false
+
+ $src_repos.Keys |
+ Select-Wildcard -Include $module.Params.name -Exclude $module.Params.exclude |
+ ForEach-Object -Process {
+ # explicit scope used inside ForEach-Object to satisfy lint (PSUseDeclaredVarsMoreThanAssignment)
+ # see https://github.com/PowerShell/PSScriptAnalyzer/issues/827
+ $Script:updated = $true
+ $Script:new_repos[$_] = $Script:src_repos[$_]
+ }
+
+ $module.Diff.before[$username] = $cur_repos
+ $module.Diff.after[$username] = $new_repos
+
+ if ($updated -and -not (Compare-Hashtable -ReferenceObject $cur_repos -DifferenceObject $new_repos)) {
+ if (-not $module.CheckMode) {
+ $null = New-Item -Path $repo_dir -ItemType Directory -Force -ErrorAction SilentlyContinue
+ $new_repos | Export-Clixml -LiteralPath $repo_path -Force
+ }
+ $module.Result.changed = $true
+ }
+}
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_psrepository_copy.py b/ansible_collections/community/windows/plugins/modules/win_psrepository_copy.py
new file mode 100644
index 00000000..686c0062
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_psrepository_copy.py
@@ -0,0 +1,143 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2020, Brian Scholer <@briantist>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_psrepository_copy
+short_description: Copies registered PSRepositories to other user profiles
+version_added: '1.3.0'
+description:
+ - Copies specified registered PSRepositories to other user profiles on the system.
+ - Can include the C(Default) profile so that new users start with the selected repositories.
+ - Can include special service accounts like the local SYSTEM user, LocalService, NetworkService.
+options:
+ source:
+ description:
+ - The full path to the source repositories XML file.
+ - Defaults to the repositories registered to the current user.
+ type: path
+ default: '%LOCALAPPDATA%\Microsoft\Windows\PowerShell\PowerShellGet\PSRepositories.xml'
+ name:
+ description:
+ - The names of repositories to copy.
+ - Names are interpreted as wildcards.
+ type: list
+ elements: str
+ default: ['*']
+ exclude:
+ description:
+ - The names of repositories to exclude.
+ - Names are interpreted as wildcards.
+ - If a name matches both an include (I(name)) and I(exclude), it will be excluded.
+ type: list
+ elements: str
+ profiles:
+ description:
+ - The names of user profiles to populate with repositories.
+ - Names are interpreted as wildcards.
+ - The C(Default) profile can also be matched.
+ - The C(Public) and C(All Users) profiles cannot be targeted, as PSRepositories are not loaded from them.
+ type: list
+ elements: str
+ default: ['*']
+ exclude_profiles:
+ description:
+ - The names of user profiles to exclude.
+ - If a profile matches both an include (I(profiles)) and I(exclude_profiles), it will be excluded.
+ - By default, the service account profiles are excluded.
+ - To explcitly exclude nothing, set I(exclude_profiles=[]).
+ type: list
+ elements: str
+ default:
+ - systemprofile
+ - LocalService
+ - NetworkService
+notes:
+ - Does not require the C(PowerShellGet) module or any other external dependencies.
+ - User profiles are loaded from the registry. If a given path does not exist (like if the profile directory was deleted), it is silently skipped.
+ - If setting service account profiles, you may need C(become=yes). See examples.
+ - "When PowerShellGet first sets up a repositories file, it always adds C(PSGallery), however if this module creates a new repos file and your selected
+ repositories don't include C(PSGallery), it won't be in your destination."
+ - "The values searched in I(profiles) (and I(exclude_profiles)) are profile names, not necessarily user names. This can happen when the profile path is
+ deliberately changed or when domain user names conflict with users from the local computer or another domain. In this case the second+ user may have the
+ domain name or local computer name appended, like C(JoeUser.Contoso) vs. C(JoeUser).
+ If you intend to filter user profiles, ensure your filters catch the right names."
+ - "In the case of the service accounts, the specific profiles are C(systemprofile) (for the C(SYSTEM) user), and C(LocalService) or C(NetworkService)
+ for those accounts respectively."
+ - "Repositories with credentials (requiring authentication) or proxy information will copy, but the credentials and proxy details will not as that
+ information is not stored with repository."
+seealso:
+ - module: community.windows.win_psrepository
+ - module: community.windows.win_psrepository_info
+author:
+ - Brian Scholer (@briantist)
+'''
+
+EXAMPLES = r'''
+- name: Copy the current user's PSRepositories to all non-service account profiles and Default profile
+ community.windows.win_psrepository_copy:
+
+- name: Copy the current user's PSRepositories to all profiles and Default profile
+ community.windows.win_psrepository_copy:
+ exclude_profiles: []
+
+- name: Copy the current user's PSRepositories to all profiles beginning with A, B, or C
+ community.windows.win_psrepository_copy:
+ profiles:
+ - 'A*'
+ - 'B*'
+ - 'C*'
+
+- name: Copy the current user's PSRepositories to all profiles beginning B except Brian and Brianna
+ community.windows.win_psrepository_copy:
+ profiles: 'B*'
+ exclude_profiles:
+ - Brian
+ - Brianna
+
+- name: Copy a specific set of repositories to profiles beginning with 'svc' with exceptions
+ community.windows.win_psrepository_copy:
+ name:
+ - CompanyRepo1
+ - CompanyRepo2
+ - PSGallery
+ profiles: 'svc*'
+ exclude_profiles: 'svc-restricted'
+
+- name: Copy repos matching a pattern with exceptions
+ community.windows.win_psrepository_copy:
+ name: 'CompanyRepo*'
+ exclude: 'CompanyRepo*-Beta'
+
+- name: Copy repositories from a custom XML file on the target host
+ community.windows.win_psrepository_copy:
+ source: 'C:\data\CustomRepostories.xml'
+
+### A sample workflow of seeding a system with a custom repository
+
+# A playbook that does initial host setup or builds system images
+
+- name: Register custom respository
+ community.windows.win_psrepository:
+ name: PrivateRepo
+ source_location: https://example.com/nuget/feed/etc
+ installation_policy: trusted
+
+- name: Ensure all current and new users have this repository registered
+ community.windows.win_psrepository_copy:
+ name: PrivateRepo
+
+# In another playbook, run by other users (who may have been created later)
+
+- name: Install a module
+ community.windows.win_psmodule:
+ name: CompanyModule
+ repository: PrivateRepo
+ state: present
+'''
+
+RETURN = r'''
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_psrepository_info.ps1 b/ansible_collections/community/windows/plugins/modules/win_psrepository_info.ps1
new file mode 100644
index 00000000..c0a7545b
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_psrepository_info.ps1
@@ -0,0 +1,68 @@
+#!powershell
+
+# Copyright: (c) 2020, Brian Scholer <@briantist>
+# 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.CamelConversion
+#Requires -Module PowerShellGet
+
+$spec = @{
+ options = @{
+ name = @{ type = 'str' ; default = '*' }
+ }
+ supports_check_mode = $true
+}
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+function Convert-ObjectToSnakeCase {
+ <#
+ .SYNOPSIS
+ Converts an object with CamelCase properties to a dictionary with snake_case keys.
+ Works in the spirit of and depends on the existing CamelConversion module util.
+ #>
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
+ [Object]
+ $InputObject ,
+
+ [Parameter()]
+ [Switch]
+ $NoRecurse ,
+
+ [Parameter()]
+ [Switch]
+ $OmitNull
+ )
+
+ Process {
+ $result = [Ordered]@{}
+ foreach ($property in $InputObject.PSObject.Properties) {
+ $value = $property.Value
+ if (-not $NoRecurse -and $value -is [System.Collections.IDictionary]) {
+ $value = Convert-DictToSnakeCase -dict $value
+ }
+ elseif (-not $NoRecurse -and ($value -is [Array] -or $value -is [System.Collections.ArrayList])) {
+ $value = Convert-ListToSnakeCase -list $value
+ }
+ elseif ($null -eq $value) {
+ if ($OmitNull) {
+ continue
+ }
+ }
+ elseif (-not $NoRecurse -and $value -isnot [System.ValueType] -and $value -isnot [string]) {
+ $value = Convert-ObjectToSnakeCase -InputObject $value
+ }
+
+ $name = Convert-StringToSnakeCase -string $property.Name
+ $result[$name] = $value
+ }
+ $result
+ }
+}
+
+$module.Result.repositories = @(Get-PSRepository -Name $module.Params.name | Convert-ObjectToSnakeCase)
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_psrepository_info.py b/ansible_collections/community/windows/plugins/modules/win_psrepository_info.py
new file mode 100644
index 00000000..9a54d041
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_psrepository_info.py
@@ -0,0 +1,107 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2020, Brian Scholer <@briantist>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_psrepository_info
+short_description: Gather information about PSRepositories
+description:
+ - Gather information about all or a specific PSRepository.
+options:
+ name:
+ description:
+ - The name of the repository to retrieve.
+ - Supports any wildcard pattern supported by C(Get-PSRepository).
+ - If omitted then all repositories will returned.
+ type: str
+ default: '*'
+requirements:
+ - C(PowerShellGet) module
+seealso:
+ - module: community.windows.win_psrepository
+author:
+ - Brian Scholer (@briantist)
+'''
+
+EXAMPLES = r'''
+- name: Get info for a single repository
+ community.windows.win_psrepository_info:
+ name: PSGallery
+ register: repo_info
+
+- name: Find all repositories that start with 'MyCompany'
+ community.windows.win_psrepository_info:
+ name: MyCompany*
+
+- name: Get info for all repositories
+ community.windows.win_psrepository_info:
+ register: repo_info
+
+- name: Remove all repositories that don't have a publish_location set
+ community.windows.win_psrepository:
+ name: "{{ item }}"
+ state: absent
+ loop: "{{ repo_info.repositories | rejectattr('publish_location', 'none') | list }}"
+'''
+
+RETURN = r'''
+repositories:
+ description:
+ - A list of repositories (or an empty list is there are none).
+ returned: always
+ type: list
+ elements: dict
+ contains:
+ name:
+ description:
+ - The name of the repository.
+ type: str
+ sample: PSGallery
+ installation_policy:
+ description:
+ - The installation policy of the repository. The sample values are the only possible values.
+ type: str
+ sample:
+ - Trusted
+ - Untrusted
+ trusted:
+ description:
+ - A boolean flag reflecting the value of C(installation_policy) as to whether the repository is trusted.
+ type: bool
+ package_management_provider:
+ description:
+ - The name of the package management provider for this repository.
+ type: str
+ sample: NuGet
+ provider_options:
+ description:
+ - Provider-specific options for this repository.
+ type: dict
+ source_location:
+ description:
+ - The location used to find and retrieve modules. This should always have a value.
+ type: str
+ sample: https://www.powershellgallery.com/api/v2
+ publish_location:
+ description:
+ - The location used to publish modules.
+ type: str
+ sample: https://www.powershellgallery.com/api/v2/package/
+ script_source_location:
+ description:
+ - The location used to find and retrieve scripts.
+ type: str
+ sample: https://www.powershellgallery.com/api/v2/items/psscript
+ script_publish_location:
+ description:
+ - The location used to publish scripts.
+ type: str
+ sample: https://www.powershellgallery.com/api/v2/package/
+ registered:
+ description:
+ - Whether the module is registered. Should always be C(True)
+ type: bool
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_psscript.ps1 b/ansible_collections/community/windows/plugins/modules/win_psscript.ps1
new file mode 100644
index 00000000..dc54741f
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_psscript.ps1
@@ -0,0 +1,198 @@
+#!powershell
+
+# Copyright: (c) 2020, Brian Scholer <@briantist>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#Requires -Module @{ ModuleName = 'PowerShellGet' ; ModuleVersion = '1.6.0' }
+
+$spec = @{
+ supports_check_mode = $true
+ options = @{
+ name = @{
+ type = 'str'
+ required = $true
+ }
+ repository = @{ type = 'str' }
+ scope = @{
+ type = 'str'
+ choices = @('all_users', 'current_user')
+ default = 'all_users'
+ }
+ state = @{
+ type = 'str'
+ choices = @('present', 'absent', 'latest')
+ default = 'present'
+ }
+ required_version = @{ type = 'str' }
+ minimum_version = @{ type = 'str' }
+ maximum_version = @{ type = 'str' }
+ source_username = @{ type = 'str' }
+ source_password = @{
+ type = 'str'
+ no_log = $true
+ }
+ allow_prerelease = @{
+ type = 'bool'
+ default = $false
+ }
+ }
+
+ mutually_exclusive = @(
+ @('required_version', 'minimum_version'),
+ @('required_version', 'maximum_version')
+ )
+
+ required_together = @(
+ , @('source_username', 'source_password')
+ )
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+$state = $module.Params.state
+
+# Enable TLS1.1/TLS1.2 if they're available but disabled (eg. .NET 4.5)
+$security_protocols = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::SystemDefault
+if ([System.Net.SecurityProtocolType].GetMember("Tls11").Count -gt 0) {
+ $security_protocols = $security_protocols -bor [System.Net.SecurityProtocolType]::Tls11
+}
+if ([System.Net.SecurityProtocolType].GetMember("Tls12").Count -gt 0) {
+ $security_protocols = $security_protocols -bor [System.Net.SecurityProtocolType]::Tls12
+}
+[System.Net.ServicePointManager]::SecurityProtocol = $security_protocols
+
+function Get-SplattableParameter {
+ [CmdletBinding(DefaultParameterSetName = 'All')]
+ [OutputType([hashtable])]
+ param(
+ [Parameter(Mandatory = $true)]
+ [Ansible.Basic.AnsibleModule]
+ $Module ,
+
+ [Parameter(ParameterSetName = 'ForCommand')]
+ [Alias('For')]
+ [String]
+ $ForCommand ,
+
+ [Parameter(ParameterSetName = 'WithCommand', ValueFromPipeline = $true)]
+ [Alias('Command')]
+ [System.Management.Automation.CommandInfo]
+ $WithCommand
+ )
+
+ Process {
+ $ret = @{}
+
+ $cmd = switch ($PSCmdlet.ParameterSetName) {
+ 'WithCommand' { $WithCommand }
+ 'ForCommand' { Get-Command -Name $ForCommand }
+ }
+
+ if ($cmd) {
+ $commons = [System.Management.Automation.Cmdlet]::CommonParameters + [System.Management.Automation.Cmdlet]::CommonOptionalParameters
+ $validParams = $cmd.Parameters.GetEnumerator() | ForEach-Object -Process {
+ if ($commons -notcontains $_.Key) {
+ $_.Key
+ }
+ }
+ }
+
+ switch -Wildcard ($Module.Params.Keys) {
+ '*' {
+ $value = $Module.Params[$_]
+ if ($null -eq $value) {
+ continue
+ }
+
+ $key = $_.Replace('_', '')
+ if ($validParams -and $validParams -notcontains $key) {
+ continue
+ }
+ }
+
+ 'scope' { $value = $value.Replace('_', '') }
+ 'source_username' { continue } # handled in password block
+ 'source_password' {
+ $key = 'Credential'
+ $secure = ConvertTo-SecureString -String $value -AsPlainText -Force
+ $value = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Module.Params.source_username, $secure
+ }
+ '*_version' {
+ if ($Module.Params.state -eq 'latest') {
+ $Module.FailJson("version options can't be used with 'state': 'latest'")
+ }
+ }
+ 'repository' {
+ if ($Module.Params.state -eq 'absent') {
+ $Module.FailJson("'repository' is not valid with 'state': 'absent'")
+ }
+ }
+
+ '*' { $ret[$key] = $value }
+ }
+
+ $ret
+ }
+}
+
+$pGet, $pUninstall, $pFind, $pInstall = Get-Command -Name @(
+ 'Get-InstalledScript'
+ , 'Uninstall-Script'
+ , 'Find-Script'
+ , 'Install-Script'
+) | Get-SplattableParameter -Module $module
+
+$existing = Get-InstalledScript @pGet -ErrorAction SilentlyContinue
+$existing = if ($existing) {
+ $existing | Group-Object -Property Name -AsHashTable -AsString
+}
+else {
+ @{}
+}
+
+if ($state -eq 'absent') {
+ if ($existing.Count) {
+ try {
+ $module.Result.changed = $true
+ if (-not $module.CheckMode) {
+ $existing.Values | Uninstall-Script -Force -ErrorAction Stop
+ }
+ }
+ catch {
+ $module.FailJson("Error uninstalling scripts.", $_)
+ }
+ }
+}
+else {
+ # state is 'present' or 'latest'
+ try {
+ $remote = Find-Script @pFind -ErrorAction Stop
+ }
+ catch {
+ $module.FailJson("Error searching for scripts.", $_)
+ }
+
+ try {
+ $toInstall = $remote | Where-Object -FilterScript {
+ -not $existing.ContainsKey($_.Name) -or (
+ $state -eq 'latest' -and
+ ($_.Version -as [version]) -gt ($existing[$_.Name].Version -as [version])
+ )
+ }
+
+ if (($toInstall | Group-Object -Property Name -NoElement | Where-Object -Property Count -gt 1)) {
+ $module.FailJson("Multiple scripts found. Please choose a specific repository.")
+ }
+
+ $module.Result.changed = $toInstall -as [bool]
+
+ if ($toInstall -and -not $module.CheckMode) {
+ $toInstall | Install-Script -Scope:$pInstall.scope -Force -ErrorAction Stop
+ }
+ }
+ catch {
+ $module.FailJson("Error installing scripts.", $_)
+ }
+}
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_psscript.py b/ansible_collections/community/windows/plugins/modules/win_psscript.py
new file mode 100644
index 00000000..00269d03
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_psscript.py
@@ -0,0 +1,128 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2020, Brian Scholer <@briantist>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_psscript
+short_description: Install and manage PowerShell scripts from a PSRepository
+description:
+ - Add or remove PowerShell scripts from registered PSRepositories.
+options:
+ name:
+ description:
+ - The name of the script you want to install or remove.
+ type: str
+ required: True
+ repository:
+ description:
+ - The registered name of the repository you want to install from.
+ - Cannot be used when I(state=absent).
+ - If ommitted, all repositories will be searched.
+ - To register a repository, use M(community.windows.win_psrepository).
+ type: str
+ scope:
+ description:
+ - Determines whether the script is installed for only the C(current_user) or for C(all_users).
+ type: str
+ choices:
+ - current_user
+ - all_users
+ default: all_users
+ state:
+ description:
+ - The desired state of the script. C(absent) removes the script.
+ - C(latest) will ensure the most recent version available is installed.
+ - C(present) only installs if the script is missing.
+ type: str
+ choices:
+ - present
+ - absent
+ - latest
+ default: present
+ required_version:
+ description:
+ - The exact version of the script to install.
+ - Cannot be used with I(minimum_version) or I(maximum_version).
+ - Cannot be used when I(state=latest).
+ type: str
+ minimum_version:
+ description:
+ - The minimum version of the script to install.
+ - Cannot be used when I(state=latest).
+ type: str
+ maximum_version:
+ description:
+ - The maximum version of the script to install.
+ - Cannot be used when I(state=latest).
+ type: str
+ allow_prerelease:
+ description:
+ - If C(yes) installs scripts flagged as prereleases.
+ type: bool
+ default: no
+ source_username:
+ description:
+ - The username portion of the credential required to access the repository.
+ - Must be used together with I(source_password).
+ type: str
+ source_password:
+ description:
+ - The password portion of the credential required to access the repository.
+ - Must be used together with I(source_username).
+ type: str
+requirements:
+ - C(PowerShellGet) module v1.6.0+
+seealso:
+ - module: community.windows.win_psrepository
+ - module: community.windows.win_psrepository_info
+ - module: community.windows.win_psmodule
+notes:
+ - Unlike PowerShell modules, scripts do not support side-by-side installations of multiple versions. Installing a new version will replace the existing one.
+author:
+ - Brian Scholer (@briantist)
+'''
+
+EXAMPLES = r'''
+- name: Install a script from PSGallery
+ community.windows.win_psscript:
+ name: Test-RPC
+ repository: PSGallery
+
+- name: Find and install the latest version of a script from any repository
+ community.windows.win_psscript:
+ name: Get-WindowsAutoPilotInfo
+ state: latest
+
+- name: Remove a script that isn't needed
+ community.windows.win_psscript:
+ name: Defrag-Partition
+ state: absent
+
+- name: Install a specific version of a script for the current user
+ community.windows.win_psscript:
+ name: CleanOldFiles
+ scope: current_user
+ required_version: 3.10.2
+
+- name: Install a script below a certain version
+ community.windows.win_psscript:
+ name: New-FeatureEnable
+ maximum_version: 2.99.99
+
+- name: Ensure a minimum version of a script is present
+ community.windows.win_psscript:
+ name: OldStandby
+ minimum_version: 3.0.0
+
+- name: Install any available version that fits a specific range
+ community.windows.win_psscript:
+ name: FinickyScript
+ minimum_version: 2.5.1
+ maximum_version: 2.6.19
+'''
+
+RETURN = r'''
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_psscript_info.ps1 b/ansible_collections/community/windows/plugins/modules/win_psscript_info.ps1
new file mode 100644
index 00000000..8f6cc908
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_psscript_info.ps1
@@ -0,0 +1,129 @@
+#!powershell
+
+# Copyright: (c) 2020, Brian Scholer <@briantist>
+# 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.CamelConversion
+#Requires -Module PowerShellGet
+
+$spec = @{
+ options = @{
+ name = @{ type = 'str' ; default = '*' }
+ repository = @{ type = 'str' }
+ }
+ supports_check_mode = $true
+}
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+function Convert-ObjectToSnakeCase {
+ <#
+ .SYNOPSIS
+ Converts an object with CamelCase properties to a dictionary with snake_case keys.
+ Works in the spirit of and depends on the existing CamelConversion module util.
+ #>
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
+ [Object]
+ $InputObject ,
+
+ [Parameter()]
+ [Switch]
+ $NoRecurse ,
+
+ [Parameter()]
+ [Switch]
+ $OmitNull
+ )
+
+ Process {
+ $result = [Ordered]@{}
+ foreach ($property in $InputObject.PSObject.Properties) {
+ $value = $property.Value
+ if (-not $NoRecurse -and $value -is [System.Collections.IDictionary]) {
+ $value = Convert-DictToSnakeCase -dict $value
+ }
+ elseif (-not $NoRecurse -and ($value -is [Array] -or $value -is [System.Collections.ArrayList])) {
+ $value = Convert-ListToSnakeCase -list $value
+ }
+ elseif ($null -eq $value) {
+ if ($OmitNull) {
+ continue
+ }
+ }
+ elseif (-not $NoRecurse -and $value -isnot [System.ValueType] -and $value -isnot [string]) {
+ $value = Convert-ObjectToSnakeCase -InputObject $value
+ }
+
+ $name = Convert-StringToSnakeCase -string $property.Name
+ $result[$name] = $value
+ }
+ $result
+ }
+}
+
+function ConvertTo-SerializableScriptInfo {
+ <#
+ .SYNOPSIS
+ Transforms some members of a PSRepositoryItemInfo object to be more serialization-friendly.
+
+ .DESCRIPTION
+ Stringifies [DateTime], [enum], and [type] values for serialization
+ #>
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
+ [Object]
+ $InputObject ,
+
+ [Parameter()]
+ [string[]]
+ $ExcludeProperty = @(
+ <#
+ Includes is for modules, containing the stuff they export.
+ #>
+ 'Includes' ,
+
+ <#
+ This is always 'Script' for scripts.
+ #>
+ 'Type'
+ )
+ )
+
+ Process {
+ $properties = foreach ($p in $InputObject.PSObject.Properties) {
+ $pName = $p.Name
+ $pValue = $p.Value
+
+ if ($pValue -is [datetime]) {
+ @{
+ Name = $pName
+ Expression = { $pValue.ToString('o') }.GetNewClosure()
+ }
+ }
+ elseif ($pValue -is [enum] -or $pValue -is [type]) {
+ @{
+ Name = $pName
+ Expression = { $pValue.ToString() }.GetNewClosure()
+ }
+ }
+ else {
+ $pName
+ }
+ }
+
+ $InputObject | Select-Object -Property $properties -ExcludeProperty $ExcludeProperty
+ }
+}
+
+$module.Result.scripts = @(
+ Get-InstalledScript -Name $module.Params.name -ErrorAction SilentlyContinue |
+ Where-Object -FilterScript { -not $module.Params.repository -or $_.Repository -eq $module.Params.repository } |
+ ConvertTo-SerializableScriptInfo |
+ Convert-ObjectToSnakeCase -NoRecurse
+)
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_psscript_info.py b/ansible_collections/community/windows/plugins/modules/win_psscript_info.py
new file mode 100644
index 00000000..7e4c4e33
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_psscript_info.py
@@ -0,0 +1,206 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2020, Brian Scholer <@briantist>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_psscript_info
+short_description: Gather information about installed PowerShell Scripts
+description:
+ - Gather information about PowerShell Scripts installed via PowerShellGet.
+options:
+ name:
+ description:
+ - The name of the script.
+ - Supports any wildcard pattern supported by C(Get-InstalledScript).
+ - If omitted then all scripts will returned.
+ type: str
+ default: '*'
+ repository:
+ description:
+ - The name of the PSRepository the scripts were installed from.
+ - This acts as a filter against the scripts that would be returned based on the I(name) option.
+ - Only scripts installed from a registered repository will be returned.
+ - If the repository was re-registered after script installation with a new C(SourceLocation), this will not match.
+ type: str
+requirements:
+ - C(PowerShellGet) module
+seealso:
+ - module: community.windows.win_psrepository_info
+ - module: community.windows.win_psmodule_info
+author:
+ - Brian Scholer (@briantist)
+'''
+
+EXAMPLES = r'''
+- name: Get info about all script on the system
+ community.windows.win_psscript_info:
+
+- name: Get info about the Test-RPC script
+ community.windows.win_psscript_info:
+ name: Test-RPC
+
+- name: Get info about test scripts
+ community.windows.win_psscript_info:
+ name: Test*
+
+- name: Get info about all scripts installed from the PSGallery repository
+ community.windows.win_psscript_info:
+ repository: PSGallery
+ register: gallery_scripts
+
+- name: Update all scripts retrieved from above example
+ community.windows.win_psscript:
+ name: "{{ item }}"
+ state: latest
+ loop: "{{ gallery_scripts.scripts | map(attribute=name) }}"
+
+- name: Get info about all scripts on the system
+ community.windows.win_psscript_info:
+ register: all_scripts
+
+- name: Find scripts installed from a repository that isn't registered now
+ set_fact:
+ missing_repository_scripts: "{{
+ all_scripts
+ | json_query('scripts[?repository!=null && repository==repository_source_location].{name: name, version: version, repository: repository}')
+ | list
+ }}"
+
+- debug:
+ var: missing_repository_scripts
+'''
+
+RETURN = r'''
+scripts:
+ description:
+ - A list of installed scripts (or an empty list is there are none).
+ returned: always
+ type: list
+ elements: dict
+ contains:
+ name:
+ description:
+ - The name of the script.
+ type: str
+ sample: Test-RPC
+ version:
+ description:
+ - The script version.
+ type: str
+ sample: 1.2.3
+ installed_location:
+ description:
+ - The path where the script is installed.
+ type: str
+ sample: 'C:\Program Files\WindowsPowerShell\Scripts'
+ author:
+ description:
+ - The author of the script.
+ type: str
+ sample: Ryan Ries
+ copyright:
+ description:
+ - The copyright of the script.
+ type: str
+ sample: 'Jordan Borean 2017'
+ company_name:
+ description:
+ - The company name of the script.
+ type: str
+ sample: Microsoft Corporation
+ description:
+ description:
+ - The description of the script.
+ type: str
+ sample: This scripts tests network connectivity.
+ dependencies:
+ description:
+ - The script's dependencies.
+ type: list
+ elements: str
+ icon_uri:
+ description:
+ - The address of the icon of the script.
+ type: str
+ sample: 'https://raw.githubusercontent.com/scripter/script/main/logo.png'
+ license_uri:
+ description:
+ - The address of the license for the script.
+ type: str
+ sample: 'https://raw.githubusercontent.com/scripter/script/main/LICENSE'
+ project_uri:
+ description:
+ - The address of the script's project.
+ type: str
+ sample: 'https://github.com/scripter/script'
+ repository_source_location:
+ description:
+ - The source location of the repository where the script was installed from.
+ type: str
+ sample: 'https://www.powershellgallery.com/api/v2'
+ repository:
+ description:
+ - The PSRepository where the script was installed from.
+ - This value is not historical. It depends on the PSRepositories that are registered now for the current user.
+ - The C(repository_source_location) must match the current source location of a registered repository to get a repository name.
+ - If there is no match, then this value will match C(repository_source_location).
+ type: str
+ sample: PSGallery
+ release_notes:
+ description:
+ - The script's release notes. This is a free text field and no specific format should be assumed.
+ type: str
+ sample: |
+ ## 1.5.5
+ - Add optional param for detailed info
+
+ ## 1.4.7
+ - Bug fix for deadlock when getting parameters in an event
+
+ ## 1.1.4
+ - Bug fix when installing package from private feeds
+ installed_date:
+ description:
+ - The date the script was installed.
+ type: str
+ sample: '2018-02-14T17:55:34.9620740-05:00'
+ published_date:
+ description:
+ - The date the script was published.
+ type: str
+ sample: '2017-03-15T04:18:09.0000000'
+ updated_date:
+ description:
+ - The date the script was last updated.
+ type: str
+ sample: '2019-12-31T09:20:02.0000000'
+ package_management_provider:
+ description:
+ - This is the PowerShellGet package management provider used to install the script.
+ type: str
+ sample: NuGet
+ tags:
+ description:
+ - The tags defined in the script's C(AdditionalMetadata).
+ type: list
+ elements: str
+ sample:
+ - networking
+ - serialization
+ - git
+ - dsc
+ power_shell_get_format_version:
+ description:
+ - The version of the PowerShellGet specification format.
+ type: str
+ sample: '2.0'
+ additional_metadata:
+ description:
+ - Additional metadata included with the script or during publishing of the script.
+ - Many of the fields here are surfaced at the top level with some standardization. The values here may differ slightly as a result.
+ - The field names here vary widely in case, and are not normalized or converted to snake_case.
+ type: dict
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_pssession_configuration.ps1 b/ansible_collections/community/windows/plugins/modules/win_pssession_configuration.ps1
new file mode 100644
index 00000000..e4dd6b7b
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_pssession_configuration.ps1
@@ -0,0 +1,555 @@
+#!powershell
+
+# Copyright: (c) 2020, Brian Scholer <@briantist>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#AnsibleRequires -CSharpUtil Ansible.AccessToken
+
+$type = @{
+ guid = [Func[[Object], [System.Guid]]] {
+ [System.Guid]::ParseExact($args[0].Trim([char[]]'}{').Replace('-', ''), 'N')
+ }
+ version = [Func[[Object], [System.Version]]] {
+ [System.Version]::Parse($args[0])
+ }
+ int64 = [Func[[Object], [System.Int64]]] {
+ [System.Int64]::Parse($args[0])
+ }
+ double = [Func[[Object], [System.Double]]] {
+ [System.Double]::Parse($args[0])
+ }
+}
+
+$pssc_options = @{
+ guid = @{ type = $type.guid }
+ schema_version = @{ type = $type.version }
+ author = @{ type = 'str' }
+ description = @{ type = 'str' }
+ company_name = @{ type = 'str' }
+ copyright = @{ type = 'str' }
+ session_type = @{ type = 'str' ; choices = @('default', 'empty', 'restricted_remote_server') }
+ transcript_directory = @{ type = 'path' }
+ run_as_virtual_account = @{ type = 'bool' }
+ run_as_virtual_account_groups = @{ type = 'list' ; elements = 'str' }
+ mount_user_drive = @{ type = 'bool' }
+ user_drive_maximum_size = @{ type = $type.int64 }
+ group_managed_service_account = @{ type = 'str' }
+ scripts_to_process = @{ type = 'list' ; elements = 'str' }
+ role_definitions = @{ type = 'dict' }
+ required_groups = @{ type = 'dict' }
+ language_mode = @{ type = 'str' ; choices = @('no_language', 'restricted_language', 'constrained_language', 'full_language') }
+ execution_policy = @{ type = 'str' ; choices = @('default', 'remote_signed', 'restricted', 'undefined', 'unrestricted') }
+ powershell_version = @{ type = $type.version }
+ modules_to_import = @{ type = 'list' ; elements = 'raw' }
+ visible_aliases = @{ type = 'list' ; elements = 'str' }
+ visible_cmdlets = @{ type = 'list' ; elements = 'raw' }
+ visible_functions = @{ type = 'list' ; elements = 'raw' }
+ visible_external_commands = @{ type = 'list' ; elements = 'str' }
+ alias_definitions = @{ type = 'dict' }
+ function_definitions = @{ type = 'dict' }
+ variable_definitions = @{ type = 'list' ; elements = 'dict' }
+ environment_variables = @{ type = 'dict' }
+ types_to_process = @{ type = 'list' ; elements = 'path' }
+ formats_to_process = @{ type = 'list' ; elements = 'path' }
+ assemblies_to_load = @{ type = 'list' ; elements = 'str' }
+}
+
+$session_configuration_options = @{
+ name = @{ type = 'str' ; required = $true }
+ processor_architecure = @{ type = 'str' ; choices = @('amd64', 'x86') }
+ access_mode = @{ type = 'str' ; choices = @('disabled', 'local', 'remote') }
+ use_shared_process = @{ type = 'bool' }
+ thread_apartment_state = @{ type = 'str' ; choices = @('mta', 'sta') }
+ thread_options = @{ type = 'str' ; choices = @('default', 'reuse_thread', 'use_current_thread', 'use_new_thread') }
+ startup_script = @{ type = 'path' }
+ maximum_received_data_size_per_command_mb = @{ type = $type.double }
+ maximum_received_object_size_mb = @{ type = $type.double }
+ security_descriptor_sddl = @{ type = 'str' }
+ run_as_credential_username = @{ type = 'str' }
+ run_as_credential_password = @{ type = 'str' ; no_log = $true }
+}
+
+$behavior_options = @{
+ state = @{ type = 'str' ; choices = @('present', 'absent') ; default = 'present' }
+ lenient_config_fields = @{ type = 'list' ; elements = 'str' ; default = @('guid', 'author', 'company_name', 'copyright', 'description') }
+ async_timeout = @{ type = 'int' ; default = 300 }
+ async_poll = @{ type = 'int' ; default = 1 }
+ <#
+ # TODO: possible future enhancement to wait for existing connections to finish
+ # Existing connections can be found with:
+ # Get-WSManInstance -ComputerName localhost -ResourceURI shell -Enumerate
+
+ existing_connection_timeout_seconds = @{ type = 'int' ; default = 0 }
+ existing_connection_timeout_interval_ms = @{ type = 'int' ; default = 500 }
+ existing_connection_timeout_action = @{ type = 'str' ; choices = @('terminate', 'fail') ; default = 'terminate' }
+ existing_connection_wait_states = @{ type = 'list' ; elements = 'str' ; default = @('connected') }
+#>
+}
+
+$spec = @{
+ options = $pssc_options + $session_configuration_options + $behavior_options
+ required_together = @(
+ , @('run_as_credential_username', 'run_as_credential_password')
+ )
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+
+function Import-PowerShellDataFileLegacy {
+ <#
+ .SYNOPSIS
+ A pre-PowerShell 5.0 version of Import-PowerShellDataFile
+
+ .DESCRIPTION
+ Safely imports a PowerShell Data file in PowerShell versions before 5.0
+ when the built-in command was introduced. Non-literal Path support is not included.
+ #>
+ [CmdletBinding()]
+ [OutputType([hashtable])]
+ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingInvokeExpression', '', Justification = 'Required to process PS data file')]
+ param(
+ [Parameter(Mandatory = $true)]
+ [Alias('Path')]
+ [String]
+ $LiteralPath
+ )
+
+ End {
+ $astloader = [System.Management.Automation.Language.Parser]::ParseFile($LiteralPath, [ref] $null , [ref] $null)
+ $ht = $astloader.Find({ param($ast)
+ $ast -is [System.Management.Automation.Language.HashtableAst]
+ }, $false)
+
+ if (-not $ht) {
+ throw "Invalid PowerShell Data File."
+ }
+
+ # SafeGetValue() is not available before PowerShell 5 anyway, so we'll do the unsafe load and just execute it.
+ # The only files we're loading are ones we generated from options, or ones that were already attached to existing
+ # session configurations.
+ # $ht.SafeGetValue()
+ Invoke-Expression -Command $ht.Extent.Text
+ }
+}
+
+if (-not (Get-Command -Name 'Microsoft.PowerShell.Utility\Import-PowerShellDataFile' -ErrorAction SilentlyContinue)) {
+ New-Alias -Name 'Import-PowerShellDataFile' -Value 'Import-PowerShellDataFileLegacy'
+}
+
+function ConvertFrom-SnakeCase {
+ [CmdletBinding()]
+ [OutputType([String])]
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
+ [String]
+ $SnakedString
+ )
+
+ Process {
+ [regex]::Replace($SnakedString, '^_?.|_.', { param($m) $m.Value.TrimStart('_').ToUpperInvariant() })
+ }
+}
+
+function ConvertFrom-AnsibleOption {
+ [CmdletBinding()]
+ [OutputType([System.Collections.IDictionary])]
+ param(
+ [Parameter(Mandatory = $true)]
+ [System.Collections.IDictionary]
+ $Params ,
+
+ [Parameter(Mandatory = $true)]
+ [hashtable]
+ $OptionSet
+ )
+
+ End {
+ $ret = @{}
+ foreach ($option in $OptionSet.GetEnumerator()) {
+ $raw_name = $option.Name
+ switch -Wildcard ($raw_name) {
+ 'run_as_credential_*' {
+ $raw_name = $raw_name -replace '_[^_]+$'
+ $name = ConvertFrom-SnakeCase -SnakedString $raw_name
+ if (-not $ret.Contains($name)) {
+ $un = $Params["${raw_name}_username"]
+ if ($un) {
+ $secpw = ConvertTo-SecureString -String $Params["${raw_name}_password"] -AsPlainText -Force
+ $value = New-Object -TypeName PSCredential -ArgumentList $un, $secpw
+ $ret[$name] = $value
+ }
+ }
+ break
+ }
+
+ default {
+ $value = $Params[$raw_name]
+ if ($null -ne $value) {
+ if ($option.Value.choices) {
+ # the options that have choices have them listed in snake_case versions of their real values
+ $value = ConvertFrom-SnakeCase -SnakedString $value
+ }
+ $name = ConvertFrom-SnakeCase -SnakedString $raw_name
+ $ret[$name] = $value
+ }
+ }
+ }
+ }
+
+ $ret
+ }
+}
+
+function Write-GeneratedSessionConfiguration {
+ [CmdletBinding()]
+ [OutputType([System.IO.FileInfo])]
+ param(
+ [Parameter(Mandatory = $true)]
+ [System.Collections.IDictionary]
+ $ParameterSet ,
+
+ [Parameter()]
+ [String]
+ $OutFile
+ )
+
+ End {
+ $file = if ($OutFile) {
+ $OutFile
+ }
+ else {
+ [System.IO.Path]::GetTempFileName()
+ }
+
+ $file = $file -replace '(?<!\.pssc)$', '.pssc'
+ New-PSSessionConfigurationFile -Path $file @ParameterSet
+ [System.IO.FileInfo]$file
+ }
+}
+
+function Compare-ConfigFile {
+ <#
+ .SYNOPSIS
+ This function compares the existing config file to the desired
+
+ .DESCRIPTION
+ We'll load the contents of both the desired and existing config, remove fields that shouldn't be
+ compared, then generate a new config based on the existing and compare those files.
+
+ This could be done as a direct file compare, without loading the contents as objects.
+ The primary reasons to do it this slightly more complicated way are:
+
+ - To ignore GUID as a value that matters: if you don't supply it a new one is generated for you,
+ but PSSessionConfigurations don't use this for anything; it's just metadata. If you supply one,
+ we want to compare it. If you don't, we shouldn't count the "mismatch" against you though.
+
+ - To normalize the existing file based on the following stuff so we avoid unnecessary changes:
+
+ - A file compare either has to be case insensitive (won't catch changes in values) or case sensitive
+ (will may force changes on differences that don't matter, like case differences in key values)
+
+ - A file compare will see changes on whitespace and line ending differences; although those could be
+ accounted for in other ways, this method handles them.
+
+ - A file compare will see changes on other non-impacting syntax style differences like indentation.
+ #>
+ [CmdletBinding()]
+ [OutputType([bool])]
+ param(
+ [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
+ [System.IO.FileInfo]
+ $ConfigFilePath ,
+
+ [Parameter(Mandatory = $true)]
+ [System.IO.FileInfo]
+ $NewConfigFile ,
+
+ [Parameter(Mandatory = $true)]
+ [System.Collections.IDictionary]
+ $Params ,
+
+ [Parameter()]
+ [String[]]
+ $UseExistingIfMissing
+ )
+
+ Process {
+ $desired_config = $NewConfigFile.FullName
+
+ $existing_content = Import-PowerShellDataFile -LiteralPath $ConfigFilePath.FullName
+ $desired_content = Import-PowerShellDataFile -LiteralPath $desired_config
+
+ $regen = $false
+ foreach ($ignorable_param in $UseExistingIfMissing) {
+ # here we're checking for the parameters that shouldn't be compared if they are in the existing
+ # config, but missing in the desired config. To account for this, we copy the value from the
+ # existing into the desired so that when we regenerate it, it'll match the existing if there
+ # aren't other changes.
+ if (-not $Params.Contains($ignorable_param) -and $existing_content.Contains($ignorable_param)) {
+ $desired_content[$ignorable_param] = $existing_content[$ignorable_param]
+ $regen = $true
+ }
+ }
+
+ # re-write and read the desired config file
+ if ($regen) {
+ $NewConfigFile.Delete()
+ $desired_config = Write-GeneratedSessionConfiguration -ParameterSet $desired_content -OutFile $desired_config
+ }
+
+ $desired_content = Get-Content -Raw -LiteralPath $desired_config
+
+ # re-write/import the existing one too to get a pristine version
+ # this will account for unimporant case differences, comments, whitespace, etc.
+ $pristine_config = Write-GeneratedSessionConfiguration -ParameterSet $existing_content
+ $existing_content = Get-Content -Raw -LiteralPath $pristine_config
+
+ # with all this de/serializing out of the way we can just do a simple case-sensitive string compare
+ $desired_content -ceq $existing_content
+
+ Remove-Item -LiteralPath $pristine_config -Force -ErrorAction SilentlyContinue
+ }
+}
+
+function Compare-SessionOption {
+ <#
+ .DESCRIPTION
+ This function is used for comparing the session options that don't get set in the config file.
+ This _should_ have been straightforward for anything other than RunAsCredential, except that for
+ some godforesaken reason a smattering of settings have names that differ from their parameter name.
+
+ This list is defined internally in PowerShell here:
+ https://git.io/JfUk7
+
+ #>
+ [CmdletBinding()]
+ [OutputType([bool])]
+ param(
+ [Parameter(Mandatory = $true)]
+ [System.Collections.IDictionary]
+ $DesiredOptions ,
+
+ [Parameter(Mandatory = $true)]
+ [Object]
+ $ExistingOptions
+ )
+
+ End {
+ $optnamer = @{
+ ThreadApartmentState = 'pssessionthreadapartmentstate'
+ ThreadOptions = 'pssessionthreadoptions'
+ MaximumReceivedDataSizePerCommandMb = 'PSMaximumReceivedDataSizePerCommandMB'
+ MaximumReceivedObjectSizeMb = 'PSMaximumReceivedObjectSizeMB'
+ } | Add-Member -MemberType ScriptMethod -Name GetValueOrKey -Value {
+ param($key)
+
+ $val = $this[$key]
+ if ($null -eq $val) {
+ return $key
+ }
+ else {
+ return $val
+ }
+ } -Force -PassThru
+
+ if ($DesiredOptions.Contains('RunAsCredential')) {
+ # since we can't retrieve/compare password, a change must always be made if a cred is specified
+ return $false
+ }
+ $smatch = $true
+ foreach ($opt in $DesiredOptions.GetEnumerator()) {
+ $smatch = $smatch -and (
+ $existing.($optnamer.GetValueOrKey($opt.Name)) -ceq $opt.Value
+ )
+ if (-not $smatch) {
+ break
+ }
+ }
+ return $smatch
+ }
+}
+
+<#
+ For use with possible future enhancement.
+ Right now the biggest challenges to this are:
+ - Ansible's connection itself: the number doesn't go to 0 while we're here running
+ and waiting for it. I thought being async it would disappear but either that's not
+ the case or it's taking too long to do so.
+
+ - I have not found a reliable way to determine which WinRM connection is the one used for
+ the Ansible connection. Over psrp we can use Get-PSSession -ComputerName but that won't
+ work for the winrm connection plugin.
+
+ - Connections seem to take time to disappear. In tests when trying to start time-limited
+ sessions, like:
+ icm -computername . -scriptblock { Start-Sleep -Seconds 30 } -AsJob
+ After the time elapses the connection lingers for a little while after. Should be ok but
+ does add some challenges to writing tests.
+
+ - Checking for instances of the shell resource looks reliable, but I'm not yet certain
+ if it captures all WinRM connections, like CIM connections. Still would be better than
+ nothing.
+#>
+# function Wait-WinRMConnection {
+# <#
+# .SYNOPSIS
+# Waits for existing WinRM connections to finish
+
+# .DESCRIPTION
+# Finds existing WinRM connections that are in a set of states (configurable), and waits for them
+# to disappear, or times out.
+# #>
+# [CmdletBinding()]
+# param(
+# [Parameter(Mandatory=$true)]
+# [Ansible.Basic.AnsibleModule]
+# $Module
+# )
+
+# End {
+# $action = $Module.Params.existing_connection_timeout_action
+# $states = $Module.Params.existing_connection_wait_states
+# $timeout_ms = [System.Math]::Min(0, $Module.Params.existing_connection_timeout_seconds) * 1000
+# $interval = [System.Math]::Max([System.Math]::Min(100, $Module.Params.existing_connection_timeout_interval_ms), $timeout_ms)
+
+# # Would only with psrp
+# $thiscon = Get-PSSession -ComputerName . | Select-Object -ExpandProperty InstanceId
+
+# $sw = New-Object -TypeName System.Diagnostics.Stopwatch
+
+# do {
+# $connections = Get-WSManInstance -ComputerName localhost -ResourceURI shell -Enumerate |
+# Where-Object -FilterScript {
+# $states -contains $_.State -and (
+# -not $thiscon -or
+# $thiscon -ne $_.ShellId
+# )
+# }
+
+# $sw.Start()
+# Start-Sleep -Milliseconds $interval
+# } while ($connections -and $sw.ElapsedMilliseconds -lt $timeout_ms)
+# $sw.Stop()
+
+# if ($connections -and $action -eq 'fail') {
+# # somehow $connections.Count sometimes is blank (not 0) but I can't figure out how that's possible
+# $Module.FailJson("$($connections.Count) remained after timeout.")
+# }
+# }
+# }
+
+$PSDefaultParameterValues = @{
+ '*-PSSessionConfiguration:Force' = $true
+ 'ConvertFrom-AnsibleOption:Params' = $module.Params
+ 'Wait-WinRMConnection:Module' = $module
+}
+
+$opt_pssc = ConvertFrom-AnsibleOption -OptionSet $pssc_options
+$opt_session = ConvertFrom-AnsibleOption -OptionSet $session_configuration_options
+
+$existing = Get-PSSessionConfiguration -Name $opt_session.Name -ErrorAction SilentlyContinue
+
+try {
+ if ($opt_pssc.Count) {
+ # config file options were passed to the module, so generate a config file from those
+ $desired_config = Write-GeneratedSessionConfiguration -ParameterSet $opt_pssc
+ }
+ if ($existing) {
+ # the endpoint is registered
+ if ($existing.ConfigFilePath -and (Test-Path -LiteralPath $existing.ConfigFilePath)) {
+ # the registered endpoint uses a config file
+ if ($desired_config) {
+ # a desired config file exists, so compare it to the existing one
+ $content_match = $existing |
+ Compare-ConfigFile -NewConfigFile $desired_config -Params $opt_pssc -UseExistingIfMissing (
+ $module.Params.lenient_config_fields | ConvertFrom-SnakeCase
+ )
+ }
+ else {
+ # existing endpoint has a config file but no config file options were passed, so there is no match
+ $content_match = $false
+ }
+ }
+ else {
+ # existing endpoint doesn't use a config file, so it's a match if there are also no config options passed
+ $content_match = $opt_pssc.Count -eq 0
+ }
+
+ $session_match = Compare-SessionOption -DesiredOptions $opt_session -ExistingOptions $existing
+ }
+
+ $state = $module.Params.state
+
+ $create = $state -eq 'present' -and (-not $existing -or -not $content_match)
+ $remove = $existing -and ($state -eq 'absent' -or -not $content_match)
+ $session_change = -not $session_match -and $state -ne 'absent'
+
+ $module.Result.changed = $create -or $remove -or $session_change
+
+ # In this module, we pre-emptively remove the session configuratin if there's any change
+ # in the config file options, and then re-register later if needed.
+ # But if the RunAs credential is wrong, the register will fail, and since we already removed
+ # the existing one, it will be gone.
+ #
+ # So let's ensure we can actually use the credential by logging on with TokenUtil,
+ # that way we can fail before touching the existing config.
+ if ($opt_session.Contains('RunAsCredential')) {
+ $cred = $opt_session.RunAsCredential
+ $username = $cred.Username
+ $domain = $null
+ if ($username.Contains('\')) {
+ $domain, $username = $username.Split('\')
+ }
+ try {
+ $handle = [Ansible.AccessToken.TokenUtil]::LogonUser($username, $domain, $cred.GetNetworkCredential().Password, 'Network', 'Default')
+ $handle.Dispose()
+ }
+ catch {
+ $module.FailJson("Could not validate RunAs Credential: $($_.Exception.Message)", $_)
+ }
+ }
+
+ if (-not $module.CheckMode) {
+ if ($remove) {
+ # Wait-WinRMConnection
+ Unregister-PSSessionConfiguration -Name $opt_session.Name
+ }
+
+ if ($create) {
+ if ($desired_config) {
+ $opt_session.Path = $desired_config
+ }
+ # Wait-WinRMConnection
+ $null = Register-PSSessionConfiguration @opt_session
+ }
+ elseif ($session_change) {
+ $psso = $opt_session
+ # Wait-WinRMConnection
+ Set-PSSessionConfiguration @psso
+ }
+ }
+}
+catch [System.Management.Automation.ParameterBindingException] {
+ $e = $_
+ if ($e.Exception.ErrorId -eq 'NamedParameterNotFound') {
+ $psv = $PSVersionTable.PSVersion.ToString(2)
+ $param = $e.Exception.ParameterName
+ $cmd = $e.InvocationInfo.MyCommand.Name
+ $message = "Parameter '$param' is not available for '$cmd' in PowerShell $psv."
+ }
+ else {
+ $message = "Unknown parameter binding error: $($e.Exception.Message)"
+ }
+
+ $module.FailJson($message, $e)
+}
+finally {
+ if ($desired_config) {
+ Remove-Item -LiteralPath $desired_config -Force -ErrorAction SilentlyContinue
+ }
+}
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_pssession_configuration.py b/ansible_collections/community/windows/plugins/modules/win_pssession_configuration.py
new file mode 100644
index 00000000..d6beb9ab
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_pssession_configuration.py
@@ -0,0 +1,390 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2020, Brian Scholer <@briantist>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_pssession_configuration
+short_description: Manage PSSession Configurations
+description:
+ - Register, unregister, and modify PSSession Configurations for PowerShell remoting.
+options:
+ name:
+ description:
+ - The name of the session configuration to manage.
+ type: str
+ required: yes
+ state:
+ description:
+ - The desired state of the configuration.
+ type: str
+ choices:
+ - present
+ - absent
+ default: present
+ guid:
+ description:
+ - The GUID (UUID) of the session configuration file.
+ - This value is metadata, so it only matters if you use it externally.
+ - If not set, a value will be generated automatically.
+ - Acceptable GUID formats are flexible. Any string of 32 hexadecimal digits will be accepted, with all hyphens C(-) and opening/closing C({}) ignored.
+ - See also I(lenient_config_fields).
+ type: raw
+ schema_version:
+ description:
+ - The schema version of the session configuration file.
+ - If not set, a value will be generated automatically.
+ - Must be a valid .Net System.Version string.
+ type: raw
+ author:
+ description:
+ - The author of the session configuration.
+ - This value is metadata and does not affect the functionality of the session configuration.
+ - If not set, a value may be generated automatically.
+ - See also I(lenient_config_fields).
+ type: str
+ description:
+ description:
+ - The description of the session configuration.
+ - This value is metadata and does not affect the functionality of the session configuration.
+ - See also I(lenient_config_fields).
+ type: str
+ company_name:
+ description:
+ - The company that authored the session configuration.
+ - This value is metadata and does not affect the functionality of the session configuration.
+ - If not set, a value may be generated automatically.
+ - See also I(lenient_config_fields).
+ type: str
+ copyright:
+ description:
+ - The copyright statement of the session configuration.
+ - This value is metadata and does not affect the functionality of the session configuration.
+ - If not set, a value may be generated automatically.
+ - See also I(lenient_config_fields).
+ type: str
+ session_type:
+ description:
+ - Controls what type of session this is.
+ type: str
+ choices:
+ - default
+ - empty
+ - restricted_remote_server
+ transcript_directory:
+ description:
+ - Automatic session transcripts will be written to this directory.
+ type: path
+ run_as_virtual_account:
+ description:
+ - If C(yes) the session runs as a virtual account.
+ - Do not use I(run_as_credential_username) and I(run_as_credential_password) to specify a virtual account.
+ type: bool
+ run_as_virtual_account_groups:
+ description:
+ - If I(run_as_virtual_account=yes) this is a list of groups to add the virtual account to.
+ type: list
+ elements: str
+ mount_user_drive:
+ description:
+ - If C(yes) the session creates and mounts a user-specific PSDrive for use with file transfers.
+ type: bool
+ user_drive_maximum_size:
+ description:
+ - The maximum size of the user drive in bytes.
+ - Must fit into an Int64.
+ type: raw
+ group_managed_service_account:
+ description:
+ - If the session will run as a group managed service account (gMSA) then this is the name.
+ - Do not use I(run_as_credential_username) and I(run_as_credential_password) to specify a gMSA.
+ type: str
+ scripts_to_process:
+ description:
+ - A list of paths to script files ending in C(.ps1) that should be applied to the session.
+ type: list
+ elements: str
+ role_definitions:
+ description:
+ - A dict defining the roles for JEA sessions.
+ - For more information see U(https://docs.microsoft.com/en-us/powershell/scripting/learn/remoting/jea/session-configurations#role-definitions).
+ type: dict
+ required_groups:
+ description:
+ - For JEA sessions, defines conditional access rules about which groups a connecting user must belong to.
+ - For more information see U(https://docs.microsoft.com/en-us/powershell/scripting/learn/remoting/jea/session-configurations#conditional-access-rules).
+ type: dict
+ language_mode:
+ description:
+ - Determines the language mode of the PowerShell session.
+ type: str
+ choices:
+ - no_language
+ - restricted_language
+ - constrained_language
+ - full_language
+ execution_policy:
+ description:
+ - The execution policy controlling script execution in the PowerShell session.
+ type: str
+ choices:
+ - default
+ - remote_signed
+ - restricted
+ - undefined
+ - unrestricted
+ powershell_version:
+ description:
+ - The minimum required PowerShell version for this session.
+ - Must be a valid .Net System.Version string.
+ type: raw
+ modules_to_import:
+ description:
+ - A list of modules that should be imported into the session.
+ - Any valid PowerShell module spec can be used here, so simple str names or dicts can be used.
+ - If a dict is used, no snake_case conversion is done, so the original PowerShell names must be used.
+ type: list
+ elements: raw
+ visible_aliases:
+ description:
+ - The aliases that can be used in the session.
+ - For more information see U(https://docs.microsoft.com/en-us/powershell/scripting/learn/remoting/jea/role-capabilities).
+ type: list
+ elements: str
+ visible_cmdlets:
+ description:
+ - The cmdlets that can be used in the session.
+ - The elements can be simple names or complex command specifications.
+ - For more information see U(https://docs.microsoft.com/en-us/powershell/scripting/learn/remoting/jea/role-capabilities).
+ type: list
+ elements: raw
+ visible_functions:
+ description:
+ - The functions that can be used in the session.
+ - The elements can be simple names or complex command specifications.
+ - For more information see U(https://docs.microsoft.com/en-us/powershell/scripting/learn/remoting/jea/role-capabilities).
+ type: list
+ elements: raw
+ visible_external_commands:
+ description:
+ - The external commands and scripts that can be used in the session.
+ - For more information see U(https://docs.microsoft.com/en-us/powershell/scripting/learn/remoting/jea/role-capabilities).
+ type: list
+ elements: str
+ alias_definitions:
+ description:
+ - A dict that defines aliases for each session.
+ type: dict
+ function_definitions:
+ description:
+ - A dict that defines functions for each session.
+ type: dict
+ variable_definitions:
+ description:
+ - A list of dicts where each elements defines a variable for each session.
+ type: list
+ elements: dict
+ environment_variables:
+ description:
+ - A dict that defines environment variables for each session.
+ type: dict
+ types_to_process:
+ description:
+ - Paths to type definition files to process for each session.
+ type: list
+ elements: path
+ formats_to_process:
+ description:
+ - Paths to format definition files to process for each session.
+ type: list
+ elements: path
+ assemblies_to_load:
+ description:
+ - The assemblies that should be loaded into each session.
+ type: list
+ elements: str
+ processor_architecure:
+ description:
+ - The processor architecture of the session (32 bit vs. 64 bit).
+ type: str
+ choices:
+ - amd64
+ - x86
+ access_mode:
+ description:
+ - Controls whether the session configuration allows connection from the C(local) machine only, both local and C(remote), or none (C(disabled)).
+ type: str
+ choices:
+ - disabled
+ - local
+ - remote
+ use_shared_process:
+ description:
+ - If C(yes) then the session shares a process for each session.
+ type: bool
+ thread_apartment_state:
+ description:
+ - The apartment state for the PowerShell session.
+ type: str
+ choices:
+ - mta
+ - sta
+ thread_options:
+ description:
+ - Sets thread options for the session.
+ type: str
+ choices:
+ - default
+ - reuse_thread
+ - use_current_thread
+ - use_new_thread
+ startup_script:
+ description:
+ - A script that gets run on session startup.
+ type: path
+ maximum_received_data_size_per_command_mb:
+ description:
+ - Sets the maximum received data size per command in MB.
+ - Must fit into a double precision floating point value.
+ type: raw
+ maximum_received_object_size_mb:
+ description:
+ - Sets the maximum object size in MB.
+ - Must fit into a double precision floating point value.
+ type: raw
+ security_descriptor_sddl:
+ description:
+ - An SDDL string that controls which users and groups can connect to the session.
+ - If I(role_definitions) is specified the security descriptor will be set based on that.
+ - If this option is not specified the default security descriptor will be applied.
+ type: str
+ run_as_credential_username:
+ description:
+ - Used to set a RunAs account for the session. All commands executed in the session will be run as this user.
+ - To use a gMSA, see I(group_managed_service_account).
+ - To use a virtual account, see I(run_as_virtual_account) and I(run_as_virtual_account_groups).
+ - Status will always be C(changed) when a RunAs credential is set because the password cannot be retrieved for comparison.
+ type: str
+ run_as_credential_password:
+ description:
+ - The password for I(run_as_credential_username).
+ type: str
+ lenient_config_fields:
+ description:
+ - Some fields used in the session configuration do not affect its function, and are sometimes auto-generated when not specified.
+ - To avoid unnecessarily changing the configuration on each run, the values of these options will only be enforced when they are explicitly specified.
+ type: list
+ elements: str
+ default:
+ - guid
+ - author
+ - company_name
+ - copyright
+ - description
+ async_timeout:
+ description:
+ - Sets a timeout for how long in seconds to wait for asynchronous module execution and waiting for the connection to recover.
+ - Replicates the functionality of the C(async) keyword.
+ - Has no effect in check mode.
+ type: int
+ default: 300
+ async_poll:
+ description:
+ - Sets a delay in seconds between each check of the asynchronous execution status.
+ - Replicates the functionality of the C(poll) keyword.
+ - Has no effect in check mode.
+ - I(async_poll=0) is not supported.
+ type: int
+ default: 1
+notes:
+ - This module will restart the WinRM service on any change. This will terminate all WinRM connections including those by other Ansible runs.
+ - Internally this module uses C(async) when not in check mode to ensure things go smoothly when restarting the WinRM service.
+ - The standard C(async) and C(poll) keywords cannot be used; instead use the I(async_timeout) and I(async_poll) options to control asynchronous execution.
+ - Options that don't list a default value here will use the defaults of C(New-PSSessionConfigurationFile) and C(Register-PSSessionConfiguration).
+ - If a value can be specified in both a session config file and directly in the session options, this module will prefer the setting be in the config file.
+seealso:
+ - name: C(New-PSSessionConfigurationFile) Reference
+ description: Details and defaults for options that end up in the session configuration file.
+ link: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/new-pssessionconfigurationfile
+ - name: C(Register-PSSessionConfiguration) Reference
+ description: Details and defaults for options that are not specified in the session config file.
+ link: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/register-pssessionconfiguration
+ - name: PowerShell Just Enough Administration (JEA)
+ description: Refer to the JEA documentation for advanced usage of some options
+ link: https://docs.microsoft.com/en-us/powershell/scripting/learn/remoting/jea/overview
+ - name: About Session Configurations
+ description: General information about session configurations.
+ link: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_session_configurations
+ - name: About Session Configuration Files
+ description: General information about session configuration files.
+ link: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_session_configuration_files
+author:
+ - Brian Scholer (@briantist)
+'''
+
+EXAMPLES = r'''
+- name: Register a session configuration that loads modules automatically
+ community.windows.win_pssession_configuration:
+ name: WebAdmin
+ modules_to_import:
+ - WebAdministration
+ - IISAdministration
+ description: This endpoint has IIS modules pre-loaded
+
+- name: Set up an admin endpoint with a restricted execution policy
+ community.windows.win_pssession_configuration:
+ name: GloboCorp.Admin
+ company_name: Globo Corp
+ description: Admin Endpoint
+ execution_policy: restricted
+
+- name: Create a complex JEA endpoint
+ community.windows.win_pssession_configuration:
+ name: RBAC.Endpoint
+ session_type: restricted_remote_server
+ run_as_virtual_account: True
+ transcript_directory: '\\server\share\Transcripts'
+ language_mode: no_language
+ execution_policy: restricted
+ role_definitions:
+ 'CORP\IT Support':
+ RoleCapabilities:
+ - PasswordResetter
+ - EmployeeOffboarder
+ 'CORP\Webhosts':
+ RoleCapabilities: IISAdmin
+ visible_functions:
+ - tabexpansion2
+ - help
+ visible_cmdlets:
+ - Get-Help
+ - Name: Get-Service
+ Parameters:
+ - Name: DependentServices
+ - Name: RequiredServices
+ - Name: Name
+ ValidateSet:
+ - WinRM
+ - W3SVC
+ - WAS
+ visible_aliases:
+ - gsv
+ state: present
+
+- name: Remove a session configuration
+ community.windows.win_pssession_configuration:
+ name: UnusedEndpoint
+ state: absent
+
+- name: Set a sessions configuration with tweaked async values
+ community.windows.win_pssession_configuration:
+ name: MySession
+ description: A sample session
+ async_timeout: 500
+ async_poll: 5
+'''
+
+RETURN = r'''
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_rabbitmq_plugin.ps1 b/ansible_collections/community/windows/plugins/modules/win_rabbitmq_plugin.ps1
new file mode 100644
index 00000000..f4ac711c
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_rabbitmq_plugin.ps1
@@ -0,0 +1,156 @@
+#!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
+
+function Get-EnabledPlugin($rabbitmq_plugins_cmd) {
+ $list_plugins_cmd = "$rabbitmq_plugins_cmd list -E -m"
+ try {
+ $enabled_plugins = @(Invoke-Expression "& $list_plugins_cmd" | Where-Object { $_ })
+ return , $enabled_plugins
+ }
+ catch {
+ Fail-Json -obj $result -message "Can't execute `"$($list_plugins_cmd)`": $($_.Exception.Message)"
+ }
+}
+
+function Enable-Plugin($rabbitmq_plugins_cmd, $plugin_name) {
+ $enable_plugin_cmd = "$rabbitmq_plugins_cmd enable $plugin_name"
+ try {
+ Invoke-Expression "& $enable_plugin_cmd"
+ }
+ catch {
+ Fail-Json -obj $result -message "Can't execute `"$($enable_plugin_cmd)`": $($_.Exception.Message)"
+ }
+}
+
+function Disable-Plugin($rabbitmq_plugins_cmd, $plugin_name) {
+ $enable_plugin_cmd = "$rabbitmq_plugins_cmd disable $plugin_name"
+ try {
+ Invoke-Expression "& $enable_plugin_cmd"
+ }
+ catch {
+ Fail-Json -obj $result -message "Can't execute `"$($enable_plugin_cmd)`": $($_.Exception.Message)"
+ }
+}
+
+function Get-RabbitmqPathFromRegistry {
+ $reg64Path = "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\RabbitMQ"
+ $reg32Path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\RabbitMQ"
+
+ if (Test-Path -LiteralPath $reg64Path) {
+ $regPath = $reg64Path
+ }
+ elseif (Test-Path -LiteralPath $reg32Path) {
+ $regPath = $reg32Path
+ }
+
+ if ($regPath) {
+ $path = Split-Path -Parent (Get-ItemProperty -LiteralPath $regPath "UninstallString").UninstallString
+ $version = (Get-ItemProperty -LiteralPath $regPath "DisplayVersion").DisplayVersion
+ return "$path\rabbitmq_server-$version"
+ }
+}
+
+function Get-RabbitmqBinPath($installation_path) {
+ $result = Join-Path -Path $installation_path -ChildPath 'bin'
+ if (Test-Path -LiteralPath $result) {
+ return $result
+ }
+
+ $result = Join-Path -Path $installation_path -ChildPath 'sbin'
+ if (Test-Path -LiteralPath $result) {
+ return $result
+ }
+}
+
+$ErrorActionPreference = "Stop"
+
+$result = @{
+ changed = $false
+ enabled = @()
+ disabled = @()
+}
+
+$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
+
+$names = Get-AnsibleParam -obj $params -name "names" -type "str" -failifempty $true -aliases "name"
+$new_only = Get-AnsibleParam -obj $params -name "new_only" -type "bool" -default $false
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "enabled" -validateset "enabled", "disabled"
+$prefix = Get-AnsibleParam -obj $params -name "prefix" -type "str"
+
+if ($diff_support) {
+ $result.diff = @{}
+ $result.diff.prepared = ""
+}
+
+$plugins = $names.Split(",")
+
+if ($prefix) {
+ $rabbitmq_bin_path = Get-RabbitmqBinPath -installation_path $prefix
+ if (-not $rabbitmq_bin_path) {
+ Fail-Json -obj $result -message "No binary folder in prefix `"$($prefix)`""
+ }
+}
+else {
+ $rabbitmq_reg_path = Get-RabbitmqPathFromRegistry
+ if ($rabbitmq_reg_path) {
+ $rabbitmq_bin_path = Get-RabbitmqBinPath -installation_path $rabbitmq_reg_path
+ }
+}
+
+if ($rabbitmq_bin_path) {
+ $rabbitmq_plugins_cmd = "'$(Join-Path -Path $rabbitmq_bin_path -ChildPath "rabbitmq-plugins")'"
+}
+else {
+ $rabbitmq_plugins_cmd = "rabbitmq-plugins"
+}
+
+$enabled_plugins = Get-EnabledPlugin -rabbitmq_plugins_cmd $rabbitmq_plugins_cmd
+
+if ($state -eq "enabled") {
+ $plugins_to_enable = $plugins | Where-Object { -not ($enabled_plugins -contains $_) }
+ foreach ($plugin in $plugins_to_enable) {
+ if (-not $check_mode) {
+ Enable-Plugin -rabbitmq_plugins_cmd $rabbitmq_plugins_cmd -plugin_name $plugin
+ }
+ if ($diff_support) {
+ $result.diff.prepared += "+[$plugin]`n"
+ }
+ $result.enabled += $plugin
+ $result.changed = $true
+ }
+
+ if (-not $new_only) {
+ $plugins_to_disable = $enabled_plugins | Where-Object { -not ($plugins -contains $_) }
+ foreach ($plugin in $plugins_to_disable) {
+ if (-not $check_mode) {
+ Disable-Plugin -rabbitmq_plugins_cmd $rabbitmq_plugins_cmd -plugin_name $plugin
+ }
+ if ($diff_support) {
+ $result.diff.prepared += "-[$plugin]`n"
+ }
+ $result.disabled += $plugin
+ $result.changed = $true
+ }
+ }
+}
+else {
+ $plugins_to_disable = $enabled_plugins | Where-Object { $plugins -contains $_ }
+ foreach ($plugin in $plugins_to_disable) {
+ if (-not $check_mode) {
+ Disable-Plugin -rabbitmq_plugins_cmd $rabbitmq_plugins_cmd -plugin_name $plugin
+ }
+ if ($diff_support) {
+ $result.diff.prepared += "-[$plugin]`n"
+ }
+ $result.disabled += $plugin
+ $result.changed = $true
+ }
+}
+
+Exit-Json $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_rabbitmq_plugin.py b/ansible_collections/community/windows/plugins/modules/win_rabbitmq_plugin.py
new file mode 100644
index 00000000..c981c8d9
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_rabbitmq_plugin.py
@@ -0,0 +1,58 @@
+#!/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)
+
+DOCUMENTATION = r'''
+---
+module: win_rabbitmq_plugin
+short_description: Manage RabbitMQ plugins
+description:
+ - Manage RabbitMQ plugins.
+options:
+ names:
+ description:
+ - Comma-separated list of plugin names.
+ type: str
+ required: yes
+ aliases: [ name ]
+ new_only:
+ description:
+ - Only enable missing plugins.
+ - Does not disable plugins that are not in the names list.
+ type: bool
+ default: no
+ state:
+ description:
+ - Specify if plugins are to be enabled or disabled.
+ type: str
+ choices: [ disabled, enabled ]
+ default: enabled
+ prefix:
+ description:
+ - Specify a custom install prefix to a Rabbit.
+ type: str
+author:
+ - Artem Zinenko (@ar7z1)
+'''
+
+EXAMPLES = r'''
+- name: Enables the rabbitmq_management plugin
+ community.windows.win_rabbitmq_plugin:
+ names: rabbitmq_management
+ state: enabled
+'''
+
+RETURN = r'''
+enabled:
+ description: List of plugins enabled during task run.
+ returned: always
+ type: list
+ sample: ["rabbitmq_management"]
+disabled:
+ description: List of plugins disabled during task run.
+ returned: always
+ type: list
+ sample: ["rabbitmq_management"]
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_rds_cap.ps1 b/ansible_collections/community/windows/plugins/modules/win_rds_cap.ps1
new file mode 100644
index 00000000..4aa6516b
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_rds_cap.ps1
@@ -0,0 +1,407 @@
+#!powershell
+
+# Copyright: (c) 2018, Kevin Subileau (@ksubileau)
+# 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"
+
+# List of authentication methods as string. Used for parameter validation and conversion to integer flag, so order is important!
+$auth_methods_set = @("none", "password", "smartcard", "both")
+# List of session timeout actions as string. Used for parameter validation and conversion to integer flag, so order is important!
+$session_timeout_actions_set = @("disconnect", "reauth")
+
+$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
+
+$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent", "present", "enabled", "disabled"
+$auth_method = Get-AnsibleParam -obj $params -name "auth_method" -type "str" -validateset $auth_methods_set
+$order = Get-AnsibleParam -obj $params -name "order" -type "int"
+$session_timeout = Get-AnsibleParam -obj $params -name "session_timeout" -type "int"
+$session_timeout_action_params = @{
+ obj = $params
+ name = "session_timeout_action"
+ type = "str"
+ default = "disconnect"
+ validateset = $session_timeout_actions_set
+}
+$session_timeout_action = Get-AnsibleParam @session_timeout_action_params
+$idle_timeout = Get-AnsibleParam -obj $params -name "idle_timeout" -type "int"
+$allow_only_sdrts_servers = Get-AnsibleParam -obj $params -name "allow_only_sdrts_servers" -type "bool"
+$user_groups = Get-AnsibleParam -obj $params -name "user_groups" -type "list"
+$computer_groups = Get-AnsibleParam -obj $params -name "computer_groups" -type "list"
+
+# Device redirections
+$redirect_clipboard = Get-AnsibleParam -obj $params -name "redirect_clipboard" -type "bool"
+$redirect_drives = Get-AnsibleParam -obj $params -name "redirect_drives" -type "bool"
+$redirect_printers = Get-AnsibleParam -obj $params -name "redirect_printers" -type "bool"
+$redirect_serial = Get-AnsibleParam -obj $params -name "redirect_serial" -type "bool"
+$redirect_pnp = Get-AnsibleParam -obj $params -name "redirect_pnp" -type "bool"
+
+
+Function ConvertTo-Sid {
+ [OutputType([string])]
+ [CmdletBinding()]
+ param (
+ [Parameter(Mandatory, ValueFromPipeline)]
+ [string[]]
+ $InputObject
+ )
+
+ process {
+ foreach ($user in $InputObject) {
+ # RDS uses the UPN format with the builtin domain but Convert-ToSID tries to look this up as a domain.
+ # Ensure the input value is in the Netlogon format to ensure BUILTIN is resolved properly
+ if ($user.EndsWith("@builtin", [System.StringComparison]::OrdinalIgnoreCase)) {
+ $user = "BUILTIN\$($user.Substring(0, $user.Length - 8))"
+ }
+
+ Convert-ToSID -account_name $user
+ }
+ }
+}
+
+
+function Get-CAP([string] $name) {
+ $cap_path = "RDS:\GatewayServer\CAP\$name"
+ $cap = @{
+ Name = $name
+ }
+
+ # Fetch CAP properties
+ Get-ChildItem -LiteralPath $cap_path | ForEach-Object { $cap.Add($_.Name, $_.CurrentValue) }
+ # Convert boolean values
+ $cap.Enabled = $cap.Status -eq 1
+ $cap.Remove("Status")
+ $cap.AllowOnlySDRTSServers = $cap.AllowOnlySDRTSServers -eq 1
+
+ # Convert multiple choices values
+ $cap.AuthMethod = $auth_methods_set[$cap.AuthMethod]
+ $cap.SessionTimeoutAction = $session_timeout_actions_set[$cap.SessionTimeoutAction]
+
+ # Fetch CAP device redirection settings
+ $cap.DeviceRedirection = @{}
+ Get-ChildItem -LiteralPath "$cap_path\DeviceRedirection" | ForEach-Object { $cap.DeviceRedirection.Add($_.Name, ($_.CurrentValue -eq 1)) }
+
+ # Fetch CAP user and computer groups in Down-Level Logon format
+ $cap.UserGroups = @(
+ Get-ChildItem -LiteralPath "$cap_path\UserGroups" |
+ Select-Object -ExpandProperty Name |
+ ForEach-Object { Convert-FromSID -sid (ConvertTo-Sid -InputObject $_) }
+ )
+ $cap.ComputerGroups = @(
+ Get-ChildItem -LiteralPath "$cap_path\ComputerGroups" |
+ Select-Object -ExpandProperty Name |
+ ForEach-Object { Convert-FromSID -sid (ConvertTo-Sid -InputObject $_) }
+ )
+
+ return $cap
+}
+
+function Set-CAPPropertyValue {
+ [CmdletBinding(SupportsShouldProcess = $true)]
+ param (
+ [Parameter(Mandatory = $true)]
+ [string] $name,
+ [Parameter(Mandatory = $true)]
+ [string] $property,
+ [Parameter(Mandatory = $true)]
+ $value,
+ [Parameter()]
+ $resultobj = @{}
+ )
+
+ $cap_path = "RDS:\GatewayServer\CAP\$name"
+
+ try {
+ Set-Item -LiteralPath "$cap_path\$property" -Value $value -ErrorAction Stop
+ }
+ catch {
+ Fail-Json -obj $resultobj -message "Failed to set property $property of CAP ${name}: $($_.Exception.Message)"
+ }
+}
+
+$result = @{
+ changed = $false
+}
+$diff_text = $null
+
+# Validate CAP name
+if ($name -match "[*/\\;:?`"<>|\t]+") {
+ Fail-Json -obj $result -message "Invalid character in CAP name."
+}
+
+# Validate user groups
+if ($null -ne $user_groups) {
+ if ($user_groups.Count -lt 1) {
+ Fail-Json -obj $result -message "Parameter 'user_groups' cannot be an empty list."
+ }
+
+ $user_groups = $user_groups | ForEach-Object {
+ $group = $_
+ # Test that the group is resolvable on the local machine
+ $sid = ConvertTo-Sid -InputObject $group
+ if (!$sid) {
+ Fail-Json -obj $result -message "$group is not a valid user group on the host machine or domain"
+ }
+
+ # Return the normalized group name in Down-Level Logon format
+ Convert-FromSID -sid $sid
+ }
+ $user_groups = @($user_groups)
+}
+
+# Validate computer groups
+if ($null -ne $computer_groups) {
+ $computer_groups = $computer_groups | ForEach-Object {
+ $group = $_
+ # Test that the group is resolvable on the local machine
+ $sid = ConvertTo-Sid -InputObject $group
+ if (!$sid) {
+ Fail-Json -obj $result -message "$group is not a valid computer group on the host machine or domain"
+ }
+
+ # Return the normalized group name in Down-Level Logon format
+ Convert-FromSID -sid $sid
+ }
+ $computer_groups = @($computer_groups)
+}
+
+# Validate order parameter
+if ($null -ne $order -and $order -lt 1) {
+ Fail-Json -obj $result -message "Parameter 'order' must be a strictly positive integer."
+}
+
+# Ensure RemoteDesktopServices module is loaded
+if ($null -eq (Get-Module -Name RemoteDesktopServices -ErrorAction SilentlyContinue)) {
+ Import-Module -Name RemoteDesktopServices
+}
+
+# Check if a CAP with the given name already exists
+$cap_exist = Test-Path -LiteralPath "RDS:\GatewayServer\CAP\$name"
+
+if ($state -eq 'absent') {
+ if ($cap_exist) {
+ Remove-Item -LiteralPath "RDS:\GatewayServer\CAP\$name" -Recurse -WhatIf:$check_mode
+ $diff_text += "-[$name]"
+ $result.changed = $true
+ }
+}
+else {
+ $diff_text_added_prefix = ''
+ if (-not $cap_exist) {
+ if ($null -eq $user_groups) {
+ Fail-Json -obj $result -message "User groups must be defined to create a new CAP."
+ }
+
+ # Auth method is required when creating a new CAP. Set it to password by default.
+ if ($null -eq $auth_method) {
+ $auth_method = "password"
+ }
+
+ # Create a new CAP
+ if (-not $check_mode) {
+ $CapArgs = @{
+ Name = $name
+ UserGroupNames = $user_groups -join ';'
+ }
+ $cimParams = @{
+ Namespace = "Root\CIMV2\TerminalServices"
+ ClassName = "Win32_TSGatewayConnectionAuthorizationPolicy"
+ MethodName = "Create"
+ Arguments = $CapArgs
+ }
+ $return = Invoke-CimMethod @cimParams
+ if ($return.ReturnValue -ne 0) {
+ Fail-Json -obj $result -message "Failed to create CAP $name (code: $($return.ReturnValue))"
+ }
+ }
+
+ $cap_exist = -not $check_mode
+
+ $diff_text_added_prefix = '+'
+ $result.changed = $true
+ }
+
+ $diff_text += "$diff_text_added_prefix[$name]`n"
+
+ # We cannot configure a CAP that was created above in check mode as it won't actually exist
+ if ($cap_exist) {
+ $cap = Get-CAP -Name $name
+ $wmi_cap = Get-CimInstance -ClassName Win32_TSGatewayConnectionAuthorizationPolicy -Namespace Root\CIMv2\TerminalServices -Filter "name='$($name)'"
+
+ if ($state -in @('disabled', 'enabled')) {
+ $cap_enabled = $state -ne 'disabled'
+ if ($cap.Enabled -ne $cap_enabled) {
+ $diff_text += "-State = $(@('disabled', 'enabled')[[int]$cap.Enabled])`n+State = $state`n"
+ Set-CAPPropertyValue -Name $name -Property Status -Value ([int]$cap_enabled) -ResultObj $result -WhatIf:$check_mode
+ $result.changed = $true
+ }
+ }
+
+ if ($null -ne $auth_method -and $auth_method -ne $cap.AuthMethod) {
+ $diff_text += "-AuthMethod = $($cap.AuthMethod)`n+AuthMethod = $auth_method`n"
+ $set_params = @{
+ Name = $name
+ Property = "AuthMethod"
+ Value = ([array]::IndexOf($auth_methods_set, $auth_method))
+ ResultObj = $result
+ WhatIf = $check_mode
+ }
+ Set-CAPPropertyValue @set_params
+ $result.changed = $true
+ }
+
+ if ($null -ne $order -and $order -ne $cap.EvaluationOrder) {
+ # Order cannot be greater than the total number of existing CAPs (InvalidArgument exception)
+ $cap_count = (Get-ChildItem -LiteralPath "RDS:\GatewayServer\CAP").Count
+ if ($order -gt $cap_count) {
+ $msg = -join @(
+ "Given value '$order' for parameter 'order' is greater than the number of existing CAPs. "
+ "The actual order will be capped to '$cap_count'."
+ )
+ Add-Warning -obj $result -message $msg
+ $order = $cap_count
+ }
+
+ $diff_text += "-Order = $($cap.EvaluationOrder)`n+Order = $order`n"
+ Set-CAPPropertyValue -Name $name -Property EvaluationOrder -Value $order -ResultObj $result -WhatIf:$check_mode
+ $result.changed = $true
+ }
+
+ if ($null -ne $session_timeout -and ($session_timeout -ne $cap.SessionTimeout -or $session_timeout_action -ne $cap.SessionTimeoutAction)) {
+ try {
+ Set-Item -Path "RDS:\GatewayServer\CAP\$name\SessionTimeout" `
+ -Value $session_timeout `
+ -SessionTimeoutAction ([array]::IndexOf($session_timeout_actions_set, $session_timeout_action)) `
+ -ErrorAction Stop `
+ -WhatIf:$check_mode
+ }
+ catch {
+ Fail-Json -obj $result -message "Failed to set property ComputerGroupType of RAP ${name}: $($_.Exception.Message)"
+ }
+
+ $diff_text += "-SessionTimeoutAction = $($cap.SessionTimeoutAction)`n+SessionTimeoutAction = $session_timeout_action`n"
+ $diff_text += "-SessionTimeout = $($cap.SessionTimeout)`n+SessionTimeout = $session_timeout`n"
+ $result.changed = $true
+ }
+
+ if ($null -ne $idle_timeout -and $idle_timeout -ne $cap.IdleTimeout) {
+ $diff_text += "-IdleTimeout = $($cap.IdleTimeout)`n+IdleTimeout = $idle_timeout`n"
+ Set-CAPPropertyValue -Name $name -Property IdleTimeout -Value $idle_timeout -ResultObj $result -WhatIf:$check_mode
+ $result.changed = $true
+ }
+
+ if ($null -ne $allow_only_sdrts_servers -and $allow_only_sdrts_servers -ne $cap.AllowOnlySDRTSServers) {
+ $diff_text += "-AllowOnlySDRTSServers = $($cap.AllowOnlySDRTSServers)`n+AllowOnlySDRTSServers = $allow_only_sdrts_servers`n"
+ Set-CAPPropertyValue -Name $name -Property AllowOnlySDRTSServers -Value ([int]$allow_only_sdrts_servers) -ResultObj $result -WhatIf:$check_mode
+ $result.changed = $true
+ }
+
+ if ($null -ne $redirect_clipboard -and $redirect_clipboard -ne $cap.DeviceRedirection.Clipboard) {
+ $diff_text += "-RedirectClipboard = $($cap.DeviceRedirection.Clipboard)`n+RedirectClipboard = $redirect_clipboard`n"
+ Set-CAPPropertyValue -Name $name -Property "DeviceRedirection\Clipboard" -Value ([int]$redirect_clipboard) -ResultObj $result -WhatIf:$check_mode
+ $result.changed = $true
+ }
+
+ if ($null -ne $redirect_drives -and $redirect_drives -ne $cap.DeviceRedirection.DiskDrives) {
+ $diff_text += "-RedirectDrives = $($cap.DeviceRedirection.DiskDrives)`n+RedirectDrives = $redirect_drives`n"
+ Set-CAPPropertyValue -Name $name -Property "DeviceRedirection\DiskDrives" -Value ([int]$redirect_drives) -ResultObj $result -WhatIf:$check_mode
+ $result.changed = $true
+ }
+
+ if ($null -ne $redirect_printers -and $redirect_printers -ne $cap.DeviceRedirection.Printers) {
+ $diff_text += "-RedirectPrinters = $($cap.DeviceRedirection.Printers)`n+RedirectPrinters = $redirect_printers`n"
+ Set-CAPPropertyValue -Name $name -Property "DeviceRedirection\Printers" -Value ([int]$redirect_printers) -ResultObj $result -WhatIf:$check_mode
+ $result.changed = $true
+ }
+
+ if ($null -ne $redirect_serial -and $redirect_serial -ne $cap.DeviceRedirection.SerialPorts) {
+ $diff_text += "-RedirectSerial = $($cap.DeviceRedirection.SerialPorts)`n+RedirectSerial = $redirect_serial`n"
+ Set-CAPPropertyValue -Name $name -Property "DeviceRedirection\SerialPorts" -Value ([int]$redirect_serial) -ResultObj $result -WhatIf:$check_mode
+ $result.changed = $true
+ }
+
+ if ($null -ne $redirect_pnp -and $redirect_pnp -ne $cap.DeviceRedirection.PlugAndPlayDevices) {
+ $diff_text += "-RedirectPnP = $($cap.DeviceRedirection.PlugAndPlayDevices)`n+RedirectPnP = $redirect_pnp`n"
+ Set-CAPPropertyValue -Name $name -Property "DeviceRedirection\PlugAndPlayDevices" -Value ([int]$redirect_pnp) -ResultObj $result -WhatIf:$check_mode
+ $result.changed = $true
+ }
+
+ if ($null -ne $user_groups) {
+ $groups_to_remove = @($cap.UserGroups | Where-Object { $user_groups -notcontains $_ })
+ $groups_to_add = @($user_groups | Where-Object { $cap.UserGroups -notcontains $_ })
+
+ $user_groups_diff = $null
+ foreach ($group in $groups_to_add) {
+ if (-not $check_mode) {
+ $return = $wmi_cap | Invoke-CimMethod -MethodName AddUserGroupNames -Arguments @{ UserGroupNames = $group }
+ if ($return.ReturnValue -ne 0) {
+ Fail-Json -obj $result -message "Failed to add user group $($group) (code: $($return.ReturnValue))"
+ }
+ }
+ $user_groups_diff += " +$group`n"
+ $result.changed = $true
+ }
+
+ foreach ($group in $groups_to_remove) {
+ if (-not $check_mode) {
+ $return = $wmi_cap | Invoke-CimMethod -MethodName RemoveUserGroupNames -Arguments @{ UserGroupNames = $group }
+ if ($return.ReturnValue -ne 0) {
+ Fail-Json -obj $result -message "Failed to remove user group $($group) (code: $($return.ReturnValue))"
+ }
+ }
+ $user_groups_diff += " -$group`n"
+ $result.changed = $true
+ }
+
+ if ($user_groups_diff) {
+ $diff_text += "~UserGroups`n$user_groups_diff"
+ }
+ }
+
+ if ($null -ne $computer_groups) {
+ $groups_to_remove = @($cap.ComputerGroups | Where-Object { $computer_groups -notcontains $_ })
+ $groups_to_add = @($computer_groups | Where-Object { $cap.ComputerGroups -notcontains $_ })
+
+ $computer_groups_diff = $null
+ foreach ($group in $groups_to_add) {
+ if (-not $check_mode) {
+ $return = $wmi_cap | Invoke-CimMethod -MethodName AddComputerGroupNames -Arguments @{ ComputerGroupNames = $group }
+ if ($return.ReturnValue -ne 0) {
+ Fail-Json -obj $result -message "Failed to add computer group $($group) (code: $($return.ReturnValue))"
+ }
+ }
+ $computer_groups_diff += " +$group`n"
+ $result.changed = $true
+ }
+
+ foreach ($group in $groups_to_remove) {
+ if (-not $check_mode) {
+ $return = $wmi_cap | Invoke-CimMethod -MethodName RemoveComputerGroupNames -Arguments @{ ComputerGroupNames = $group }
+ if ($return.ReturnValue -ne 0) {
+ Fail-Json -obj $result -message "Failed to remove computer group $($group) (code: $($return.ReturnValue))"
+ }
+ }
+ $computer_groups_diff += " -$group`n"
+ $result.changed = $true
+ }
+
+ if ($computer_groups_diff) {
+ $diff_text += "~ComputerGroups`n$computer_groups_diff"
+ }
+ }
+ }
+}
+
+if ($diff_mode -and $result.changed -eq $true) {
+ $result.diff = @{
+ prepared = $diff_text
+ }
+}
+
+Exit-Json $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_rds_cap.py b/ansible_collections/community/windows/plugins/modules/win_rds_cap.py
new file mode 100644
index 00000000..2513b047
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_rds_cap.py
@@ -0,0 +1,128 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Kevin Subileau (@ksubileau)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_rds_cap
+short_description: Manage Connection Authorization Policies (CAP) on a Remote Desktop Gateway server
+description:
+ - Creates, removes and configures a Remote Desktop connection authorization policy (RD CAP).
+ - A RD CAP allows you to specify the users who can connect to a Remote Desktop Gateway server.
+author:
+ - Kevin Subileau (@ksubileau)
+options:
+ name:
+ description:
+ - Name of the connection authorization policy.
+ type: str
+ required: yes
+ state:
+ description:
+ - The state of connection authorization policy.
+ - If C(absent) will ensure the policy is removed.
+ - If C(present) will ensure the policy is configured and exists.
+ - If C(enabled) will ensure the policy is configured, exists and enabled.
+ - If C(disabled) will ensure the policy is configured, exists, but disabled.
+ type: str
+ choices: [ absent, enabled, disabled, present ]
+ default: present
+ auth_method:
+ description:
+ - Specifies how the RD Gateway server authenticates users.
+ - When a new CAP is created, the default value is C(password).
+ type: str
+ choices: [ both, none, password, smartcard ]
+ order:
+ description:
+ - Evaluation order of the policy.
+ - The CAP in which I(order) is set to a value of '1' is evaluated first.
+ - By default, a newly created CAP will take the first position.
+ - If the given value exceed the total number of existing policies,
+ the policy will take the last position but the evaluation order
+ will be capped to this number.
+ type: int
+ session_timeout:
+ description:
+ - The maximum time, in minutes, that a session can be idle.
+ - A value of zero disables session timeout.
+ type: int
+ session_timeout_action:
+ description:
+ - The action the server takes when a session times out.
+ - 'C(disconnect): disconnect the session.'
+ - 'C(reauth): silently reauthenticate and reauthorize the session.'
+ type: str
+ choices: [ disconnect, reauth ]
+ default: disconnect
+ idle_timeout:
+ description:
+ - Specifies the time interval, in minutes, after which an idle session is disconnected.
+ - A value of zero disables idle timeout.
+ type: int
+ allow_only_sdrts_servers:
+ description:
+ - Specifies whether connections are allowed only to Remote Desktop Session Host servers that
+ enforce Remote Desktop Gateway redirection policy.
+ type: bool
+ user_groups:
+ description:
+ - A list of user groups that is allowed to connect to the Remote Gateway server.
+ - Required when a new CAP is created.
+ type: list
+ elements: str
+ computer_groups:
+ description:
+ - A list of computer groups that is allowed to connect to the Remote Gateway server.
+ type: list
+ elements: str
+ redirect_clipboard:
+ description:
+ - Allow clipboard redirection.
+ type: bool
+ redirect_drives:
+ description:
+ - Allow disk drive redirection.
+ type: bool
+ redirect_printers:
+ description:
+ - Allow printers redirection.
+ type: bool
+ redirect_serial:
+ description:
+ - Allow serial port redirection.
+ type: bool
+ redirect_pnp:
+ description:
+ - Allow Plug and Play devices redirection.
+ type: bool
+requirements:
+ - Windows Server 2008R2 (6.1) or higher.
+ - The Windows Feature "RDS-Gateway" must be enabled.
+seealso:
+- module: community.windows.win_rds_cap
+- module: community.windows.win_rds_rap
+- module: community.windows.win_rds_settings
+'''
+
+EXAMPLES = r'''
+- name: Create a new RDS CAP with a 30 minutes timeout and clipboard redirection enabled
+ community.windows.win_rds_cap:
+ name: My CAP
+ user_groups:
+ - BUILTIN\users
+ session_timeout: 30
+ session_timeout_action: disconnect
+ allow_only_sdrts_servers: yes
+ redirect_clipboard: yes
+ redirect_drives: no
+ redirect_printers: no
+ redirect_serial: no
+ redirect_pnp: no
+ state: enabled
+'''
+
+RETURN = r'''
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_rds_rap.ps1 b/ansible_collections/community/windows/plugins/modules/win_rds_rap.ps1
new file mode 100644
index 00000000..956a812d
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_rds_rap.ps1
@@ -0,0 +1,318 @@
+#!powershell
+
+# Copyright: (c) 2018, Kevin Subileau (@ksubileau)
+# 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"
+
+# List of authentication methods as string. Used for parameter validation and conversion to integer flag, so order is important!
+$computer_group_types = @("rdg_group", "ad_network_resource_group", "allow_any")
+$computer_group_types_wmi = @{rdg_group = "RG"; ad_network_resource_group = "CG"; allow_any = "ALL" }
+
+$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
+
+$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
+$description = Get-AnsibleParam -obj $params -name "description" -type "str"
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent", "present", "enabled", "disabled"
+$computer_group_type = Get-AnsibleParam -obj $params -name "computer_group_type" -type "str" -validateset $computer_group_types
+$computer_group_failure = ($computer_group_type -eq "ad_network_resource_group" -or $computer_group_type -eq "rdg_group")
+$computer_group = Get-AnsibleParam -obj $params -name "computer_group" -type "str" -failifempty $computer_group_failure
+$user_groups = Get-AnsibleParam -obj $params -name "user_groups" -type "list"
+$allowed_ports = Get-AnsibleParam -obj $params -name "allowed_ports" -type "list"
+
+
+Function ConvertTo-Sid {
+ [OutputType([string])]
+ [CmdletBinding()]
+ param (
+ [Parameter(Mandatory, ValueFromPipeline)]
+ [string[]]
+ $InputObject
+ )
+
+ process {
+ foreach ($user in $InputObject) {
+ # RDS uses the UPN format with the builtin domain but Convert-ToSID tries to look this up as a domain.
+ # Ensure the input value is in the Netlogon format to ensure BUILTIN is resolved properly
+ if ($user.EndsWith("@builtin", [System.StringComparison]::OrdinalIgnoreCase)) {
+ $user = "BUILTIN\$($user.Substring(0, $user.Length - 8))"
+ }
+
+ Convert-ToSID -account_name $user
+ }
+ }
+}
+
+
+function Get-RAP([string] $name) {
+ $rap_path = "RDS:\GatewayServer\RAP\$name"
+ $rap = @{
+ Name = $name
+ }
+
+ # Fetch RAP properties
+ Get-ChildItem -LiteralPath $rap_path | ForEach-Object { $rap.Add($_.Name, $_.CurrentValue) }
+ # Convert boolean values
+ $rap.Enabled = $rap.Status -eq 1
+ $rap.Remove("Status")
+
+ # Convert computer group name from UPN to Down-Level Logon format
+ if ($rap.ComputerGroupType -ne 2) {
+ $rap.ComputerGroup = Convert-FromSID -sid (ConvertTo-SID -InputObject $rap.ComputerGroup)
+ }
+
+ # Convert multiple choices values
+ $rap.ComputerGroupType = $computer_group_types[$rap.ComputerGroupType]
+
+ # Convert allowed ports from string to list
+ if ($rap.PortNumbers -eq '*') {
+ $rap.PortNumbers = @("any")
+ }
+ else {
+ $rap.PortNumbers = @($rap.PortNumbers -split ',')
+ }
+
+ # Fetch RAP user groups in Down-Level Logon format
+ $rap.UserGroups = @(
+ Get-ChildItem -LiteralPath "$rap_path\UserGroups" |
+ Select-Object -ExpandProperty Name |
+ ForEach-Object { Convert-FromSID -sid (ConvertTo-Sid -InputObject $_) }
+ )
+
+ return $rap
+}
+
+function Set-RAPPropertyValue {
+ [CmdletBinding(SupportsShouldProcess = $true)]
+ param (
+ [Parameter(Mandatory = $true)]
+ [string] $name,
+ [Parameter(Mandatory = $true)]
+ [string] $property,
+ [Parameter(Mandatory = $true)]
+ $value,
+ [Parameter()]
+ $resultobj = @{}
+ )
+
+ $rap_path = "RDS:\GatewayServer\RAP\$name"
+
+ try {
+ Set-Item -LiteralPath "$rap_path\$property" -Value $value -ErrorAction stop
+ }
+ catch {
+ Fail-Json -obj $resultobj -message "Failed to set property $property of RAP ${name}: $($_.Exception.Message)"
+ }
+}
+
+$result = @{
+ changed = $false
+}
+$diff_text = $null
+
+# Validate RAP name
+if ($name -match "[*/\\;:?`"<>|\t]+") {
+ Fail-Json -obj $result -message "Invalid character in RAP name."
+}
+
+# Validate user groups
+if ($null -ne $user_groups) {
+ if ($user_groups.Count -lt 1) {
+ Fail-Json -obj $result -message "Parameter 'user_groups' cannot be an empty list."
+ }
+
+ $user_groups = $user_groups | ForEach-Object {
+ $group = $_
+ # Test that the group is resolvable on the local machine
+ $sid = ConvertTo-Sid -InputObject $group
+ if (!$sid) {
+ Fail-Json -obj $result -message "$group is not a valid user group on the host machine or domain."
+ }
+
+ # Return the normalized group name in Down-Level Logon format
+ Convert-FromSID -sid $sid
+ }
+ $user_groups = @($user_groups)
+}
+
+# Validate computer group parameter
+if ($computer_group_type -eq "allow_any" -and $null -ne $computer_group) {
+ Add-Warning -obj $result -message "Parameter 'computer_group' ignored because the computer_group_type is set to allow_any."
+}
+elseif ($computer_group_type -eq "rdg_group" -and -not (Test-Path -LiteralPath "RDS:\GatewayServer\GatewayManagedComputerGroups\$computer_group")) {
+ Fail-Json -obj $result -message "$computer_group is not a valid gateway managed computer group"
+}
+elseif ($computer_group_type -eq "ad_network_resource_group") {
+ $sid = ConvertTo-Sid -InputObject $computer_group
+ if (!$sid) {
+ Fail-Json -obj $result -message "$computer_group is not a valid computer group on the host machine or domain."
+ }
+ # Ensure the group name is in Down-Level Logon format
+ $computer_group = Convert-FromSID -sid $sid
+}
+
+# Validate port numbers
+if ($null -ne $allowed_ports) {
+ foreach ($port in $allowed_ports) {
+ if (-not ($port -eq "any" -or ($port -is [int] -and $port -ge 1 -and $port -le 65535))) {
+ Fail-Json -obj $result -message "$port is not a valid port number."
+ }
+ }
+}
+
+# Ensure RemoteDesktopServices module is loaded
+if ($null -eq (Get-Module -Name RemoteDesktopServices -ErrorAction SilentlyContinue)) {
+ Import-Module -Name RemoteDesktopServices
+}
+
+# Check if a RAP with the given name already exists
+$rap_exist = Test-Path -LiteralPath "RDS:\GatewayServer\RAP\$name"
+
+if ($state -eq 'absent') {
+ if ($rap_exist) {
+ Remove-Item -LiteralPath "RDS:\GatewayServer\RAP\$name" -Recurse -WhatIf:$check_mode
+ $diff_text += "-[$name]"
+ $result.changed = $true
+ }
+}
+else {
+ $diff_text_added_prefix = ''
+ if (-not $rap_exist) {
+ if ($null -eq $user_groups) {
+ Fail-Json -obj $result -message "User groups must be defined to create a new RAP."
+ }
+
+ # Computer group type is required when creating a new RAP. Set it to allow connect to any resource by default.
+ if ($null -eq $computer_group_type) {
+ $computer_group_type = "allow_any"
+ }
+
+ # Create a new RAP
+ if (-not $check_mode) {
+ $RapArgs = @{
+ Name = $name
+ ResourceGroupType = 'ALL'
+ UserGroupNames = $user_groups -join ';'
+ ProtocolNames = 'RDP'
+ PortNumbers = '*'
+ }
+ $cimParams = @{
+ Namespace = "Root\CIMV2\TerminalServices"
+ ClassName = "Win32_TSGatewayResourceAuthorizationPolicy"
+ MethodName = "Create"
+ Arguments = $RapArgs
+ }
+ $return = Invoke-CimMethod @cimParams
+ if ($return.ReturnValue -ne 0) {
+ Fail-Json -obj $result -message "Failed to create RAP $name (code: $($return.ReturnValue))"
+ }
+ }
+ $rap_exist = -not $check_mode
+
+ $diff_text_added_prefix = '+'
+ $result.changed = $true
+ }
+
+ $diff_text += "$diff_text_added_prefix[$name]`n"
+
+ # We cannot configure a RAP that was created above in check mode as it won't actually exist
+ if ($rap_exist) {
+ $rap = Get-RAP -Name $name
+ $wmi_rap = Get-CimInstance -ClassName Win32_TSGatewayResourceAuthorizationPolicy -Namespace Root\CIMv2\TerminalServices -Filter "name='$($name)'"
+
+ if ($state -in @('disabled', 'enabled')) {
+ $rap_enabled = $state -ne 'disabled'
+ if ($rap.Enabled -ne $rap_enabled) {
+ $diff_text += "-State = $(@('disabled', 'enabled')[[int]$rap.Enabled])`n+State = $state`n"
+ Set-RAPPropertyValue -Name $name -Property Status -Value ([int]$rap_enabled) -ResultObj $result -WhatIf:$check_mode
+ $result.changed = $true
+ }
+ }
+
+ if ($null -ne $description -and $description -ne $rap.Description) {
+ Set-RAPPropertyValue -Name $name -Property Description -Value $description -ResultObj $result -WhatIf:$check_mode
+ $diff_text += "-Description = $($rap.Description)`n+Description = $description`n"
+ $result.changed = $true
+ }
+
+ if ($null -ne $allowed_ports -and @(Compare-Object $rap.PortNumbers $allowed_ports -SyncWindow 0).Count -ne 0) {
+ $diff_text += "-AllowedPorts = [$($rap.PortNumbers -join ',')]`n+AllowedPorts = [$($allowed_ports -join ',')]`n"
+ if ($allowed_ports -contains 'any') { $allowed_ports = '*' }
+ Set-RAPPropertyValue -Name $name -Property PortNumbers -Value $allowed_ports -ResultObj $result -WhatIf:$check_mode
+ $result.changed = $true
+ }
+
+ if ($null -ne $computer_group_type -and $computer_group_type -ne $rap.ComputerGroupType) {
+ $diff_text += "-ComputerGroupType = $($rap.ComputerGroupType)`n+ComputerGroupType = $computer_group_type`n"
+ if ($computer_group_type -ne "allow_any") {
+ $diff_text += "+ComputerGroup = $computer_group`n"
+ }
+ $return = $wmi_rap | Invoke-CimMethod -MethodName SetResourceGroup -Arguments @{
+ ResourceGroupName = $computer_group
+ ResourceGroupType = $computer_group_types_wmi.$($computer_group_type)
+ }
+ if ($return.ReturnValue -ne 0) {
+ Fail-Json -obj $result -message "Failed to set computer group type to $($computer_group_type) (code: $($return.ReturnValue))"
+ }
+
+ $result.changed = $true
+
+ }
+ elseif ($null -ne $computer_group -and $computer_group -ne $rap.ComputerGroup) {
+ $diff_text += "-ComputerGroup = $($rap.ComputerGroup)`n+ComputerGroup = $computer_group`n"
+ $return = $wmi_rap | Invoke-CimMethod -MethodName SetResourceGroup -Arguments @{
+ ResourceGroupName = $computer_group
+ ResourceGroupType = $computer_group_types_wmi.$($rap.ComputerGroupType)
+ }
+ if ($return.ReturnValue -ne 0) {
+ Fail-Json -obj $result -message "Failed to set computer group name to $($computer_group) (code: $($return.ReturnValue))"
+ }
+ $result.changed = $true
+ }
+
+ if ($null -ne $user_groups) {
+ $groups_to_remove = @($rap.UserGroups | Where-Object { $user_groups -notcontains $_ })
+ $groups_to_add = @($user_groups | Where-Object { $rap.UserGroups -notcontains $_ })
+
+ $user_groups_diff = $null
+ foreach ($group in $groups_to_add) {
+ if (-not $check_mode) {
+ $return = $wmi_rap | Invoke-CimMethod -MethodName AddUserGroupNames -Arguments @{ UserGroupNames = $group }
+ if ($return.ReturnValue -ne 0) {
+ Fail-Json -obj $result -message "Failed to add user group $($group) (code: $($return.ReturnValue))"
+ }
+ }
+ $user_groups_diff += " +$group`n"
+ $result.changed = $true
+ }
+
+ foreach ($group in $groups_to_remove) {
+ if (-not $check_mode) {
+ $return = $wmi_rap | Invoke-CimMethod -MethodName RemoveUserGroupNames -Arguments @{ UserGroupNames = $group }
+ if ($return.ReturnValue -ne 0) {
+ Fail-Json -obj $result -message "Failed to remove user group $($group) (code: $($return.ReturnValue))"
+ }
+ }
+ $user_groups_diff += " -$group`n"
+ $result.changed = $true
+ }
+
+ if ($user_groups_diff) {
+ $diff_text += "~UserGroups`n$user_groups_diff"
+ }
+ }
+ }
+}
+
+if ($diff_mode -and $result.changed -eq $true) {
+ $result.diff = @{
+ prepared = $diff_text
+ }
+}
+
+Exit-Json $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_rds_rap.py b/ansible_collections/community/windows/plugins/modules/win_rds_rap.py
new file mode 100644
index 00000000..24ca0f77
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_rds_rap.py
@@ -0,0 +1,86 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Kevin Subileau (@ksubileau)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_rds_rap
+short_description: Manage Resource Authorization Policies (RAP) on a Remote Desktop Gateway server
+description:
+ - Creates, removes and configures a Remote Desktop resource authorization policy (RD RAP).
+ - A RD RAP allows you to specify the network resources (computers) that users can connect
+ to remotely through a Remote Desktop Gateway server.
+author:
+ - Kevin Subileau (@ksubileau)
+options:
+ name:
+ description:
+ - Name of the resource authorization policy.
+ required: yes
+ state:
+ description:
+ - The state of resource authorization policy.
+ - If C(absent) will ensure the policy is removed.
+ - If C(present) will ensure the policy is configured and exists.
+ - If C(enabled) will ensure the policy is configured, exists and enabled.
+ - If C(disabled) will ensure the policy is configured, exists, but disabled.
+ type: str
+ choices: [ absent, disabled, enabled, present ]
+ default: present
+ description:
+ description:
+ - Optional description of the resource authorization policy.
+ type: str
+ user_groups:
+ description:
+ - List of user groups that are associated with this resource authorization policy (RAP).
+ A user must belong to one of these groups to access the RD Gateway server.
+ - Required when a new RAP is created.
+ type: list
+ elements: str
+ allowed_ports:
+ description:
+ - List of port numbers through which connections are allowed for this policy.
+ - To allow connections through any port, specify 'any'.
+ type: list
+ elements: str
+ computer_group_type:
+ description:
+ - 'The computer group type:'
+ - 'C(rdg_group): RD Gateway-managed group'
+ - 'C(ad_network_resource_group): Active Directory Domain Services network resource group'
+ - 'C(allow_any): Allow users to connect to any network resource.'
+ type: str
+ choices: [ rdg_group, ad_network_resource_group, allow_any ]
+ computer_group:
+ description:
+ - The computer group name that is associated with this resource authorization policy (RAP).
+ - This is required when I(computer_group_type) is C(rdg_group) or C(ad_network_resource_group).
+ type: str
+requirements:
+ - Windows Server 2008R2 (6.1) or higher.
+ - The Windows Feature "RDS-Gateway" must be enabled.
+seealso:
+- module: community.windows.win_rds_cap
+- module: community.windows.win_rds_rap
+- module: community.windows.win_rds_settings
+'''
+
+EXAMPLES = r'''
+- name: Create a new RDS RAP
+ community.windows.win_rds_rap:
+ name: My RAP
+ description: Allow all users to connect to any resource through ports 3389 and 3390
+ user_groups:
+ - BUILTIN\users
+ computer_group_type: allow_any
+ allowed_ports:
+ - 3389
+ - 3390
+ state: enabled
+'''
+
+RETURN = r'''
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_rds_settings.ps1 b/ansible_collections/community/windows/plugins/modules/win_rds_settings.ps1
new file mode 100644
index 00000000..7927f333
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_rds_settings.ps1
@@ -0,0 +1,95 @@
+#!powershell
+
+# Copyright: (c) 2018, Kevin Subileau (@ksubileau)
+# 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"
+
+# List of ssl bridging methods as string. Used for parameter validation and conversion to integer flag, so order is important!
+$ssl_bridging_methods = @("none", "https_http", "https_https")
+
+$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
+
+$certificate = Get-AnsibleParam $params -name "certificate_hash" -type "str"
+$max_connections = Get-AnsibleParam $params -name "max_connections" -type "int"
+$ssl_bridging = Get-AnsibleParam -obj $params -name "ssl_bridging" -type "str" -validateset $ssl_bridging_methods
+$enable_only_messaging_capable_clients = Get-AnsibleParam $params -name "enable_only_messaging_capable_clients" -type "bool"
+
+$result = @{
+ changed = $false
+}
+$diff_text = $null
+
+# Ensure RemoteDesktopServices module is loaded
+if ($null -eq (Get-Module -Name RemoteDesktopServices -ErrorAction SilentlyContinue)) {
+ Import-Module -Name RemoteDesktopServices
+}
+
+if ($null -ne $certificate) {
+ # Validate cert path
+ $cert_path = "cert:\LocalMachine\My\$certificate"
+ If (-not (Test-Path -LiteralPath $cert_path) ) {
+ Fail-Json -obj $result -message "Unable to locate certificate at $cert_path"
+ }
+
+ # Get current certificate hash
+ $current_cert = (Get-Item -LiteralPath "RDS:\GatewayServer\SSLCertificate\Thumbprint").CurrentValue
+ if ($current_cert -ne $certificate) {
+ Set-Item -LiteralPath "RDS:\GatewayServer\SSLCertificate\Thumbprint" -Value $certificate -WhatIf:$check_mode
+ $diff_text += "-Certificate = $current_cert`n+Certificate = $certificate`n"
+ $result.changed = $true
+ }
+}
+
+if ($null -ne $max_connections) {
+ # Set the correct value for unlimited connections
+ # TODO Use a more explicit value, maybe a string (ex: "max", "none" or "unlimited") ?
+ If ($max_connections -eq -1) {
+ $max_connections = (Get-Item -LiteralPath "RDS:\GatewayServer\MaxConnectionsAllowed").CurrentValue
+ }
+
+ # Get current connections limit
+ $current_max_connections = (Get-Item -LiteralPath "RDS:\GatewayServer\MaxConnections").CurrentValue
+ if ($current_max_connections -ne $max_connections) {
+ Set-Item -LiteralPath "RDS:\GatewayServer\MaxConnections" -Value $max_connections -WhatIf:$check_mode
+ $diff_text += "-MaxConnections = $current_max_connections`n+MaxConnections = $max_connections`n"
+ $result.changed = $true
+ }
+}
+
+if ($null -ne $ssl_bridging) {
+ $current_ssl_bridging = (Get-Item -LiteralPath "RDS:\GatewayServer\SSLBridging").CurrentValue
+ # Convert the integer value to its representative string
+ $current_ssl_bridging_str = $ssl_bridging_methods[$current_ssl_bridging]
+
+ if ($current_ssl_bridging_str -ne $ssl_bridging) {
+ Set-Item -LiteralPath "RDS:\GatewayServer\SSLBridging" -Value ([array]::IndexOf($ssl_bridging_methods, $ssl_bridging)) -WhatIf:$check_mode
+ $diff_text += "-SSLBridging = $current_ssl_bridging_str`n+SSLBridging = $ssl_bridging`n"
+ $result.changed = $true
+ }
+}
+
+if ($null -ne $enable_only_messaging_capable_clients) {
+ $current_enable_only_messaging_capable_clients = (Get-Item -LiteralPath "RDS:\GatewayServer\EnableOnlyMessagingCapableClients").CurrentValue
+ # Convert the integer value to boolean
+ $current_enable_only_messaging_capable_clients = $current_enable_only_messaging_capable_clients -eq 1
+
+ if ($current_enable_only_messaging_capable_clients -ne $enable_only_messaging_capable_clients) {
+ Set-Item -LiteralPath "RDS:\GatewayServer\EnableOnlyMessagingCapableClients" -Value ([int]$enable_only_messaging_capable_clients) -WhatIf:$check_mode
+ $diff_text += "-EnableOnlyMessagingCapableClients = $current_enable_only_messaging_capable_clients`n"
+ $diff_text += "+EnableOnlyMessagingCapableClients = $enable_only_messaging_capable_clients`n"
+ $result.changed = $true
+ }
+}
+
+if ($diff_mode -and $result.changed -eq $true) {
+ $result.diff = @{
+ prepared = $diff_text
+ }
+}
+
+Exit-Json $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_rds_settings.py b/ansible_collections/community/windows/plugins/modules/win_rds_settings.py
new file mode 100644
index 00000000..e5370b05
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_rds_settings.py
@@ -0,0 +1,57 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Kevin Subileau (@ksubileau)
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_rds_settings
+short_description: Manage main settings of a Remote Desktop Gateway server
+description:
+ - Configure general settings of a Remote Desktop Gateway server.
+author:
+ - Kevin Subileau (@ksubileau)
+options:
+ certificate_hash:
+ description:
+ - Certificate hash (thumbprint) for the Remote Desktop Gateway server. The certificate hash is the unique identifier for the certificate.
+ type: str
+ max_connections:
+ description:
+ - The maximum number of connections allowed.
+ - If set to C(0), no new connections are allowed.
+ - If set to C(-1), the number of connections is unlimited.
+ type: int
+ ssl_bridging:
+ description:
+ - Specifies whether to use SSL Bridging.
+ - 'C(none): no SSL bridging.'
+ - 'C(https_http): HTTPS-HTTP bridging.'
+ - 'C(https_https): HTTPS-HTTPS bridging.'
+ type: str
+ choices: [ https_http, https_https, none ]
+ enable_only_messaging_capable_clients:
+ description:
+ - If enabled, only clients that support logon messages and administrator messages can connect.
+ type: bool
+requirements:
+ - Windows Server 2008R2 (6.1) or higher.
+ - The Windows Feature "RDS-Gateway" must be enabled.
+seealso:
+- module: community.windows.win_rds_cap
+- module: community.windows.win_rds_rap
+- module: community.windows.win_rds_settings
+'''
+
+EXAMPLES = r'''
+- name: Configure the Remote Desktop Gateway
+ community.windows.win_rds_settings:
+ certificate_hash: B0D0FA8408FC67B230338FCA584D03792DA73F4C
+ max_connections: 50
+ notify:
+ - Restart TSGateway service
+'''
+
+RETURN = r'''
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_region.ps1 b/ansible_collections/community/windows/plugins/modules/win_region.ps1
new file mode 100644
index 00000000..90713c12
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_region.ps1
@@ -0,0 +1,444 @@
+#!powershell
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -PowerShell Ansible.ModuleUtils.AddType
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+$spec = @{
+ options = @{
+ location = @{ type = "str" }
+ format = @{ type = "str" }
+ unicode_language = @{ type = "str" }
+ copy_settings = @{ type = "bool"; default = $false }
+ }
+ supports_check_mode = $true
+}
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$check_mode = $module.CheckMode
+
+$location = $module.Params.location
+$format = $module.Params.format
+$unicode_language = $module.Params.unicode_language
+$copy_settings = $module.Params.copy_settings
+
+$module.Result.restart_required = $false
+
+# This is used to get the format values based on the LCType enum based through. When running Vista/7/2008/200R2
+Add-CSharpType -AnsibleModule $module -References @'
+using System;
+using System.Text;
+using System.Runtime.InteropServices;
+using System.ComponentModel;
+
+namespace Ansible.WinRegion {
+
+ public class NativeMethods
+ {
+ [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ public static extern int GetLocaleInfoEx(
+ String lpLocaleName,
+ UInt32 LCType,
+ StringBuilder lpLCData,
+ int cchData);
+
+ [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ public static extern int GetSystemDefaultLocaleName(
+ IntPtr lpLocaleName,
+ int cchLocaleName);
+
+ [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ public static extern int GetUserDefaultLocaleName(
+ IntPtr lpLocaleName,
+ int cchLocaleName);
+
+ [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 = 0x80000003; // HKU
+ 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(); }
+ }
+
+ public class LocaleHelper {
+ private String Locale;
+
+ public LocaleHelper(String locale) {
+ Locale = locale;
+ }
+
+ public String GetValueFromType(UInt32 LCType) {
+ StringBuilder data = new StringBuilder(500);
+ int result = NativeMethods.GetLocaleInfoEx(Locale, LCType, data, 500);
+ if (result == 0)
+ throw new Win32Exception("Error getting locale info with legacy method");
+
+ return data.ToString();
+ }
+ }
+}
+'@
+
+
+Function Get-LastWin32ExceptionMessage {
+ param([int]$ErrorCode)
+ $exp = New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList $ErrorCode
+ $exp_msg = "{0} (Win32 ErrorCode {1} - 0x{1:X8})" -f $exp.Message, $ErrorCode
+ return $exp_msg
+}
+
+Function Get-SystemLocaleName {
+ $max_length = 85 # LOCALE_NAME_MAX_LENGTH
+ $ptr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($max_length)
+
+ try {
+ $res = [Ansible.WinRegion.NativeMethods]::GetSystemDefaultLocaleName($ptr, $max_length)
+
+ if ($res -eq 0) {
+ $err_code = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
+ $msg = Get-LastWin32ExceptionMessage -Error $err_code
+ $module.FailJson("Failed to get system locale: $msg")
+ }
+
+ $system_locale = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($ptr)
+ }
+ finally {
+ [System.Runtime.InteropServices.Marshal]::FreeHGlobal($ptr)
+ }
+
+ return $system_locale
+}
+
+Function Get-UserLocaleName {
+ $max_length = 85 # LOCALE_NAME_MAX_LENGTH
+ $ptr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($max_length)
+
+ try {
+ $res = [Ansible.WinRegion.NativeMethods]::GetUserDefaultLocaleName($ptr, $max_length)
+
+ if ($res -eq 0) {
+ $err_code = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
+ $msg = Get-LastWin32ExceptionMessage -Error $err_code
+ $module.FailJson("Failed to get user locale: $msg")
+ }
+
+ $user_locale = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($ptr)
+ }
+ finally {
+ [System.Runtime.InteropServices.Marshal]::FreeHGlobal($ptr)
+ }
+
+ return $user_locale
+}
+
+Function Get-ValidGeoId($cultures) {
+ $geo_ids = @()
+ foreach ($culture in $cultures) {
+ try {
+ $geo_id = [System.Globalization.RegionInfo]$culture.Name
+ $geo_ids += $geo_id.GeoId
+ }
+ catch {}
+ }
+ $geo_ids
+}
+
+Function Test-RegistryProperty($reg_key, $property) {
+ $type = Get-ItemProperty -LiteralPath $reg_key -Name $property -ErrorAction SilentlyContinue
+ if ($null -eq $type) {
+ $false
+ }
+ else {
+ $true
+ }
+}
+
+Function Copy-RegistryKey($source, $target) {
+ # Using Copy-Item -Recurse is giving me weird results, doing it recursively
+ Copy-Item -LiteralPath $source -Destination $target -WhatIf:$check_mode
+
+ foreach ($key in Get-ChildItem -LiteralPath $source) {
+ $sourceKey = "$source\$($key.PSChildName)"
+ $targetKey = (Get-Item -LiteralPath $source).PSChildName
+ Copy-RegistryKey -source "$sourceKey" -target "$target\$targetKey"
+ }
+}
+
+Function Set-UserLocale($culture) {
+ $reg_key = 'HKCU:\Control Panel\International'
+
+ $lookup = New-Object Ansible.WinRegion.LocaleHelper($culture)
+ # hex values are from http://www.pinvoke.net/default.aspx/kernel32/GetLocaleInfoEx.html
+ $wanted_values = @{
+ Locale = '{0:x8}' -f ([System.Globalization.CultureInfo]$culture).LCID
+ LocaleName = $culture
+ s1159 = $lookup.GetValueFromType(0x00000028)
+ s2359 = $lookup.GetValueFromType(0x00000029)
+ sCountry = $lookup.GetValueFromType(0x00000006)
+ sCurrency = $lookup.GetValueFromType(0x00000014)
+ sDate = $lookup.GetValueFromType(0x0000001D)
+ sDecimal = $lookup.GetValueFromType(0x0000000E)
+ sGrouping = $lookup.GetValueFromType(0x00000010)
+ sLanguage = $lookup.GetValueFromType(0x00000003) # LOCALE_ABBREVLANGNAME
+ sList = $lookup.GetValueFromType(0x0000000C)
+ sLongDate = $lookup.GetValueFromType(0x00000020)
+ sMonDecimalSep = $lookup.GetValueFromType(0x00000016)
+ sMonGrouping = $lookup.GetValueFromType(0x00000018)
+ sMonThousandSep = $lookup.GetValueFromType(0x00000017)
+ sNativeDigits = $lookup.GetValueFromType(0x00000013)
+ sNegativeSign = $lookup.GetValueFromType(0x00000051)
+ sPositiveSign = $lookup.GetValueFromType(0x00000050)
+ sShortDate = $lookup.GetValueFromType(0x0000001F)
+ sThousand = $lookup.GetValueFromType(0x0000000F)
+ sTime = $lookup.GetValueFromType(0x0000001E)
+ sTimeFormat = $lookup.GetValueFromType(0x00001003)
+ sYearMonth = $lookup.GetValueFromType(0x00001006)
+ iCalendarType = $lookup.GetValueFromType(0x00001009)
+ iCountry = $lookup.GetValueFromType(0x00000005)
+ iCurrDigits = $lookup.GetValueFromType(0x00000019)
+ iCurrency = $lookup.GetValueFromType(0x0000001B)
+ iDate = $lookup.GetValueFromType(0x00000021)
+ iDigits = $lookup.GetValueFromType(0x00000011)
+ NumShape = $lookup.GetValueFromType(0x00001014) # LOCALE_IDIGITSUBSTITUTION
+ iFirstDayOfWeek = $lookup.GetValueFromType(0x0000100C)
+ iFirstWeekOfYear = $lookup.GetValueFromType(0x0000100D)
+ iLZero = $lookup.GetValueFromType(0x00000012)
+ iMeasure = $lookup.GetValueFromType(0x0000000D)
+ iNegCurr = $lookup.GetValueFromType(0x0000001C)
+ iNegNumber = $lookup.GetValueFromType(0x00001010)
+ iPaperSize = $lookup.GetValueFromType(0x0000100A)
+ iTime = $lookup.GetValueFromType(0x00000023)
+ iTimePrefix = $lookup.GetValueFromType(0x00001005)
+ iTLZero = $lookup.GetValueFromType(0x00000025)
+ }
+
+ if (Test-RegistryProperty -reg_key $reg_key -property 'sShortTime') {
+ # sShortTime was added after Vista, will check anyway and add in the value if it exists
+ $wanted_values.sShortTime = $lookup.GetValueFromType(0x00000079)
+ }
+
+ $properties = Get-ItemProperty -LiteralPath $reg_key
+ foreach ($property in $properties.PSObject.Properties) {
+ if (Test-RegistryProperty -reg_key $reg_key -property $property.Name) {
+ $name = $property.Name
+ $old_value = $property.Value
+ $new_value = $wanted_values.$name
+
+ if ($new_value -ne $old_value) {
+ Set-ItemProperty -LiteralPath $reg_key -Name $name -Value $new_value -WhatIf:$check_mode
+ $module.Result.changed = $true
+ }
+ }
+ }
+}
+
+Function Set-SystemLocaleLegacy($unicode_language) {
+ # For when Get/Set-WinSystemLocale is not available (Pre Windows 8 and Server 2012)
+ $current_language_value = (Get-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Nls\Language').Default
+ $wanted_language_value = '{0:x4}' -f ([System.Globalization.CultureInfo]$unicode_language).LCID
+ if ($current_language_value -ne $wanted_language_value) {
+ Set-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Nls\Language' -Name 'Default' -Value $wanted_language_value -WhatIf:$check_mode
+ $module.Result.changed = $true
+ $module.Result.restart_required = $true
+ }
+
+ # This reads from the non registry (Default) key, the extra prop called (Default) see below for more details
+ $current_locale_value = (Get-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Nls\Locale')."(Default)"
+ $wanted_locale_value = '{0:x8}' -f ([System.Globalization.CultureInfo]$unicode_language).LCID
+ if ($current_locale_value -ne $wanted_locale_value) {
+ # Need to use .net to write property value, Locale has 2 (Default) properties
+ # 1: The actual (Default) property, we don't want to change Set-ItemProperty writes to this value when using (Default)
+ # 2: A property called (Default), this is what we want to change and only .net SetValue can do this one
+ if (-not $check_mode) {
+ $hive = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey("LocalMachine", $env:COMPUTERNAME)
+ $key = $hive.OpenSubKey("SYSTEM\CurrentControlSet\Control\Nls\Locale", $true)
+ $key.SetValue("(Default)", $wanted_locale_value, [Microsoft.Win32.RegistryValueKind]::String)
+ }
+ $module.Result.changed = $true
+ $module.Result.restart_required = $true
+ }
+
+ $codepage_path = 'HKLM:\SYSTEM\CurrentControlSet\Control\Nls\CodePage'
+ $current_codepage_info = Get-ItemProperty -LiteralPath $codepage_path
+ $wanted_codepage_info = ([System.Globalization.CultureInfo]::GetCultureInfo($unicode_language)).TextInfo
+
+ $current_a_cp = $current_codepage_info.ACP
+ $current_oem_cp = $current_codepage_info.OEMCP
+ $current_mac_cp = $current_codepage_info.MACCP
+ $wanted_a_cp = $wanted_codepage_info.ANSICodePage
+ $wanted_oem_cp = $wanted_codepage_info.OEMCodePage
+ $wanted_mac_cp = $wanted_codepage_info.MacCodePage
+
+ if ($current_a_cp -ne $wanted_a_cp) {
+ Set-ItemProperty -LiteralPath $codepage_path -Name 'ACP' -Value $wanted_a_cp -WhatIf:$check_mode
+ $module.Result.changed = $true
+ $module.Result.restart_required = $true
+ }
+ if ($current_oem_cp -ne $wanted_oem_cp) {
+ Set-ItemProperty -LiteralPath $codepage_path -Name 'OEMCP' -Value $wanted_oem_cp -WhatIf:$check_mode
+ $module.Result.changed = $true
+ $module.Result.restart_required = $true
+ }
+ if ($current_mac_cp -ne $wanted_mac_cp) {
+ Set-ItemProperty -LiteralPath $codepage_path -Name 'MACCP' -Value $wanted_mac_cp -WhatIf:$check_mode
+ $module.Result.changed = $true
+ $module.Result.restart_required = $true
+ }
+}
+
+if ($null -eq $format -and $null -eq $location -and $null -eq $unicode_language) {
+ $module.FailJson("An argument for 'format', 'location' or 'unicode_language' needs to be supplied")
+}
+else {
+ $valid_cultures = [System.Globalization.CultureInfo]::GetCultures('AllCultures')
+ $valid_geoids = Get-ValidGeoId -cultures $valid_cultures
+
+ if ($null -ne $location) {
+ if ($valid_geoids -notcontains $location) {
+ $module.FailJson("The argument location '$location' does not contain a valid Geo ID")
+ }
+ }
+
+ if ($null -ne $format) {
+ if ($valid_cultures.Name -notcontains $format) {
+ $module.FailJson("The argument format '$format' does not contain a valid Culture Name")
+ }
+ }
+
+ if ($null -ne $unicode_language) {
+ if ($valid_cultures.Name -notcontains $unicode_language) {
+ $module.FailJson("The argument unicode_language '$unicode_language' does not contain a valid Culture Name")
+ }
+ }
+}
+
+if ($null -ne $location) {
+ # Get-WinHomeLocation was only added in Server 2012 and above
+ # Use legacy option if older
+ if (Get-Command 'Get-WinHomeLocation' -ErrorAction SilentlyContinue) {
+ $current_location = (Get-WinHomeLocation).GeoId
+ if ($current_location -ne $location) {
+ if (-not $check_mode) {
+ Set-WinHomeLocation -GeoId $location
+ }
+ $module.Result.changed = $true
+ }
+ }
+ else {
+ $current_location = (Get-ItemProperty -LiteralPath 'HKCU:\Control Panel\International\Geo').Nation
+ if ($current_location -ne $location) {
+ Set-ItemProperty -LiteralPath 'HKCU:\Control Panel\International\Geo' -Name 'Nation' -Value $location -WhatIf:$check_mode
+ $module.Result.changed = $true
+ }
+ }
+}
+
+if ($null -ne $format) {
+ # Cannot use Get/Set-Culture as that fails to get and set the culture when running in the PSRP runspace.
+ $current_format = Get-UserLocaleName
+ if ($current_format -ne $format) {
+ Set-UserLocale -culture $format
+ $module.Result.changed = $true
+ }
+}
+
+if ($null -ne $unicode_language) {
+ # Get/Set-WinSystemLocale was only added in Server 2012 and above, use legacy option if older
+ if (Get-Command 'Get-WinSystemLocale' -ErrorAction SilentlyContinue) {
+ $current_unicode_language = Get-SystemLocaleName
+ if ($current_unicode_language -ne $unicode_language) {
+ if (-not $check_mode) {
+ Set-WinSystemLocale -SystemLocale $unicode_language
+ }
+ $module.Result.changed = $true
+ $module.Result.restart_required = $true
+ }
+ }
+ else {
+ Set-SystemLocaleLegacy -unicode_language $unicode_language
+ }
+}
+
+if ($copy_settings -eq $true -and $module.Result.changed -eq $true) {
+ if (-not $check_mode) {
+ $null = New-PSDrive -Name HKU -PSProvider Registry -Root Registry::HKEY_USERS
+
+ if (Test-Path -LiteralPath HKU:\ANSIBLE) {
+ $module.Warn("hive already loaded at HKU:\ANSIBLE, had to unload hive for win_region to continue")
+ [Ansible.WinRegion.Hive]::UnloadHive("ANSIBLE")
+ }
+
+ $loaded_hive = New-Object -TypeName Ansible.WinRegion.Hive -ArgumentList "ANSIBLE", 'C:\Users\Default\NTUSER.DAT'
+ try {
+ $sids = 'ANSIBLE', '.DEFAULT', 'S-1-5-19', 'S-1-5-20'
+ foreach ($sid in $sids) {
+ Copy-RegistryKey -source "HKCU:\Keyboard Layout" -target "HKU:\$sid"
+ Copy-RegistryKey -source "HKCU:\Control Panel\International" -target "HKU:\$sid\Control Panel"
+ Copy-RegistryKey -source "HKCU:\Control Panel\Input Method" -target "HKU:\$sid\Control Panel"
+ }
+ }
+ finally {
+ $loaded_hive.Dispose()
+ }
+
+ Remove-PSDrive HKU
+ }
+ $module.Result.changed = $true
+}
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_region.py b/ansible_collections/community/windows/plugins/modules/win_region.py
new file mode 100644
index 00000000..1c6200d8
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_region.py
@@ -0,0 +1,95 @@
+#!/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)
+
+DOCUMENTATION = r'''
+module: win_region
+short_description: Set the region and format settings
+description:
+ - Set the location settings of a Windows Server.
+ - Set the format settings of a Windows Server.
+ - Set the unicode language settings of a Windows Server.
+ - Copy across these settings to the default profile.
+options:
+ location:
+ description:
+ - The location to set for the current user, see
+ U(https://msdn.microsoft.com/en-us/library/dd374073.aspx)
+ for a list of GeoIDs you can use and what location it relates to.
+ - This needs to be set if C(format) or C(unicode_language) is not
+ set.
+ type: str
+ format:
+ description:
+ - The language format to set for the current user, see
+ U(https://msdn.microsoft.com/en-us/library/system.globalization.cultureinfo.aspx)
+ for a list of culture names to use.
+ - This needs to be set if C(location) or C(unicode_language) is not set.
+ type: str
+ unicode_language:
+ description:
+ - The unicode language format to set for all users, see
+ U(https://msdn.microsoft.com/en-us/library/system.globalization.cultureinfo.aspx)
+ for a list of culture names to use.
+ - This needs to be set if C(location) or C(format) is not set. After setting this
+ value a reboot is required for it to take effect.
+ type: str
+ copy_settings:
+ description:
+ - This will copy the current format and location values to new user
+ profiles and the welcome screen. This will only run if
+ C(location), C(format) or C(unicode_language) has resulted in a
+ change. If this process runs then it will always result in a
+ change.
+ type: bool
+ default: no
+seealso:
+- module: community.windows.win_timezone
+author:
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = r'''
+- name: Set the region format to English United States
+ community.windows.win_region:
+ format: en-US
+
+- name: Set the region format to English Australia and copy settings to new profiles
+ community.windows.win_region:
+ format: en-AU
+ copy_settings: yes
+
+- name: Set the location to United States
+ community.windows.win_region:
+ location: 244
+
+# Reboot when region settings change
+- name: Set the unicode language to English Great Britain, reboot if required
+ community.windows.win_region:
+ unicode_language: en-GB
+ register: result
+
+- ansible.windows.win_reboot:
+ when: result.restart_required
+
+# Reboot when format, location or unicode has changed
+- name: Set format, location and unicode to English Australia and copy settings, reboot if required
+ community.windows.win_region:
+ location: 12
+ format: en-AU
+ unicode_language: en-AU
+ register: result
+
+- ansible.windows.win_reboot:
+ when: result.restart_required
+'''
+
+RETURN = r'''
+restart_required:
+ description: Whether a reboot is required for the change to take effect.
+ returned: success
+ type: bool
+ sample: true
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_regmerge.ps1 b/ansible_collections/community/windows/plugins/modules/win_regmerge.ps1
new file mode 100644
index 00000000..8d511cc2
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_regmerge.ps1
@@ -0,0 +1,99 @@
+#!powershell
+
+# Copyright: (c) 2015, Jon Hawkesworth (@jhawkesworth) <jhawkesworth@protonmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.ArgvParser
+#Requires -Module Ansible.ModuleUtils.CommandUtil
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+Function Convert-RegistryPath {
+ Param (
+ [parameter(Mandatory = $True)]
+ [ValidateNotNullOrEmpty()]$Path
+ )
+
+ $output = $Path -replace "HKLM:", "HKLM"
+ $output = $output -replace "HKCU:", "HKCU"
+
+ Return $output
+}
+
+$result = @{
+ changed = $false
+}
+$params = Parse-Args $args
+
+$path = Get-AnsibleParam -obj $params -name "path" -type "path" -failifempty $true -resultobj $result
+$compare_to = Get-AnsibleParam -obj $params -name "compare_to" -type "str" -resultobj $result
+
+# check it looks like a reg key, warn if key not present - will happen first time
+# only accepting PS-Drive style key names (starting with HKLM etc, not HKEY_LOCAL_MACHINE etc)
+
+$do_comparison = $False
+
+If ($compare_to) {
+ $compare_to_key = $params.compare_to.ToString()
+ If (Test-Path $compare_to_key -pathType container ) {
+ $do_comparison = $True
+ }
+ Else {
+ $result.compare_to_key_found = $false
+ }
+}
+
+If ( $do_comparison -eq $True ) {
+ $guid = [guid]::NewGuid()
+ $exported_path = $env:TEMP + "\" + $guid.ToString() + 'ansible_win_regmerge.reg'
+
+ $expanded_compare_key = Convert-RegistryPath ($compare_to_key)
+
+ # export from the reg key location to a file
+ $reg_args = Argv-ToString -Arguments @("reg.exe", "EXPORT", $expanded_compare_key, $exported_path)
+ $res = Run-Command -command $reg_args
+ if ($res.rc -ne 0) {
+ $result.rc = $res.rc
+ $result.stdout = $res.stdout
+ $result.stderr = $res.stderr
+ Fail-Json -obj $result -message "error exporting registry '$expanded_compare_key' to '$exported_path'"
+ }
+
+ # compare the two files
+ $comparison_result = Compare-Object -ReferenceObject $(Get-Content $path) -DifferenceObject $(Get-Content $exported_path)
+
+ If ($null -ne $comparison_result -and (Get-Member -InputObject $comparison_result -Name "count" -MemberType Properties )) {
+ # Something is different, actually do reg merge
+ $reg_import_args = Argv-ToString -Arguments @("reg.exe", "IMPORT", $path)
+ $res = Run-Command -command $reg_import_args
+ if ($res.rc -ne 0) {
+ $result.rc = $res.rc
+ $result.stdout = $res.stdout
+ $result.stderr = $res.stderr
+ Fail-Json -obj $result -message "error importing registry values from '$path'"
+ }
+ $result.changed = $true
+ $result.difference_count = $comparison_result.count
+ }
+ Else {
+ $result.difference_count = 0
+ }
+
+ Remove-Item $exported_path
+ $result.compared = $true
+
+}
+Else {
+ # not comparing, merge and report changed
+ $reg_import_args = Argv-ToString -Arguments @("reg.exe", "IMPORT", $path)
+ $res = Run-Command -command $reg_import_args
+ if ($res.rc -ne 0) {
+ $result.rc = $res.rc
+ $result.stdout = $res.stdout
+ $result.stderr = $res.stderr
+ Fail-Json -obj $result -message "error importing registry value from '$path'"
+ }
+ $result.changed = $true
+ $result.compared = $false
+}
+
+Exit-Json $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_regmerge.py b/ansible_collections/community/windows/plugins/modules/win_regmerge.py
new file mode 100644
index 00000000..073f54c9
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_regmerge.py
@@ -0,0 +1,74 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2015, Jon Hawkesworth (@jhawkesworth) <jhawkesworth@protonmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_regmerge
+short_description: Merges the contents of a registry file into the Windows registry
+description:
+ - Wraps the reg.exe command to import the contents of a registry file.
+ - Suitable for use with registry files created using M(ansible.windows.win_template).
+ - Windows registry files have a specific format and must be constructed correctly with carriage return and line feed line endings otherwise they will not
+ be merged.
+ - Exported registry files often start with a Byte Order Mark which must be removed if the file is to templated using M(ansible.windows.win_template).
+ - Registry file format is described at U(https://support.microsoft.com/en-us/kb/310516)
+ - See also M(ansible.windows.win_template), M(ansible.windows.win_regedit)
+options:
+ path:
+ description:
+ - The full path including file name to the registry file on the remote machine to be merged
+ type: path
+ required: yes
+ compare_key:
+ description:
+ - The parent key to use when comparing the contents of the registry to the contents of the file. Needs to be in HKLM or HKCU part of registry.
+ Use a PS-Drive style path for example HKLM:\SOFTWARE not HKEY_LOCAL_MACHINE\SOFTWARE
+ If not supplied, or the registry key is not found, no comparison will be made, and the module will report changed.
+ type: str
+notes:
+ - Organise your registry files so that they contain a single root registry
+ key if you want to use the compare_to functionality.
+ - This module does not force registry settings to be in the state
+ described in the file. If registry settings have been modified externally
+ the module will merge the contents of the file but continue to report
+ differences on subsequent runs.
+ - To force registry change, use M(ansible.windows.win_regedit) with C(state=absent) before
+ using C(community.windows.win_regmerge).
+seealso:
+- module: ansible.windows.win_reg_stat
+- module: ansible.windows.win_regedit
+author:
+- Jon Hawkesworth (@jhawkesworth)
+'''
+
+EXAMPLES = r'''
+- name: Merge in a registry file without comparing to current registry
+ community.windows.win_regmerge:
+ path: C:\autodeploy\myCompany-settings.reg
+
+- name: Compare and merge registry file
+ community.windows.win_regmerge:
+ path: C:\autodeploy\myCompany-settings.reg
+ compare_to: HKLM:\SOFTWARE\myCompany
+'''
+
+RETURN = r'''
+compare_to_key_found:
+ description: whether the parent registry key has been found for comparison
+ returned: when comparison key not found in registry
+ type: bool
+ sample: false
+difference_count:
+ description: number of differences between the registry and the file
+ returned: changed
+ type: int
+ sample: 1
+compared:
+ description: whether a comparison has taken place between the registry and the file
+ returned: when a comparison key has been supplied and comparison has been attempted
+ type: bool
+ sample: true
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_robocopy.ps1 b/ansible_collections/community/windows/plugins/modules/win_robocopy.ps1
new file mode 100644
index 00000000..2d3e0580
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_robocopy.ps1
@@ -0,0 +1,148 @@
+#!powershell
+
+# Copyright: (c) 2015, Corwin Brown <corwin.brown@maxpoint.com>
+# 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
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
+
+$src = Get-AnsibleParam -obj $params -name "src" -type "path" -failifempty $true
+$dest = Get-AnsibleParam -obj $params -name "dest" -type "path" -failifempty $true
+$purge = Get-AnsibleParam -obj $params -name "purge" -type "bool" -default $false
+$recurse = Get-AnsibleParam -obj $params -name "recurse" -type "bool" -default $false
+$flags = Get-AnsibleParam -obj $params -name "flags" -type "str"
+
+$result = @{
+ changed = $false
+ dest = $dest
+ purge = $purge
+ recurse = $recurse
+ src = $src
+}
+
+# Search for an Error Message
+# Robocopy seems to display an error after 3 '-----' separator lines
+Function SearchForError($cmd_output, $default_msg) {
+ $separator_count = 0
+ $error_msg = $default_msg
+ ForEach ($line in $cmd_output) {
+ if (-not $line) {
+ continue
+ }
+
+ if ($separator_count -ne 3) {
+ if (Select-String -InputObject $line -pattern "^(\s+)?(\-+)(\s+)?$") {
+ $separator_count += 1
+ }
+ }
+ else {
+ if (Select-String -InputObject $line -pattern "error") {
+ $error_msg = $line
+ break
+ }
+ }
+ }
+
+ return $error_msg
+}
+
+if (-not (Test-Path -Path $src)) {
+ Fail-Json $result "$src does not exist!"
+}
+
+# Build Arguments
+$robocopy_opts = @($src, $dest)
+
+if ($check_mode) {
+ $robocopy_opts += "/l"
+}
+
+if ($null -eq $flags) {
+ if ($purge) {
+ $robocopy_opts += "/purge"
+ }
+
+ if ($recurse) {
+ $robocopy_opts += "/e"
+ }
+}
+else {
+ ForEach ($f in $flags.split(" ")) {
+ $robocopy_opts += $f
+ }
+}
+
+$result.flags = $flags
+$result.cmd = "$robocopy $robocopy_opts"
+
+Try {
+ $robocopy_output = &robocopy $robocopy_opts
+ $rc = $LASTEXITCODE
+}
+Catch {
+ Fail-Json $result "Error synchronizing $src to $dest! Msg: $($_.Exception.Message)"
+}
+
+$result.msg = "Success"
+$result.output = $robocopy_output
+$result.return_code = $rc # Backward compatibility
+$result.rc = $rc
+
+switch ($rc) {
+
+ 0 {
+ $result.msg = "No files copied."
+ }
+ 1 {
+ $result.msg = "Files copied successfully!"
+ $result.changed = $true
+ $result.failed = $false
+ }
+ 2 {
+ $result.msg = "Some Extra files or directories were detected. No files were copied."
+ Add-Warning $result $result.msg
+ $result.failed = $false
+ }
+ 3 {
+ $result.msg = "(2+1) Some files were copied. Additional files were present."
+ Add-Warning $result $result.msg
+ $result.changed = $true
+ $result.failed = $false
+ }
+ 4 {
+ $result.msg = "Some mismatched files or directories were detected. Housekeeping might be required!"
+ Add-Warning $result $result.msg
+ $result.changed = $true
+ $result.failed = $false
+ }
+ 5 {
+ $result.msg = "(4+1) Some files were copied. Some files were mismatched."
+ Add-Warning $result $result.msg
+ $result.changed = $true
+ $result.failed = $false
+ }
+ 6 {
+ $result.msg = "(4+2) Additional files and mismatched files exist. No files were copied."
+ $result.failed = $false
+ }
+ 7 {
+ $result.msg = "(4+1+2) Files were copied, a file mismatch was present, and additional files were present."
+ Add-Warning $result $result.msg
+ $result.changed = $true
+ $result.failed = $false
+ }
+ 8 {
+ Fail-Json $result (SearchForError $robocopy_output "Some files or directories could not be copied!")
+ }
+ { @(9, 10, 11, 12, 13, 14, 15) -contains $_ } {
+ Fail-Json $result (SearchForError $robocopy_output "Fatal error. Check log message!")
+ }
+ 16 {
+ Fail-Json $result (SearchForError $robocopy_output "Serious Error! No files were copied! Do you have permissions to access $src and $dest?")
+ }
+
+}
+
+Exit-Json $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_robocopy.py b/ansible_collections/community/windows/plugins/modules/win_robocopy.py
new file mode 100644
index 00000000..21a91875
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_robocopy.py
@@ -0,0 +1,139 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2015, Corwin Brown <blakfeld@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_robocopy
+short_description: Synchronizes the contents of two directories using Robocopy
+description:
+- Synchronizes the contents of files/directories from a source to destination.
+- Under the hood this just calls out to RoboCopy, since that should be available
+ on most modern Windows systems.
+options:
+ src:
+ description:
+ - Source file/directory to sync.
+ type: path
+ required: yes
+ dest:
+ description:
+ - Destination file/directory to sync (Will receive contents of src).
+ type: path
+ required: yes
+ recurse:
+ description:
+ - Includes all subdirectories (Toggles the C(/e) flag to RoboCopy).
+ - If C(flags) is set, this will be ignored.
+ type: bool
+ default: no
+ purge:
+ description:
+ - Deletes any files/directories found in the destination that do not exist in the source.
+ - Toggles the C(/purge) flag to RoboCopy.
+ - If C(flags) is set, this will be ignored.
+ type: bool
+ default: no
+ flags:
+ description:
+ - Directly supply Robocopy flags.
+ - If set, C(purge) and C(recurse) will be ignored.
+ type: str
+notes:
+- This is not a complete port of the M(ansible.posix.synchronize) module. Unlike the M(ansible.posix.synchronize)
+ module this only performs the sync/copy on the remote machine, not from the Ansible controller to the remote machine.
+- This module does not currently support all Robocopy flags.
+seealso:
+- module: ansible.posix.synchronize
+- module: ansible.windows.win_copy
+author:
+- Corwin Brown (@blakfeld)
+'''
+
+EXAMPLES = r'''
+- name: Sync the contents of one directory to another
+ community.windows.win_robocopy:
+ src: C:\DirectoryOne
+ dest: C:\DirectoryTwo
+
+- name: Sync the contents of one directory to another, including subdirectories
+ community.windows.win_robocopy:
+ src: C:\DirectoryOne
+ dest: C:\DirectoryTwo
+ recurse: yes
+
+- name: Sync the contents of one directory to another, and remove any files/directories found in destination that do not exist in the source
+ community.windows.win_robocopy:
+ src: C:\DirectoryOne
+ dest: C:\DirectoryTwo
+ purge: yes
+
+- name: Sync content in recursive mode, removing any files/directories found in destination that do not exist in the source
+ community.windows.win_robocopy:
+ src: C:\DirectoryOne
+ dest: C:\DirectoryTwo
+ recurse: yes
+ purge: yes
+
+- name: Sync two directories in recursive and purging mode, specifying additional special flags
+ community.windows.win_robocopy:
+ src: C:\DirectoryOne
+ dest: C:\DirectoryTwo
+ flags: /E /PURGE /XD SOME_DIR /XF SOME_FILE /MT:32
+
+- name: Sync one file from a remote UNC path in recursive and purging mode, specifying additional special flags
+ community.windows.win_robocopy:
+ src: \\Server1\Directory One
+ dest: C:\DirectoryTwo
+ flags: file.zip /E /PURGE /XD SOME_DIR /XF SOME_FILE /MT:32
+'''
+
+RETURN = r'''
+cmd:
+ description: The used command line.
+ returned: always
+ type: str
+ sample: robocopy C:\DirectoryOne C:\DirectoryTwo /e /purge
+src:
+ description: The Source file/directory of the sync.
+ returned: always
+ type: str
+ sample: C:\Some\Path
+dest:
+ description: The Destination file/directory of the sync.
+ returned: always
+ type: str
+ sample: C:\Some\Path
+recurse:
+ description: Whether or not the recurse flag was toggled.
+ returned: always
+ type: bool
+ sample: false
+purge:
+ description: Whether or not the purge flag was toggled.
+ returned: always
+ type: bool
+ sample: false
+flags:
+ description: Any flags passed in by the user.
+ returned: always
+ type: str
+ sample: /e /purge
+rc:
+ description: The return code returned by robocopy.
+ returned: success
+ type: int
+ sample: 1
+output:
+ description: The output of running the robocopy command.
+ returned: success
+ type: str
+ sample: "------------------------------------\\n ROBOCOPY :: Robust File Copy for Windows \\n------------------------------------\\n "
+msg:
+ description: Output interpreted into a concise message.
+ returned: always
+ type: str
+ sample: No files copied!
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_route.ps1 b/ansible_collections/community/windows/plugins/modules/win_route.ps1
new file mode 100644
index 00000000..58129fa4
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_route.ps1
@@ -0,0 +1,112 @@
+#!powershell
+
+# Copyright: (c) 2016, Daniele Lazzari <lazzari@mailup.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+# win_route (Add or remove a network static route)
+
+$params = Parse-Args $args -supports_check_mode $true
+
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -default $false
+$dest = Get-AnsibleParam -obj $params -name "destination" -type "str" -failifempty $true
+$gateway = Get-AnsibleParam -obj $params -name "gateway" -type "str"
+$metric = Get-AnsibleParam -obj $params -name "metric" -type "int" -default 1
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateSet "present", "absent"
+$result = @{
+ "changed" = $false
+ "output" = ""
+}
+
+Function Add-Route {
+ Param (
+ [Parameter(Mandatory = $true)]
+ [string]$Destination,
+ [Parameter(Mandatory = $true)]
+ [string]$Gateway,
+ [Parameter(Mandatory = $true)]
+ [int]$Metric,
+ [Parameter(Mandatory = $true)]
+ [bool]$CheckMode
+ )
+
+
+ $IpAddress = $Destination.split('/')[0]
+
+ # Check if the static route is already present
+ $Route = Get-CimInstance win32_ip4PersistedrouteTable -Filter "Destination = '$($IpAddress)'"
+ if (!($Route)) {
+ try {
+ # Find Interface Index
+ $InterfaceIndex = Find-NetRoute -RemoteIPAddress $Gateway | Select-Object -First 1 -ExpandProperty InterfaceIndex
+
+ # Add network route
+ $routeParams = @{
+ DestinationPrefix = $Destination
+ NextHop = $Gateway
+ InterfaceIndex = $InterfaceIndex
+ RouteMetric = $Metric
+ ErrorAction = "Stop"
+ WhatIf = $CheckMode
+ }
+ New-NetRoute @routeParams | Out-Null
+ $result.changed = $true
+ $result.output = "Route added"
+
+ }
+ catch {
+ $ErrorMessage = $_.Exception.Message
+ Fail-Json $result $ErrorMessage
+ }
+ }
+ else {
+ $result.output = "Static route already exists"
+ }
+
+}
+
+Function Remove-Route {
+ Param (
+ [Parameter(Mandatory = $true)]
+ [string]$Destination,
+ [bool]$CheckMode
+ )
+ $IpAddress = $Destination.split('/')[0]
+ $Route = Get-CimInstance win32_ip4PersistedrouteTable -Filter "Destination = '$($IpAddress)'"
+ if ($Route) {
+ try {
+
+ Remove-NetRoute -DestinationPrefix $Destination -Confirm:$false -ErrorAction Stop -WhatIf:$CheckMode
+ $result.changed = $true
+ $result.output = "Route removed"
+ }
+ catch {
+ $ErrorMessage = $_.Exception.Message
+ Fail-Json $result $ErrorMessage
+ }
+ }
+ else {
+ $result.output = "No route to remove"
+ }
+
+}
+
+# Set gateway if null
+if (!($gateway)) {
+ $gateway = "0.0.0.0"
+}
+
+
+if ($state -eq "present") {
+
+ Add-Route -Destination $dest -Gateway $gateway -Metric $metric -CheckMode $check_mode
+
+}
+else {
+
+ Remove-Route -Destination $dest -CheckMode $check_mode
+
+}
+
+Exit-Json $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_route.py b/ansible_collections/community/windows/plugins/modules/win_route.py
new file mode 100644
index 00000000..655ec11e
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_route.py
@@ -0,0 +1,62 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2017, Daniele Lazzari <lazzari@mailup.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_route
+short_description: Add or remove a static route
+description:
+ - Add or remove a static route.
+options:
+ destination:
+ description:
+ - Destination IP address in CIDR format (ip address/prefix length).
+ type: str
+ required: yes
+ gateway:
+ description:
+ - The gateway used by the static route.
+ - If C(gateway) is not provided it will be set to C(0.0.0.0).
+ type: str
+ metric:
+ description:
+ - Metric used by the static route.
+ type: int
+ default: 1
+ state:
+ description:
+ - If C(absent), it removes a network static route.
+ - If C(present), it adds a network static route.
+ type: str
+ choices: [ absent, present ]
+ default: present
+notes:
+ - Works only with Windows 2012 R2 and newer.
+author:
+- Daniele Lazzari (@dlazz)
+'''
+
+EXAMPLES = r'''
+---
+- name: Add a network static route
+ community.windows.win_route:
+ destination: 192.168.2.10/32
+ gateway: 192.168.1.1
+ metric: 1
+ state: present
+
+- name: Remove a network static route
+ community.windows.win_route:
+ destination: 192.168.2.10/32
+ state: absent
+'''
+RETURN = r'''
+output:
+ description: A message describing the task result.
+ returned: always
+ type: str
+ sample: "Route added"
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_say.ps1 b/ansible_collections/community/windows/plugins/modules/win_say.ps1
new file mode 100644
index 00000000..4f66430e
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_say.ps1
@@ -0,0 +1,108 @@
+#!powershell
+
+# Copyright: (c) 2016, Jon Hawkesworth (@jhawkesworth) <jhawkesworth@protonmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+$spec = @{
+ options = @{
+ msg = @{ type = "str" }
+ msg_file = @{ type = "path" }
+ start_sound_path = @{ type = "path" }
+ end_sound_path = @{ type = "path" }
+ voice = @{ type = "str" }
+ speech_speed = @{ type = "int"; default = 0 }
+ }
+ mutually_exclusive = @(
+ , @('msg', 'msg_file')
+ )
+ required_one_of = @(
+ , @('msg', 'msg_file', 'start_sound_path', 'end_sound_path')
+ )
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+
+$msg = $module.Params.msg
+$msg_file = $module.Params.msg_file
+$start_sound_path = $module.Params.start_sound_path
+$end_sound_path = $module.Params.end_sound_path
+$voice = $module.Params.voice
+$speech_speed = $module.Params.speech_speed
+
+if ($speech_speed -lt -10 -or $speech_speed -gt 10) {
+ $module.FailJson("speech_speed needs to be an integer in the range -10 to 10. The value $speech_speed is outside this range.")
+}
+
+$words = $null
+
+if ($msg_file) {
+ if (-not (Test-Path -LiteralPath $msg_file)) {
+ $msg = -join @(
+ "Message file $msg_file could not be found or opened. "
+ "Ensure you have specified the full path to the file, and the ansible windows user has permission to read the file."
+ )
+ $module.FailJson($msg)
+ }
+ $words = Get-Content -LiteralPath $msg_file | Out-String
+}
+
+if ($msg) {
+ $words = $msg
+}
+
+if ($start_sound_path) {
+ if (-not (Test-Path -LiteralPath $start_sound_path)) {
+ $msg = -join @(
+ "Start sound file $start_sound_path could not be found or opened. "
+ "Ensure you have specified the full path to the file, and the ansible windows user has permission to read the file."
+ )
+ $module.FailJson($msg)
+ }
+ if (-not $module.CheckMode) {
+ (new-object Media.SoundPlayer $start_sound_path).playSync()
+ }
+}
+
+if ($words) {
+ Add-Type -AssemblyName System.speech
+ $tts = New-Object System.Speech.Synthesis.SpeechSynthesizer
+ if ($voice) {
+ try {
+ $tts.SelectVoice($voice)
+ }
+ catch [System.Management.Automation.MethodInvocationException] {
+ $module.Result.voice_info = "Could not load voice '$voice', using system default voice."
+ $module.Warn("Could not load voice '$voice', using system default voice.")
+ }
+ }
+
+ $module.Result.voice = $tts.Voice.Name
+ if ($speech_speed -ne 0) {
+ $tts.Rate = $speech_speed
+ }
+ if (-not $module.CheckMode) {
+ $tts.Speak($words)
+ }
+ $tts.Dispose()
+}
+
+if ($end_sound_path) {
+ if (-not (Test-Path -LiteralPath $end_sound_path)) {
+ $msg = -join @(
+ "End sound file $start_sound_path could not be found or opened. "
+ "Ensure you have specified the full path to the file, and the ansible windows user has permission to read the file."
+ )
+ $module.FailJson($msg)
+ }
+ if (-not $module.CheckMode) {
+ (new-object Media.SoundPlayer $end_sound_path).playSync()
+ }
+}
+
+$module.Result.message_text = $words.ToString()
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_say.py b/ansible_collections/community/windows/plugins/modules/win_say.py
new file mode 100644
index 00000000..6ee107b7
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_say.py
@@ -0,0 +1,108 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2016, Jon Hawkesworth (@jhawkesworth) <jhawkesworth@protonmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_say
+short_description: Text to speech module for Windows to speak messages and optionally play sounds
+description:
+ - Uses .NET libraries to convert text to speech and optionally play .wav sounds. Audio Service needs to be running and some kind of speakers or
+ headphones need to be attached to the windows target(s) for the speech to be audible.
+options:
+ msg:
+ description:
+ - The text to be spoken.
+ - Use either C(msg) or C(msg_file).
+ - Optional so that you can use this module just to play sounds.
+ type: str
+ msg_file:
+ description:
+ - Full path to a windows format text file containing the text to be spoken.
+ - Use either C(msg) or C(msg_file).
+ - Optional so that you can use this module just to play sounds.
+ type: path
+ voice:
+ description:
+ - Which voice to use. See notes for how to discover installed voices.
+ - If the requested voice is not available the default voice will be used.
+ Example voice names from Windows 10 are C(Microsoft Zira Desktop) and C(Microsoft Hazel Desktop).
+ type: str
+ speech_speed:
+ description:
+ - How fast or slow to speak the text.
+ - Must be an integer value in the range -10 to 10.
+ - -10 is slowest, 10 is fastest.
+ type: int
+ default: 0
+ start_sound_path:
+ description:
+ - Full path to a C(.wav) file containing a sound to play before the text is spoken.
+ - Useful on conference calls to alert other speakers that ansible has something to say.
+ type: path
+ end_sound_path:
+ description:
+ - Full path to a C(.wav) file containing a sound to play after the text has been spoken.
+ - Useful on conference calls to alert other speakers that ansible has finished speaking.
+ type: path
+notes:
+ - Needs speakers or headphones to do anything useful.
+ - |
+ To find which voices are installed, run the following Powershell commands.
+
+ Add-Type -AssemblyName System.Speech
+ $speech = New-Object -TypeName System.Speech.Synthesis.SpeechSynthesizer
+ $speech.GetInstalledVoices() | ForEach-Object { $_.VoiceInfo }
+ $speech.Dispose()
+
+ - Speech can be surprisingly slow, so it's best to keep message text short.
+seealso:
+- module: community.windows.win_msg
+- module: community.windows.win_toast
+author:
+- Jon Hawkesworth (@jhawkesworth)
+'''
+
+EXAMPLES = r'''
+- name: Warn of impending deployment
+ community.windows.win_say:
+ msg: Warning, deployment commencing in 5 minutes, please log out.
+
+- name: Using a different voice and a start sound
+ community.windows.win_say:
+ start_sound_path: C:\Windows\Media\ding.wav
+ msg: Warning, deployment commencing in 5 minutes, please log out.
+ voice: Microsoft Hazel Desktop
+
+- name: With start and end sound
+ community.windows.win_say:
+ start_sound_path: C:\Windows\Media\Windows Balloon.wav
+ msg: New software installed
+ end_sound_path: C:\Windows\Media\chimes.wav
+
+- name: Text from file example
+ community.windows.win_say:
+ start_sound_path: C:\Windows\Media\Windows Balloon.wav
+ msg_file: AppData\Local\Temp\morning_report.txt
+ end_sound_path: C:\Windows\Media\chimes.wav
+'''
+
+RETURN = r'''
+message_text:
+ description: The text that the module attempted to speak.
+ returned: success
+ type: str
+ sample: "Warning, deployment commencing in 5 minutes."
+voice:
+ description: The voice used to speak the text.
+ returned: success
+ type: str
+ sample: Microsoft Hazel Desktop
+voice_info:
+ description: The voice used to speak the text.
+ returned: when requested voice could not be loaded
+ type: str
+ sample: Could not load voice TestVoice, using system default voice
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_scheduled_task.ps1 b/ansible_collections/community/windows/plugins/modules/win_scheduled_task.ps1
new file mode 100644
index 00000000..02cd012f
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_scheduled_task.ps1
@@ -0,0 +1,1234 @@
+#!powershell
+
+# Copyright: (c) 2015, Peter Mounce <public@neverrunwithscissors.com>
+# Copyright: (c) 2015, Michael Perzel <michaelperzel@gmail.com>
+# Copyright: (c) 2017, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -PowerShell Ansible.ModuleUtils.AddType
+#Requires -Module Ansible.ModuleUtils.Legacy
+#Requires -Module Ansible.ModuleUtils.SID
+
+$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
+$_remote_tmp = Get-AnsibleParam $params "_ansible_remote_tmp" -type "path" -default $env:TMP
+
+$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
+$path = Get-AnsibleParam -obj $params -name "path" -type "str" -default "\"
+$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent", "present"
+
+# task actions, list of dicts [{path, arguments, working_directory}]
+$actions = Get-AnsibleParam -obj $params -name "actions" -type "list"
+
+# task triggers, list of dicts [{ type, ... }]
+$triggers = Get-AnsibleParam -obj $params -name "triggers" -type "list"
+
+# task Principal properties
+$display_name = Get-AnsibleParam -obj $params -name "display_name" -type "str"
+$group = Get-AnsibleParam -obj $params -name "group" -type "str"
+$logon_options = "none", "password", "s4u", "interactive_token", "group", "service_account", "interactive_token_or_password"
+$logon_type = Get-AnsibleParam -obj $params -name "logon_type" -type "str" -validateset $logon_options
+$run_level = Get-AnsibleParam -obj $params -name "run_level" -type "str" -validateset "limited", "highest" -aliases "runlevel"
+$username = Get-AnsibleParam -obj $params -name "username" -type "str" -aliases "user"
+$password = Get-AnsibleParam -obj $params -name "password" -type "str"
+$update_password = Get-AnsibleParam -obj $params -name "update_password" -type "bool" -default $true
+
+# task RegistrationInfo properties
+$author = Get-AnsibleParam -obj $params -name "author" -type "str"
+$date = Get-AnsibleParam -obj $params -name "date" -type "str"
+$description = Get-AnsibleParam -obj $params -name "description" -type "str"
+$source = Get-AnsibleParam -obj $params -name "source" -type "str"
+$version = Get-AnsibleParam -obj $params -name "version" -type "str"
+
+# task Settings properties
+$allow_demand_start = Get-AnsibleParam -obj $params -name "allow_demand_start" -type "bool"
+$allow_hard_terminate = Get-AnsibleParam -obj $params -name "allow_hard_terminate" -type "bool"
+# https://msdn.microsoft.com/en-us/library/windows/desktop/aa383486(v=vs.85).aspx
+$compatibility = Get-AnsibleParam -obj $params -name "compatibility" -type "int"
+$delete_expired_task_after = Get-AnsibleParam -obj $params -name "delete_expired_task_after" -type "str" # time string PT...
+$disallow_start_if_on_batteries = Get-AnsibleParam -obj $params -name "disallow_start_if_on_batteries" -type "bool"
+$enabled = Get-AnsibleParam -obj $params -name "enabled" -type "bool"
+$execution_time_limit = Get-AnsibleParam -obj $params -name "execution_time_limit" -type "str" # PT72H
+$hidden = Get-AnsibleParam -obj $params -name "hidden" -type "bool"
+# TODO: support for $idle_settings, needs to be created as a COM object
+# https://msdn.microsoft.com/en-us/library/windows/desktop/aa383507(v=vs.85).aspx
+$multiple_instances = Get-AnsibleParam -obj $params -name "multiple_instances" -type "int"
+# TODO: support for $network_settings, needs to be created as a COM object
+# https://msdn.microsoft.com/en-us/library/windows/desktop/aa383512(v=vs.85).aspx
+$priority = Get-AnsibleParam -obj $params -name "priority" -type "int"
+$restart_count = Get-AnsibleParam -obj $params -name "restart_count" -type "int"
+$restart_interval = Get-AnsibleParam -obj $params -name "restart_interval" -type "str" # time string PT..
+$run_only_if_idle = Get-AnsibleParam -obj $params -name "run_only_if_idle" -type "bool"
+$run_only_if_network_available = Get-AnsibleParam -obj $params -name "run_only_if_network_available" -type "bool"
+$start_when_available = Get-AnsibleParam -obj $params -name "start_when_available" -type "bool"
+$stop_if_going_on_batteries = Get-AnsibleParam -obj $params -name "stop_if_going_on_batteries" -type "bool"
+$wake_to_run = Get-AnsibleParam -obj $params -name "wake_to_run" -type "bool"
+
+$result = @{
+ changed = $false
+}
+
+if ($diff_mode) {
+ $result.diff = @{}
+}
+
+Add-CSharpType -TempPath $_remote_tmp -References @'
+public enum TASK_ACTION_TYPE // https://msdn.microsoft.com/en-us/library/windows/desktop/aa383553(v=vs.85).aspx
+{
+ TASK_ACTION_EXEC = 0,
+ // The below are not supported and are only kept for documentation purposes
+ TASK_ACTION_COM_HANDLER = 5,
+ TASK_ACTION_SEND_EMAIL = 6,
+ TASK_ACTION_SHOW_MESSAGE = 7
+}
+
+public enum TASK_CREATION // https://msdn.microsoft.com/en-us/library/windows/desktop/aa382538(v=vs.85).aspx
+{
+ TASK_VALIDATE_ONLY = 0x1,
+ TASK_CREATE = 0x2,
+ TASK_UPDATE = 0x4,
+ TASK_CREATE_OR_UPDATE = 0x6,
+ TASK_DISABLE = 0x8,
+ TASK_DONT_ADD_PRINCIPAL_ACE = 0x10,
+ TASK_IGNORE_REGISTRATION_TRIGGERS = 0x20
+}
+
+public enum TASK_LOGON_TYPE // https://msdn.microsoft.com/en-us/library/windows/desktop/aa383566(v=vs.85).aspx
+{
+ TASK_LOGON_NONE = 0,
+ TASK_LOGON_PASSWORD = 1,
+ TASK_LOGON_S4U = 2,
+ TASK_LOGON_INTERACTIVE_TOKEN = 3,
+ TASK_LOGON_GROUP = 4,
+ TASK_LOGON_SERVICE_ACCOUNT = 5,
+ TASK_LOGON_INTERACTIVE_TOKEN_OR_PASSWORD = 6
+}
+
+public enum TASK_RUN_LEVEL // https://msdn.microsoft.com/en-us/library/windows/desktop/aa380747(v=vs.85).aspx
+{
+ TASK_RUNLEVEL_LUA = 0,
+ TASK_RUNLEVEL_HIGHEST = 1
+}
+
+public enum TASK_TRIGGER_TYPE2 // https://msdn.microsoft.com/en-us/library/windows/desktop/aa383915(v=vs.85).aspx
+{
+ TASK_TRIGGER_EVENT = 0,
+ TASK_TRIGGER_TIME = 1,
+ TASK_TRIGGER_DAILY = 2,
+ TASK_TRIGGER_WEEKLY = 3,
+ TASK_TRIGGER_MONTHLY = 4,
+ TASK_TRIGGER_MONTHLYDOW = 5,
+ TASK_TRIGGER_IDLE = 6,
+ TASK_TRIGGER_REGISTRATION = 7,
+ TASK_TRIGGER_BOOT = 8,
+ TASK_TRIGGER_LOGON = 9,
+ TASK_TRIGGER_SESSION_STATE_CHANGE = 11
+}
+
+public enum TASK_SESSION_STATE_CHANGE_TYPE // https://docs.microsoft.com/en-us/windows/win32/api/taskschd/ne-taskschd-task_session_state_change_type
+{
+ TASK_CONSOLE_CONNECT = 1,
+ TASK_CONSOLE_DISCONNECT = 2,
+ TASK_REMOTE_CONNECT = 3,
+ TASK_REMOTE_DISCONNECT = 4,
+ TASK_SESSION_LOCK = 7,
+ TASK_SESSION_UNLOCK = 8
+}
+'@
+
+########################
+### HELPER FUNCTIONS ###
+########################
+Function Convert-SnakeToPascalCase($snake) {
+ # very basic function to convert snake_case to PascalCase for use in COM
+ # objects
+ [regex]$regex = "_(\w)"
+ $pascal_case = $regex.Replace($snake, { $args[0].Value.Substring(1).ToUpper() })
+ $capitalised = $pascal_case.Substring(0, 1).ToUpper() + $pascal_case.Substring(1)
+
+ return $capitalised
+}
+
+Function Compare-Property($property_name, $parent_property, $map, $enum_map = $null) {
+ $changes = [System.Collections.ArrayList]@()
+
+ # loop through the passed in map and compare values
+ # Name = The name of property in the COM object
+ # Value = The new value to compare the existing value with
+ foreach ($entry in $map.GetEnumerator()) {
+ $new_value = $entry.Value
+
+ if ($null -ne $new_value) {
+ $property_name = $entry.Name
+ $existing_value = $parent_property.$property_name
+ if ($existing_value -cne $new_value) {
+ try {
+ $parent_property.$property_name = $new_value
+ }
+ catch {
+ Fail-Json -obj $result -message "failed to set $property_name property '$property_name' to '$new_value': $($_.Exception.Message)"
+ }
+
+ if ($null -ne $enum_map -and $enum_map.ContainsKey($property_name)) {
+ $enum = [type]$enum_map.$property_name
+ $existing_value = [Enum]::ToObject($enum, $existing_value)
+ $new_value = [Enum]::ToObject($enum, $new_value)
+ }
+ [void]$changes.Add("-$property_name=$existing_value`n+$property_name=$new_value")
+ }
+ }
+ }
+
+ return , $changes
+}
+
+Function Set-PropertyForComObject($com_object, $name, $arg, $value) {
+ $com_name = Convert-SnakeToPascalCase -snake $arg
+ try {
+ $com_object.$com_name = $value
+ }
+ catch {
+ Fail-Json -obj $result -message "failed to set $name property '$com_name' to '$value': $($_.Exception.Message)"
+ }
+}
+
+Function Compare-PropertyList {
+ Param(
+ $collection, # the collection COM object to manipulate, this must contains the Create method
+ [string]$property_name, # human friendly name of the property object, e.g. action/trigger
+ [Array]$new, # a list of new properties, passed in by Ansible
+ [Array]$existing, # a list of existing properties from the COM object collection
+ [Hashtable]$map, # metadata for the collection, see below for the structure
+ [string]$enum # the parent enum name for type value
+ )
+ <## map metadata structure
+ {
+ collection type [TASK_ACTION_TYPE] for Actions or [TASK_TRIGGER_TYPE2] for Triggers {
+ mandatory = list of mandatory properties for this type, ansible input name not the COM name
+ optional = list of optional properties that could be set for this type
+ # maps the ansible input object name to the COM name, e.g. working_directory = WorkingDirectory
+ map = {
+ ansible input name = COM name
+ }
+ }
+ }##>
+ # used by both Actions and Triggers to compare the collections of that property
+
+ $enum = [type]$enum
+ $changes = [System.Collections.ArrayList]@()
+ $new_count = $new.Count
+ $existing_count = $existing.Count
+
+ for ($i = 0; $i -lt $new_count; $i++) {
+ if ($i -lt $existing_count) {
+ $existing_property = $existing[$i]
+ }
+ else {
+ $existing_property = $null
+ }
+ $new_property = $new[$i]
+
+ # get the type of the property, for action this is set automatically
+ if (-not $new_property.ContainsKey("type")) {
+ Fail-Json -obj $result -message "entry for $property_name must contain a type key"
+ }
+ $type = $new_property.type
+ $valid_types = $map.Keys
+ $property_map = $map.$type
+
+ # now let's validate the args for the property
+ $mandatory_args = $property_map.mandatory
+ $optional_args = $property_map.optional
+ $total_args = $mandatory_args + $optional_args
+
+ # validate the mandatory arguments
+ foreach ($mandatory_arg in $mandatory_args) {
+ if (-not $new_property.ContainsKey($mandatory_arg)) {
+ $msg = "mandatory key '$mandatory_arg' for $($property_name) is not set, mandatory keys are '$($mandatory_args -join "', '")'"
+ Fail-Json -obj $result -message $msg
+ }
+ }
+ # throw a warning if in invalid key was set
+ foreach ($entry in $new_property.GetEnumerator()) {
+ $key = $entry.Name
+ if ($key -notin $total_args -and $key -ne "type") {
+ $msg = "key '$key' for $($property_name) entry is not valid and will be ignored, valid keys are '$($total_args -join "', '")'"
+ Add-Warning -obj $result -message $msg
+ }
+ }
+
+ # now we have validated the input and have gotten the metadata, let's
+ # get the diff string
+ if ($null -eq $existing_property) {
+ # we have more properties than before,just add to the new
+ # properties list
+ $diff_list = [System.Collections.ArrayList]@()
+
+ foreach ($property_arg in $total_args) {
+ if ($new_property.ContainsKey($property_arg)) {
+ $com_name = Convert-SnakeToPascalCase -snake $property_arg
+ $property_value = $new_property.$property_arg
+
+ if ($property_value -is [Hashtable]) {
+ foreach ($kv in $property_value.GetEnumerator()) {
+ $sub_com_name = Convert-SnakeToPascalCase -snake $kv.Key
+ $sub_property_value = $kv.Value
+ [void]$diff_list.Add("+$com_name.$sub_com_name=$sub_property_value")
+ }
+ }
+ else {
+ [void]$diff_list.Add("+$com_name=$property_value")
+ }
+ }
+ }
+
+ [void]$changes.Add("+$property_name[$i] = {`n +Type=$type`n $($diff_list -join ",`n ")`n+}")
+ }
+ elseif ([Enum]::ToObject($enum, $existing_property.Type) -ne $type) {
+ # the types are different so we need to change
+ $diff_list = [System.Collections.ArrayList]@()
+
+ if ($existing_property.Type -notin $valid_types) {
+ [void]$diff_list.Add("-UNKNOWN TYPE $($existing_property.Type)")
+ foreach ($property_args in $total_args) {
+ if ($new_property.ContainsKey($property_arg)) {
+ $com_name = Convert-SnakeToPascalCase -snake $property_arg
+ $property_value = $new_property.$property_arg
+
+ if ($property_value -is [Hashtable]) {
+ foreach ($kv in $property_value.GetEnumerator()) {
+ $sub_com_name = Convert-SnakeToPascalCase -snake $kv.Key
+ $sub_property_value = $kv.Value
+ [void]$diff_list.Add("+$com_name.$sub_com_name=$sub_property_value")
+ }
+ }
+ else {
+ [void]$diff_list.Add("+$com_name=$property_value")
+ }
+ }
+ }
+ }
+ else {
+ # we know the types of the existing property
+ $existing_type = [Enum]::ToObject([TASK_TRIGGER_TYPE2], $existing_property.Type)
+ [void]$diff_list.Add("-Type=$existing_type")
+ [void]$diff_list.Add("+Type=$type")
+ foreach ($property_arg in $total_args) {
+ $com_name = Convert-SnakeToPascalCase -snake $property_arg
+ $property_value = $new_property.$property_arg
+ $existing_value = $existing_property.$com_name
+
+ if ($property_value -is [Hashtable]) {
+ foreach ($kv in $property_value.GetEnumerator()) {
+ $sub_property_value = $kv.Value
+ $sub_com_name = Convert-SnakeToPascalCase -snake $kv.Key
+ $sub_existing_value = $existing_property.$com_name.$sub_com_name
+
+ if ($null -ne $sub_property_value) {
+ [void]$diff_list.Add("+$com_name.$sub_com_name=$sub_property_value")
+ }
+
+ if ($null -ne $sub_existing_value) {
+ [void]$diff_list.Add("-$com_name.$sub_com_name=$sub_existing_value")
+ }
+ }
+ }
+ else {
+ if ($null -ne $property_value) {
+ [void]$diff_list.Add("+$com_name=$property_value")
+ }
+
+ if ($null -ne $existing_value) {
+ [void]$diff_list.Add("-$com_name=$existing_value")
+ }
+ }
+ }
+ }
+
+ [void]$changes.Add("$property_name[$i] = {`n $($diff_list -join ",`n ")`n}")
+ }
+ else {
+ # compare the properties of existing and new
+ $diff_list = [System.Collections.ArrayList]@()
+
+ foreach ($property_arg in $total_args) {
+ $com_name = Convert-SnakeToPascalCase -snake $property_arg
+ $property_value = $new_property.$property_arg
+ $existing_value = $existing_property.$com_name
+
+ if ($property_value -is [Hashtable]) {
+ foreach ($kv in $property_value.GetEnumerator()) {
+ $sub_property_value = $kv.Value
+
+ if ($null -ne $sub_property_value) {
+ $sub_com_name = Convert-SnakeToPascalCase -snake $kv.Key
+ $sub_existing_value = $existing_property.$com_name.$sub_com_name
+
+ if ($sub_property_value -cne $sub_existing_value) {
+ [void]$diff_list.Add("-$com_name.$sub_com_name=$sub_existing_value")
+ [void]$diff_list.Add("+$com_name.$sub_com_name=$sub_property_value")
+ }
+ }
+ }
+ }
+ elseif ($null -ne $property_value -and $property_value -cne $existing_value) {
+ [void]$diff_list.Add("-$com_name=$existing_value")
+ [void]$diff_list.Add("+$com_name=$property_value")
+ }
+ }
+
+ if ($diff_list.Count -gt 0) {
+ [void]$changes.Add("$property_name[$i] = {`n $($diff_list -join ",`n ")`n}")
+ }
+ }
+
+ # finally rebuild the new property collection
+ $new_object = $collection.Create($type)
+ foreach ($property_arg in $total_args) {
+ $new_value = $new_property.$property_arg
+ if ($new_value -is [Hashtable]) {
+ $com_name = Convert-SnakeToPascalCase -snake $property_arg
+ $new_object_property = $new_object.$com_name
+
+ foreach ($kv in $new_value.GetEnumerator()) {
+ $value = $kv.Value
+ if ($null -ne $value) {
+ Set-PropertyForComObject -com_object $new_object_property -name $property_name -arg $kv.Key -value $value
+ }
+ }
+ }
+ elseif ($null -ne $new_value) {
+ Set-PropertyForComObject -com_object $new_object -name $property_name -arg $property_arg -value $new_value
+ }
+ }
+ }
+
+ # if there were any extra properties not in the new list, create diff str
+ if ($existing_count -gt $new_count) {
+ for ($i = $new_count; $i -lt $existing_count; $i++) {
+ $diff_list = [System.Collections.ArrayList]@()
+ $existing_property = $existing[$i]
+ $existing_type = [Enum]::ToObject($enum, $existing_property.Type)
+
+ if ($map.ContainsKey($existing_type)) {
+ $property_map = $map.$existing_type
+ $property_args = $property_map.mandatory + $property_map.optional
+
+ foreach ($property_arg in $property_args) {
+ $com_name = Convert-SnakeToPascalCase -snake $property_arg
+ $existing_value = $existing_property.$com_name
+ if ($null -ne $existing_value) {
+ [void]$diff_list.Add("-$com_name=$existing_value")
+ }
+ }
+ }
+ else {
+ [void]$diff_list.Add("-UNKNOWN TYPE $existing_type")
+ }
+
+ [void]$changes.Add("-$property_name[$i] = {`n $($diff_list -join ",`n ")`n-}")
+ }
+ }
+
+ return , $changes
+}
+
+Function Compare-Action($task_definition) {
+ # compares the Actions property and returns a list of list of changed
+ # actions for use in a diff string
+ # ActionCollection - https://msdn.microsoft.com/en-us/library/windows/desktop/aa446804(v=vs.85).aspx
+ # Action - https://msdn.microsoft.com/en-us/library/windows/desktop/aa446803(v=vs.85).aspx
+ if ($null -eq $actions) {
+ return , [System.Collections.ArrayList]@()
+ }
+
+ $task_actions = $task_definition.Actions
+ $existing_count = $task_actions.Count
+
+ # because we clear the actions and re-add them to keep the order, we need
+ # to convert the existing actions to a new list.
+ # The Item property in actions starts at 1
+ $existing_actions = [System.Collections.ArrayList]@()
+ for ($i = 1; $i -le $existing_count; $i++) {
+ [void]$existing_actions.Add($task_actions.Item($i))
+ }
+ if ($existing_count -gt 0) {
+ $task_actions.Clear()
+ }
+
+ $map = @{
+ [TASK_ACTION_TYPE]::TASK_ACTION_EXEC = @{
+ mandatory = @('path')
+ optional = @('arguments', 'working_directory')
+ }
+ }
+ $changes = Compare-PropertyList -collection $task_actions -property_name "action" -new $actions -existing $existing_actions -map $map -enum TASK_ACTION_TYPE
+
+ return , $changes
+}
+
+Function Compare-Principal($task_definition, $task_definition_xml) {
+ # compares the Principal property and returns a list of changed objects for
+ # use in a diff string
+ # https://msdn.microsoft.com/en-us/library/windows/desktop/aa382071(v=vs.85).aspx
+ $principal_map = @{
+ DisplayName = $display_name
+ LogonType = $logon_type
+ RunLevel = $run_level
+ }
+ $enum_map = @{
+ LogonType = "TASK_LOGON_TYPE"
+ RunLevel = "TASK_RUN_LEVEL"
+ }
+ $task_principal = $task_definition.Principal
+ $changes = Compare-Property -property_name "Principal" -parent_property $task_principal -map $principal_map -enum_map $enum_map
+
+ # Principal.UserId and GroupId only returns the username portion of the
+ # username, skipping the domain or server name. This makes the
+ # comparison process useless so we need to parse the task XML to get
+ # the actual sid/username. Depending on OS version this could be the SID
+ # or it could be the username, we need to handle that accordingly
+ $principal_username_sid = $task_definition_xml.Task.Principals.Principal.UserId
+ if ($null -ne $principal_username_sid -and $principal_username_sid -notmatch "^S-\d-\d+(-\d+){1,14}(-\d+){0,1}$") {
+ $principal_username_sid = Convert-ToSID -account_name $principal_username_sid
+ }
+ $principal_group_sid = $task_definition_xml.Task.Principals.Principal.GroupId
+ if ($null -ne $principal_group_sid -and $principal_group_sid -notmatch "^S-\d-\d+(-\d+){1,14}(-\d+){0,1}$") {
+ $principal_group_sid = Convert-ToSID -account_name $principal_group_sid
+ }
+
+ if ($null -ne $username_sid) {
+ $new_user_name = Convert-FromSid -sid $username_sid
+ if ($null -ne $principal_group_sid) {
+ $existing_account_name = Convert-FromSid -sid $principal_group_sid
+ [void]$changes.Add("-GroupId=$existing_account_name`n+UserId=$new_user_name")
+ $task_principal.UserId = $new_user_name
+ $task_principal.GroupId = $null
+ }
+ elseif ($null -eq $principal_username_sid) {
+ [void]$changes.Add("+UserId=$new_user_name")
+ $task_principal.UserId = $new_user_name
+ }
+ elseif ($principal_username_sid -ne $username_sid) {
+ $existing_account_name = Convert-FromSid -sid $principal_username_sid
+ [void]$changes.Add("-UserId=$existing_account_name`n+UserId=$new_user_name")
+ $task_principal.UserId = $new_user_name
+ }
+ }
+ if ($null -ne $group_sid) {
+ $new_group_name = Convert-FromSid -sid $group_sid
+ if ($null -ne $principal_username_sid) {
+ $existing_account_name = Convert-FromSid -sid $principal_username_sid
+ [void]$changes.Add("-UserId=$existing_account_name`n+GroupId=$new_group_name")
+ $task_principal.UserId = $null
+ $task_principal.GroupId = $new_group_name
+ }
+ elseif ($null -eq $principal_group_sid) {
+ [void]$changes.Add("+GroupId=$new_group_name")
+ $task_principal.GroupId = $new_group_name
+ }
+ elseif ($principal_group_sid -ne $group_sid) {
+ $existing_account_name = Convert-FromSid -sid $principal_group_sid
+ [void]$changes.Add("-GroupId=$existing_account_name`n+GroupId=$new_group_name")
+ $task_principal.GroupId = $new_group_name
+ }
+ }
+
+ return , $changes
+}
+
+Function Compare-RegistrationInfo($task_definition) {
+ # compares the RegistrationInfo property and returns a list of changed
+ # objects for use in a diff string
+ # https://msdn.microsoft.com/en-us/library/windows/desktop/aa382100(v=vs.85).aspx
+ $reg_info_map = @{
+ Author = $author
+ Date = $date
+ Description = $description
+ Source = $source
+ Version = $version
+ }
+ $changes = Compare-Property -property_name "RegistrationInfo" -parent_property $task_definition.RegistrationInfo -map $reg_info_map
+
+ return , $changes
+}
+
+Function Compare-Setting($task_definition) {
+ # compares the task Settings property and returns a list of changed objects
+ # for use in a diff string
+ # https://msdn.microsoft.com/en-us/library/windows/desktop/aa383480(v=vs.85).aspx
+ $settings_map = @{
+ AllowDemandStart = $allow_demand_start
+ AllowHardTerminate = $allow_hard_terminate
+ Compatibility = $compatibility
+ DeleteExpiredTaskAfter = $delete_expired_task_after
+ DisallowStartIfOnBatteries = $disallow_start_if_on_batteries
+ ExecutionTimeLimit = $execution_time_limit
+ Enabled = $enabled
+ Hidden = $hidden
+ # IdleSettings = $idle_settings # TODO: this takes in a COM object
+ MultipleInstances = $multiple_instances
+ # NetworkSettings = $network_settings # TODO: this takes in a COM object
+ Priority = $priority
+ RestartCount = $restart_count
+ RestartInterval = $restart_interval
+ RunOnlyIfIdle = $run_only_if_idle
+ RunOnlyIfNetworkAvailable = $run_only_if_network_available
+ StartWhenAvailable = $start_when_available
+ StopIfGoingOnBatteries = $stop_if_going_on_batteries
+ WakeToRun = $wake_to_run
+ }
+ $changes = Compare-Property -property_name "Settings" -parent_property $task_definition.Settings -map $settings_map
+
+ return , $changes
+}
+
+Function Compare-Trigger($task_definition) {
+ # compares the task Triggers property and returns a list of changed objects
+ # for use in a diff string
+ # TriggerCollection - https://msdn.microsoft.com/en-us/library/windows/desktop/aa383875(v=vs.85).aspx
+ # Trigger - https://msdn.microsoft.com/en-us/library/windows/desktop/aa383868(v=vs.85).aspx
+ if ($null -eq $triggers) {
+ return , [System.Collections.ArrayList]@()
+ }
+
+ $task_triggers = $task_definition.Triggers
+ $existing_count = $task_triggers.Count
+
+ # because we clear the actions and re-add them to keep the order, we need
+ # to convert the existing actions to a new list.
+ # The Item property in actions starts at 1
+ $existing_triggers = [System.Collections.ArrayList]@()
+ for ($i = 1; $i -le $existing_count; $i++) {
+ [void]$existing_triggers.Add($task_triggers.Item($i))
+ }
+ if ($existing_count -gt 0) {
+ $task_triggers.Clear()
+ }
+
+ $map = @{
+ [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_BOOT = @{
+ mandatory = @()
+ optional = @('delay', 'enabled', 'end_boundary', 'execution_time_limit', 'start_boundary', 'repetition')
+ }
+ [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_DAILY = @{
+ mandatory = @('start_boundary')
+ optional = @('days_interval', 'enabled', 'end_boundary', 'execution_time_limit', 'random_delay', 'repetition')
+ }
+ [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_EVENT = @{
+ mandatory = @('subscription')
+ # TODO: ValueQueries is a COM object
+ optional = @('delay', 'enabled', 'end_boundary', 'execution_time_limit', 'start_boundary', 'repetition')
+ }
+ [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_IDLE = @{
+ mandatory = @()
+ optional = @('enabled', 'end_boundary', 'execution_time_limit', 'start_boundary', 'repetition')
+ }
+ [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_LOGON = @{
+ mandatory = @()
+ optional = @('delay', 'enabled', 'end_boundary', 'execution_time_limit', 'start_boundary', 'user_id', 'repetition')
+ }
+ [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_MONTHLYDOW = @{
+ mandatory = @('start_boundary')
+ # Make sure run_on_last_week_of_month comes after weeks_of_month
+ # https://github.com/ansible-collections/community.windows/issues/414
+ optional = @('days_of_week', 'enabled', 'end_boundary', 'execution_time_limit', 'months_of_year', 'random_delay', 'weeks_of_month',
+ 'run_on_last_week_of_month', 'repetition')
+ }
+ [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_MONTHLY = @{
+ mandatory = @('days_of_month', 'start_boundary')
+ optional = @('enabled', 'end_boundary', 'execution_time_limit', 'months_of_year', 'random_delay', 'run_on_last_day_of_month',
+ 'start_boundary', 'repetition')
+ }
+ [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_REGISTRATION = @{
+ mandatory = @()
+ optional = @('delay', 'enabled', 'end_boundary', 'execution_time_limit', 'start_boundary', 'repetition')
+ }
+ [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_TIME = @{
+ mandatory = @('start_boundary')
+ optional = @('enabled', 'end_boundary', 'execution_time_limit', 'random_delay', 'repetition')
+ }
+ [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_WEEKLY = @{
+ mandatory = @('days_of_week', 'start_boundary')
+ optional = @('enabled', 'end_boundary', 'execution_time_limit', 'random_delay', 'weeks_interval', 'repetition')
+ }
+ [TASK_TRIGGER_TYPE2]::TASK_TRIGGER_SESSION_STATE_CHANGE = @{
+ mandatory = @()
+ optional = @('delay', 'enabled', 'end_boundary', 'execution_time_limit', 'repetition', 'start_boundary', 'state_change', 'user_id' )
+ }
+ }
+ $compareParams = @{
+ collection = $task_triggers
+ property_name = "trigger"
+ new = $triggers
+ existing = $existing_triggers
+ map = $map
+ enum = "TASK_TRIGGER_TYPE2"
+ }
+ $changes = Compare-PropertyList @compareParams
+
+ return , $changes
+}
+
+Function Test-TaskExist($task_folder, $name) {
+ # checks if a task exists in the TaskFolder COM object, returns null if the
+ # task does not exist, otherwise returns the RegisteredTask object
+ $task = $null
+ if ($task_folder) {
+ $raw_tasks = $task_folder.GetTasks(1) # 1 = TASK_ENUM_HIDDEN
+
+ for ($i = 1; $i -le $raw_tasks.Count; $i++) {
+ if ($raw_tasks.Item($i).Name -eq $name) {
+ $task = $raw_tasks.Item($i)
+ break
+ }
+ }
+ }
+
+ return $task
+}
+
+Function Test-XmlDurationFormat($key, $value) {
+ # validate value is in the Duration Data Type format
+ # PnYnMnDTnHnMnS
+ try {
+ $time_span = [System.Xml.XmlConvert]::ToTimeSpan($value)
+ return $time_span
+ }
+ catch [System.FormatException] {
+ Fail-Json -obj $result -message "trigger option '$key' must be in the XML duration format but was '$value'"
+ }
+}
+
+######################################
+### VALIDATION/BUILDING OF OPTIONS ###
+######################################
+
+# invalid characters in task name
+$invalid_name_chars = '\/:*?"<>|'
+$invalid_name_chars_regex = "[$([regex]::Escape($invalid_name_chars))]"
+
+if ($name -cmatch $invalid_name_chars_regex) {
+ Fail-Json -obj $result -message "Invalid task name '$name'. The following characters are not valid: $invalid_name_chars"
+}
+
+# convert username and group to SID if set
+$username_sid = $null
+if ($username) {
+ $username_sid = Convert-ToSID -account_name $username
+}
+$group_sid = $null
+if ($group) {
+ $group_sid = Convert-ToSID -account_name $group
+}
+
+# validate store_password and logon_type
+if ($null -ne $logon_type) {
+ $full_enum_name = "TASK_LOGON_$($logon_type.ToUpper())"
+ $logon_type = [TASK_LOGON_TYPE]::$full_enum_name
+}
+
+# now validate the logon_type option with the other parameters
+if ($null -ne $username -and $null -ne $group) {
+ Fail-Json -obj $result -message "username and group can not be set at the same time"
+}
+if ($null -ne $logon_type) {
+ if ($logon_type -eq [TASK_LOGON_TYPE]::TASK_LOGON_S4U -and $null -eq $password) {
+ Fail-Json -obj $result -message "password must be set when logon_type=s4u"
+ }
+
+ if ($logon_type -eq [TASK_LOGON_TYPE]::TASK_LOGON_GROUP -and $null -eq $group) {
+ Fail-Json -obj $result -message "group must be set when logon_type=group"
+ }
+
+ # SIDs == Local System, Local Service and Network Service
+ if ($logon_type -eq [TASK_LOGON_TYPE]::TASK_LOGON_SERVICE_ACCOUNT -and $username_sid -notin @("S-1-5-18", "S-1-5-19", "S-1-5-20")) {
+ Fail-Json -obj $result -message "username must be SYSTEM, LOCAL SERVICE or NETWORK SERVICE when logon_type=service_account"
+ }
+}
+
+# convert the run_level to enum value
+if ($null -ne $run_level) {
+ if ($run_level -eq "limited") {
+ $run_level = [TASK_RUN_LEVEL]::TASK_RUNLEVEL_LUA
+ }
+ else {
+ $run_level = [TASK_RUN_LEVEL]::TASK_RUNLEVEL_HIGHEST
+ }
+}
+
+# manually add the only support action type for each action - also convert PSCustomObject to Hashtable
+for ($i = 0; $i -lt $actions.Count; $i++) {
+ $action = $actions[$i]
+ $action.type = [TASK_ACTION_TYPE]::TASK_ACTION_EXEC
+ if (-not $action.ContainsKey("path")) {
+ Fail-Json -obj $result -message "action entry must contain the key 'path'"
+ }
+ $actions[$i] = $action
+}
+
+# convert and validate the triggers - and convert PSCustomObject to Hashtable
+for ($i = 0; $i -lt $triggers.Count; $i++) {
+ $trigger = $triggers[$i]
+ $valid_trigger_types = @('event', 'time', 'daily', 'weekly', 'monthly', 'monthlydow', 'idle', 'registration', 'boot', 'logon', 'session_state_change')
+ if (-not $trigger.ContainsKey("type")) {
+ Fail-Json -obj $result -message "a trigger entry must contain a key 'type' with a value of '$($valid_trigger_types -join "', '")'"
+ }
+
+ $trigger_type = $trigger.type
+ if ($trigger_type -notin $valid_trigger_types) {
+ $msg = "the specified trigger type '$trigger_type' is not valid, type must be a value of '$($valid_trigger_types -join "', '")'"
+ Fail-Json -obj $result -message $msg
+ }
+
+ $full_enum_name = "TASK_TRIGGER_$($trigger_type.ToUpper())"
+ $trigger_type = [TASK_TRIGGER_TYPE2]::$full_enum_name
+ $trigger.type = $trigger_type
+
+ $date_properties = @('start_boundary', 'end_boundary')
+ foreach ($property_name in $date_properties) {
+ # validate the date is in the DateTime format
+ # yyyy-mm-ddThh:mm:ss
+ if ($trigger.ContainsKey($property_name)) {
+ $date_value = $trigger.$property_name
+ try {
+ $date = Get-Date -Date $date_value -Format "yyyy-MM-dd'T'HH:mm:ssK"
+ # make sure we convert it to the full string format
+ $trigger.$property_name = $date.ToString()
+ }
+ catch [System.Management.Automation.ParameterBindingException] {
+ Fail-Json -obj $result -message "trigger option '$property_name' must be in the format 'YYYY-MM-DDThh:mm:ss' format but was '$date_value'"
+ }
+ }
+ }
+
+ $time_properties = @('execution_time_limit', 'delay', 'random_delay')
+ foreach ($property_name in $time_properties) {
+ if ($trigger.ContainsKey($property_name)) {
+ $time_span = $trigger.$property_name
+ Test-XmlDurationFormat -key $property_name -value $time_span
+ }
+ }
+
+ if ($trigger.ContainsKey("repetition")) {
+ if ($trigger.repetition -is [Array]) {
+ # Legacy doesn't natively support deprecate by date, need to do this manually until we use Ansible.Basic
+ if (-not $result.ContainsKey('deprecations')) {
+ $result.deprecations = @()
+ }
+ $result.deprecations += @{
+ msg = "repetition is a list, should be defined as a dict"
+ date = "2021-07-01"
+ collection_name = "community.windows"
+ }
+ $trigger.repetition = $trigger.repetition[0]
+ }
+
+ $interval_timespan = $null
+ if ($trigger.repetition.ContainsKey("interval") -and $null -ne $trigger.repetition.interval) {
+ $interval_timespan = Test-XmlDurationFormat -key "interval" -value $trigger.repetition.interval
+ }
+
+ $duration_timespan = $null
+ if ($trigger.repetition.ContainsKey("duration") -and $null -ne $trigger.repetition.duration) {
+ $duration_timespan = Test-XmlDurationFormat -key "duration" -value $trigger.repetition.duration
+ }
+
+ if ($null -ne $interval_timespan -and $null -ne $duration_timespan -and $interval_timespan -gt $duration_timespan) {
+ $msg = -join @(
+ "trigger repetition option 'interval' value '$($trigger.repetition.interval)' "
+ "must be less than or equal to 'duration' value '$($trigger.repetition.duration)'"
+ )
+ Fail-Json -obj $result -message $msg
+ }
+ }
+
+ # convert out human readble text to the hex values for these properties
+ if ($trigger.ContainsKey("days_of_week")) {
+ $days = $trigger.days_of_week
+ if ($days -is [String]) {
+ $days = $days.Split(",").Trim()
+ }
+ elseif ($days -isnot [Array]) {
+ $days = @($days)
+ }
+
+ $day_value = 0
+ foreach ($day in $days) {
+ # https://msdn.microsoft.com/en-us/library/windows/desktop/aa382057(v=vs.85).aspx
+ switch ($day) {
+ sunday { $day_value = $day_value -bor 0x01 }
+ monday { $day_value = $day_value -bor 0x02 }
+ tuesday { $day_value = $day_value -bor 0x04 }
+ wednesday { $day_value = $day_value -bor 0x08 }
+ thursday { $day_value = $day_value -bor 0x10 }
+ friday { $day_value = $day_value -bor 0x20 }
+ saturday { $day_value = $day_value -bor 0x40 }
+ default { Fail-Json -obj $result -message "invalid day of week '$day', check the spelling matches the full day name" }
+ }
+ }
+ if ($day_value -eq 0) {
+ $day_value = $null
+ }
+
+ $trigger.days_of_week = $day_value
+ }
+ if ($trigger.ContainsKey("days_of_month")) {
+ $days = $trigger.days_of_month
+ if ($days -is [String]) {
+ $days = $days.Split(",").Trim()
+ }
+ elseif ($days -isnot [Array]) {
+ $days = @($days)
+ }
+
+ $day_value = 0
+ foreach ($day in $days) {
+ # https://msdn.microsoft.com/en-us/library/windows/desktop/aa382063(v=vs.85).aspx
+ switch ($day) {
+ 1 { $day_value = $day_value -bor 0x01 }
+ 2 { $day_value = $day_value -bor 0x02 }
+ 3 { $day_value = $day_value -bor 0x04 }
+ 4 { $day_value = $day_value -bor 0x08 }
+ 5 { $day_value = $day_value -bor 0x10 }
+ 6 { $day_value = $day_value -bor 0x20 }
+ 7 { $day_value = $day_value -bor 0x40 }
+ 8 { $day_value = $day_value -bor 0x80 }
+ 9 { $day_value = $day_value -bor 0x100 }
+ 10 { $day_value = $day_value -bor 0x200 }
+ 11 { $day_value = $day_value -bor 0x400 }
+ 12 { $day_value = $day_value -bor 0x800 }
+ 13 { $day_value = $day_value -bor 0x1000 }
+ 14 { $day_value = $day_value -bor 0x2000 }
+ 15 { $day_value = $day_value -bor 0x4000 }
+ 16 { $day_value = $day_value -bor 0x8000 }
+ 17 { $day_value = $day_value -bor 0x10000 }
+ 18 { $day_value = $day_value -bor 0x20000 }
+ 19 { $day_value = $day_value -bor 0x40000 }
+ 20 { $day_value = $day_value -bor 0x80000 }
+ 21 { $day_value = $day_value -bor 0x100000 }
+ 22 { $day_value = $day_value -bor 0x200000 }
+ 23 { $day_value = $day_value -bor 0x400000 }
+ 24 { $day_value = $day_value -bor 0x800000 }
+ 25 { $day_value = $day_value -bor 0x1000000 }
+ 26 { $day_value = $day_value -bor 0x2000000 }
+ 27 { $day_value = $day_value -bor 0x4000000 }
+ 28 { $day_value = $day_value -bor 0x8000000 }
+ 29 { $day_value = $day_value -bor 0x10000000 }
+ 30 { $day_value = $day_value -bor 0x20000000 }
+ 31 { $day_value = $day_value -bor 0x40000000 }
+ default { Fail-Json -obj $result -message "invalid day of month '$day', please specify numbers from 1-31" }
+ }
+ }
+ if ($day_value -eq 0) {
+ $day_value = $null
+ }
+ $trigger.days_of_month = $day_value
+ }
+ if ($trigger.ContainsKey("weeks_of_month")) {
+ $weeks = $trigger.weeks_of_month
+ if ($weeks -is [String]) {
+ $weeks = $weeks.Split(",").Trim()
+ }
+ elseif ($weeks -isnot [Array]) {
+ $weeks = @($weeks)
+ }
+
+ $week_value = 0
+ foreach ($week in $weeks) {
+ # https://msdn.microsoft.com/en-us/library/windows/desktop/aa382061(v=vs.85).aspx
+ switch ($week) {
+ 1 { $week_value = $week_value -bor 0x01 }
+ 2 { $week_value = $week_value -bor 0x02 }
+ 3 { $week_value = $week_value -bor 0x04 }
+ 4 { $week_value = $week_value -bor 0x08 }
+ default { Fail-Json -obj $result -message "invalid week of month '$week', please specify weeks from 1-4" }
+ }
+
+ }
+ if ($week_value -eq 0) {
+ $week_value = $null
+ }
+ $trigger.weeks_of_month = $week_value
+ }
+ if ($trigger.ContainsKey("months_of_year")) {
+ $months = $trigger.months_of_year
+ if ($months -is [String]) {
+ $months = $months.Split(",").Trim()
+ }
+ elseif ($months -isnot [Array]) {
+ $months = @($months)
+ }
+
+ $month_value = 0
+ foreach ($month in $months) {
+ # https://msdn.microsoft.com/en-us/library/windows/desktop/aa382064(v=vs.85).aspx
+ switch ($month) {
+ january { $month_value = $month_value -bor 0x01 }
+ february { $month_value = $month_value -bor 0x02 }
+ march { $month_value = $month_value -bor 0x04 }
+ april { $month_value = $month_value -bor 0x08 }
+ may { $month_value = $month_value -bor 0x10 }
+ june { $month_value = $month_value -bor 0x20 }
+ july { $month_value = $month_value -bor 0x40 }
+ august { $month_value = $month_value -bor 0x80 }
+ september { $month_value = $month_value -bor 0x100 }
+ october { $month_value = $month_value -bor 0x200 }
+ november { $month_value = $month_value -bor 0x400 }
+ december { $month_value = $month_value -bor 0x800 }
+ default { Fail-Json -obj $result -message "invalid month name '$month', please specify full month name" }
+ }
+ }
+ if ($month_value -eq 0) {
+ $month_value = $null
+ }
+ $trigger.months_of_year = $month_value
+ }
+ if ($trigger.ContainsKey("state_change")) {
+ $trigger.state_change = switch ($trigger.state_change) {
+ console_connect { [TASK_SESSION_STATE_CHANGE_TYPE]::TASK_CONSOLE_CONNECT }
+ console_disconnect { [TASK_SESSION_STATE_CHANGE_TYPE]::TASK_CONSOLE_DISCONNECT }
+ remote_connect { [TASK_SESSION_STATE_CHANGE_TYPE]::TASK_REMOTE_CONNECT }
+ remote_disconnect { [TASK_SESSION_STATE_CHANGE_TYPE]::TASK_REMOTE_DISCONNECT }
+ session_lock { [TASK_SESSION_STATE_CHANGE_TYPE]::TASK_SESSION_LOCK }
+ session_unlock { [TASK_SESSION_STATE_CHANGE_TYPE]::TASK_SESSION_UNLOCK }
+ default {
+ Fail-Json -obj $result -message "invalid state_change '$($trigger.state_change)'"
+ }
+ }
+ }
+ $triggers[$i] = $trigger
+}
+
+# add \ to start of path if it is not already there
+if (-not $path.StartsWith("\")) {
+ $path = "\$path"
+}
+# ensure path does not end with \ if more than 1 char
+if ($path.EndsWith("\") -and $path.Length -ne 1) {
+ $path = $path.Substring(0, $path.Length - 1)
+}
+
+########################
+### START CODE BLOCK ###
+########################
+$service = New-Object -ComObject Schedule.Service
+try {
+ $service.Connect()
+}
+catch {
+ Fail-Json -obj $result -message "failed to connect to the task scheduler service: $($_.Exception.Message)"
+}
+
+# check that the path for the task set exists, create if need be
+try {
+ $task_folder = $service.GetFolder($path)
+}
+catch {
+ $task_folder = $null
+}
+
+# try and get the task at the path
+$task = Test-TaskExist -task_folder $task_folder -name $name
+$task_path = Join-Path -Path $path -ChildPath $name
+
+if ($state -eq "absent") {
+ if ($null -ne $task) {
+ if (-not $check_mode) {
+ try {
+ $task_folder.DeleteTask($name, 0)
+ }
+ catch {
+ Fail-Json -obj $result -message "failed to delete task '$name' at path '$path': $($_.Exception.Message)"
+ }
+ }
+ if ($diff_mode) {
+ $result.diff.prepared = "-[Task]`n-$task_path`n"
+ }
+ $result.changed = $true
+
+ # check if current folder has any more tasks
+ $other_tasks = $task_folder.GetTasks(1) # 1 = TASK_ENUM_HIDDEN
+ if ($other_tasks.Count -eq 0 -and $task_folder.Name -ne "\") {
+ try {
+ $task_folder.DeleteFolder($null, $null)
+ }
+ catch {
+ Fail-Json -obj $result -message "failed to delete empty task folder '$path' after task deletion: $($_.Exception.Message)"
+ }
+ }
+ }
+}
+else {
+ if ($null -eq $task) {
+ $create_diff_string = "+[Task]`n+$task_path`n`n"
+ # to create a bare minimum task we need 1 action
+ if ($null -eq $actions -or $actions.Count -eq 0) {
+ Fail-Json -obj $result -message "cannot create a task with no actions, set at least one action with a path to an executable"
+ }
+
+ # Create a bare minimum task here, further properties will be set later on
+ $task_definition = $service.NewTask(0)
+
+ # Set Actions info
+ # https://msdn.microsoft.com/en-us/library/windows/desktop/aa446803(v=vs.85).aspx
+ $create_diff_string += "[Actions]`n"
+ $task_actions = $task_definition.Actions
+ foreach ($action in $actions) {
+ $create_diff_string += "+action[0] = {`n +Type=$([TASK_ACTION_TYPE]::TASK_ACTION_EXEC),`n +Path=$($action.path)`n"
+ $task_action = $task_actions.Create([TASK_ACTION_TYPE]::TASK_ACTION_EXEC)
+ $task_action.Path = $action.path
+ if ($null -ne $action.arguments) {
+ $create_diff_string += " +Arguments=$($action.arguments)`n"
+ $task_action.Arguments = $action.arguments
+ }
+ if ($null -ne $action.working_directory) {
+ $create_diff_string += " +WorkingDirectory=$($action.working_directory)`n"
+ $task_action.WorkingDirectory = $action.working_directory
+ }
+ $create_diff_string += "+}`n"
+ }
+
+ # Register the new task
+ # https://msdn.microsoft.com/en-us/library/windows/desktop/aa382577(v=vs.85).aspx
+ if ($check_mode) {
+ # Only validate the task in check mode
+ $task_creation_flags = [TASK_CREATION]::TASK_VALIDATE_ONLY
+ }
+ else {
+ # Create the task but do not fire it as we still need to configure it further below
+ $task_creation_flags = [TASK_CREATION]::TASK_CREATE -bor [TASK_CREATION]::TASK_IGNORE_REGISTRATION_TRIGGERS
+ }
+
+ # folder doesn't exist, need to create
+ if ($null -eq $task_folder) {
+ $task_folder = $service.GetFolder("\")
+ try {
+ if (-not $check_mode) {
+ $task_folder = $task_folder.CreateFolder($path)
+ }
+ }
+ catch {
+ Fail-Json -obj $result -message "failed to create new folder at path '$path': $($_.Exception.Message)"
+ }
+ }
+
+ try {
+ $task = $task_folder.RegisterTaskDefinition($name, $task_definition, $task_creation_flags, $null, $null, $null)
+ }
+ catch {
+ Fail-Json -obj $result -message "failed to register new task definition: $($_.Exception.Message)"
+ }
+ if ($diff_mode) {
+ $result.diff.prepared = $create_diff_string
+ }
+
+ $result.changed = $true
+ }
+
+ # we cannot configure a task that was created above in check mode as it
+ # won't actually exist
+ if ($task) {
+ $task_definition = $task.Definition
+ $task_definition_xml = [xml]$task_definition.XmlText
+
+ $action_changes = Compare-Action -task_definition $task_definition
+ $principal_changed = Compare-Principal -task_definition $task_definition -task_definition_xml $task_definition_xml
+ $reg_info_changed = Compare-RegistrationInfo -task_definition $task_definition
+ $settings_changed = Compare-Setting -task_definition $task_definition
+ $trigger_changes = Compare-Trigger -task_definition $task_definition
+
+ # compile the diffs into one list with headers
+ $task_diff = [System.Collections.ArrayList]@()
+ if ($action_changes.Count -gt 0) {
+ [void]$task_diff.Add("[Actions]")
+ foreach ($action_change in $action_changes) {
+ [void]$task_diff.Add($action_change)
+ }
+ [void]$task_diff.Add("`n")
+ }
+ if ($principal_changed.Count -gt 0) {
+ [void]$task_diff.Add("[Principal]")
+ foreach ($principal_change in $principal_changed) {
+ [void]$task_diff.Add($principal_change)
+ }
+ [void]$task_diff.Add("`n")
+ }
+ if ($reg_info_changed.Count -gt 0) {
+ [void]$task_diff.Add("[Registration Info]")
+ foreach ($reg_info_change in $reg_info_changed) {
+ [void]$task_diff.Add($reg_info_change)
+ }
+ [void]$task_diff.Add("`n")
+ }
+ if ($settings_changed.Count -gt 0) {
+ [void]$task_diff.Add("[Settings]")
+ foreach ($settings_change in $settings_changed) {
+ [void]$task_diff.add($settings_change)
+ }
+ [void]$task_diff.Add("`n")
+ }
+ if ($trigger_changes.Count -gt 0) {
+ [void]$task_diff.Add("[Triggers]")
+ foreach ($trigger_change in $trigger_changes) {
+ [void]$task_diff.Add("$trigger_change")
+ }
+ [void]$task_diff.Add("`n")
+ }
+
+ if ($null -ne $password -and (($update_password -eq $true) -or ($task_diff.Count -gt 0))) {
+ # because we can't compare the passwords we just need to reset it
+ $register_username = $username
+ $register_password = $password
+ $register_logon_type = $task_principal.LogonType
+ }
+ else {
+ # will inherit from the Principal property values
+ $register_username = $null
+ $register_password = $null
+ $register_logon_type = $null
+ }
+
+ if ($task_diff.Count -gt 0 -or $null -ne $register_password) {
+ if ($check_mode) {
+ # Only validate the task in check mode
+ $task_creation_flags = [TASK_CREATION]::TASK_VALIDATE_ONLY
+ }
+ else {
+ # Create the task
+ $task_creation_flags = [TASK_CREATION]::TASK_CREATE_OR_UPDATE
+ }
+ try {
+ $task_folder.RegisterTaskDefinition(
+ $name,
+ $task_definition,
+ $task_creation_flags,
+ $register_username,
+ $register_password,
+ $register_logon_type
+ ) | Out-Null
+ }
+ catch {
+ Fail-Json -obj $result -message "failed to modify scheduled task: $($_.Exception.Message)"
+ }
+
+ $result.changed = $true
+
+ if ($diff_mode) {
+ $changed_diff_text = $task_diff -join "`n"
+ if ($null -ne $result.diff.prepared) {
+ $diff_text = "$($result.diff.prepared)`n$changed_diff_text"
+ }
+ else {
+ $diff_text = $changed_diff_text
+ }
+ $result.diff.prepared = $diff_text.Trim()
+ }
+ }
+ }
+}
+
+Exit-Json -obj $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_scheduled_task.py b/ansible_collections/community/windows/plugins/modules/win_scheduled_task.py
new file mode 100644
index 00000000..d43089b8
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_scheduled_task.py
@@ -0,0 +1,539 @@
+#!/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)
+
+DOCUMENTATION = r'''
+---
+module: win_scheduled_task
+short_description: Manage scheduled tasks
+description:
+- Creates/modifies or removes Windows scheduled tasks.
+options:
+ # module definition options
+ name:
+ description:
+ - The name of the scheduled task without the path.
+ type: str
+ required: yes
+ path:
+ description:
+ - Task folder in which this task will be stored.
+ - Will create the folder when C(state=present) and the folder does not
+ already exist.
+ - Will remove the folder when C(state=absent) and there are no tasks left
+ in the folder.
+ type: str
+ default: \
+ state:
+ description:
+ - When C(state=present) will ensure the task exists.
+ - When C(state=absent) will ensure the task does not exist.
+ type: str
+ choices: [ absent, present ]
+ default: present
+
+ # Action options
+ actions:
+ description:
+ - A list of action to configure for the task.
+ - See suboptions for details on how to construct each list entry.
+ - When creating a task there MUST be at least one action but when deleting
+ a task this can be a null or an empty list.
+ - The ordering of this list is important, the module will ensure the order
+ is kept when modifying the task.
+ - This module only supports the C(ExecAction) type but can still delete the
+ older legacy types.
+ type: list
+ elements: dict
+ suboptions:
+ path:
+ description:
+ - The path to the executable for the ExecAction.
+ type: str
+ required: yes
+ arguments:
+ description:
+ - An argument string to supply for the executable.
+ type: str
+ working_directory:
+ description:
+ - The working directory to run the executable from.
+ type: str
+
+ # Trigger options
+ triggers:
+ description:
+ - A list of triggers to configure for the task.
+ - See suboptions for details on how to construct each list entry.
+ - The ordering of this list is important, the module will ensure the order
+ is kept when modifying the task.
+ - There are multiple types of triggers, see U(https://msdn.microsoft.com/en-us/library/windows/desktop/aa383868.aspx)
+ for a list of trigger types and their options.
+ - The suboption options listed below are not required for all trigger
+ types, read the description for more details.
+ type: list
+ elements: dict
+ suboptions:
+ type:
+ description:
+ - The trigger type, this value controls what below options are
+ required.
+ type: str
+ required: yes
+ choices: [ boot, daily, event, idle, logon, monthlydow, monthly, registration, time, weekly, session_state_change ]
+ enabled:
+ description:
+ - Whether to set the trigger to enabled or disabled
+ - Used in all trigger types.
+ type: bool
+ start_boundary:
+ description:
+ - The start time for the task, even if the trigger meets the other
+ start criteria, it won't start until this time is met.
+ - If you wish to run a task at 9am on a day you still need to specify
+ the date on which the trigger is activated, you can set any date even
+ ones in the past.
+ - Required when C(type) is C(daily), C(monthlydow), C(monthly),
+ C(time), C(weekly).
+ - Optional for the rest of the trigger types.
+ - This is in ISO 8601 DateTime format C(YYYY-MM-DDThh:mm:ss).
+ type: str
+ end_boundary:
+ description:
+ - The end time for when the trigger is deactivated.
+ - This is in ISO 8601 DateTime format C(YYYY-MM-DDThh:mm:ss).
+ type: str
+ execution_time_limit:
+ description:
+ - The maximum amount of time that the task is allowed to run for.
+ - Optional for all the trigger types.
+ - Is in the ISO 8601 Duration format C(P[n]Y[n]M[n]DT[n]H[n]M[n]S).
+ type: str
+ delay:
+ description:
+ - The time to delay the task from running once the trigger has been
+ fired.
+ - Optional when C(type) is C(boot), C(event), C(logon),
+ C(registration), C(session_state_change).
+ - Is in the ISO 8601 Duration format C(P[n]Y[n]M[n]DT[n]H[n]M[n]S).
+ type: str
+ random_delay:
+ description:
+ - The delay time that is randomly added to the start time of the
+ trigger.
+ - Optional when C(type) is C(daily), C(monthlydow), C(monthly),
+ C(time), C(weekly).
+ - Is in the ISO 8601 Duration format C(P[n]Y[n]M[n]DT[n]H[n]M[n]S).
+ type: str
+ subscription:
+ description:
+ - Only used and is required for C(type=event).
+ - The XML query string that identifies the event that fires the
+ trigger.
+ type: str
+ user_id:
+ description:
+ - The username that the trigger will target.
+ - Optional when C(type) is C(logon), C(session_state_change).
+ - Can be the username or SID of a user.
+ - When C(type=logon) and you want the trigger to fire when a user in a
+ group logs on, leave this as null and set C(group) to the group you
+ wish to trigger.
+ type: str
+ days_of_week:
+ description:
+ - The days of the week for the trigger.
+ - Can be a list or comma separated string of full day names e.g. monday
+ instead of mon.
+ - Required when C(type) is C(weekly).
+ - Optional when C(type=monthlydow).
+ type: str
+ days_of_month:
+ description:
+ - The days of the month from 1 to 31 for the triggers.
+ - If you wish to set the trigger for the last day of any month
+ use C(run_on_last_day_of_month).
+ - Can be a list or comma separated string of day numbers.
+ - Required when C(type=monthly).
+ type: str
+ weeks_of_month:
+ description:
+ - The weeks of the month for the trigger.
+ - Can be a list or comma separated string of the numbers 1 to 4
+ representing the first to 4th week of the month.
+ - Optional when C(type=monthlydow).
+ type: str
+ months_of_year:
+ description:
+ - The months of the year for the trigger.
+ - Can be a list or comma separated string of full month names e.g.
+ march instead of mar.
+ - Optional when C(type) is C(monthlydow), C(monthly).
+ type: str
+ run_on_last_week_of_month:
+ description:
+ - Boolean value that sets whether the task runs on the last week of the
+ month.
+ - Optional when C(type) is C(monthlydow).
+ type: bool
+ run_on_last_day_of_month:
+ description:
+ - Boolean value that sets whether the task runs on the last day of the
+ month.
+ - Optional when C(type) is C(monthly).
+ type: bool
+ weeks_interval:
+ description:
+ - The interval of weeks to run on, e.g. C(1) means every week while
+ C(2) means every other week.
+ - Optional when C(type=weekly).
+ type: int
+ repetition:
+ description:
+ - Allows you to define the repetition action of the trigger that defines how often the task is run and how long the repetition pattern is repeated
+ after the task is started.
+ - It takes in the following keys, C(duration), C(interval), C(stop_at_duration_end)
+ suboptions:
+ duration:
+ description:
+ - Defines how long the pattern is repeated.
+ - The value is in the ISO 8601 Duration format C(P[n]Y[n]M[n]DT[n]H[n]M[n]S).
+ - By default this is not set which means it will repeat indefinitely.
+ type: str
+ interval:
+ description:
+ - The amount of time between each restart of the task.
+ - The value is written in the ISO 8601 Duration format C(P[n]Y[n]M[n]DT[n]H[n]M[n]S).
+ type: str
+ stop_at_duration_end:
+ description:
+ - Whether a running instance of the task is stopped at the end of the repetition pattern.
+ type: bool
+ state_change:
+ description:
+ - Allows you to define the kind of Terminal Server session change that triggers a task.
+ - Optional when C(type=session_state_change)
+ type: str
+ choices:
+ - console_connect
+ - console_disconnect
+ - remote_connect
+ - remote_disconnect
+ - session_lock
+ - session_unlock
+ version_added: 1.6.0
+
+ # Principal options
+ display_name:
+ description:
+ - The name of the user/group that is displayed in the Task Scheduler UI.
+ type: str
+ group:
+ description:
+ - The group that will run the task.
+ - C(group) and C(username) are exclusive to each other and cannot be set
+ at the same time.
+ - C(logon_type) can either be not set or equal C(group).
+ type: str
+ logon_type:
+ description:
+ - The logon method that the task will run with.
+ - C(password) means the password will be stored and the task has access
+ to network resources.
+ - C(s4u) means the existing token will be used to run the task and no
+ password will be stored with the task. Means no network or encrypted
+ files access.
+ - C(interactive_token) means the user must already be logged on
+ interactively and will run in an existing interactive session.
+ - C(group) means that the task will run as a group.
+ - C(service_account) means that a service account like System, Local
+ Service or Network Service will run the task.
+ type: str
+ choices: [ none, password, s4u, interactive_token, group, service_account, token_or_password ]
+ run_level:
+ description:
+ - The level of user rights used to run the task.
+ - If not specified the task will be created with limited rights.
+ type: str
+ choices: [ limited, highest ]
+ aliases: [ runlevel ]
+ username:
+ description:
+ - The user to run the scheduled task as.
+ - Will default to the current user under an interactive token if not
+ specified during creation.
+ - The user account specified must have the C(SeBatchLogonRight) logon right
+ which can be added with M(ansible.windows.win_user_right).
+ type: str
+ aliases: [ user ]
+ password:
+ description:
+ - The password for the user account to run the scheduled task as.
+ - This is required when running a task without the user being logged in,
+ excluding the builtin service accounts and Group Managed Service Accounts (gMSA).
+ - If set, will always result in a change unless C(update_password) is set
+ to C(no) and no other changes are required for the service.
+ type: str
+ update_password:
+ description:
+ - Whether to update the password even when not other changes have occurred.
+ - When C(yes) will always result in a change when executing the module.
+ type: bool
+ default: yes
+
+ # RegistrationInfo options
+ author:
+ description:
+ - The author of the task.
+ type: str
+ date:
+ description:
+ - The date when the task was registered.
+ type: str
+ description:
+ description:
+ - The description of the task.
+ type: str
+ source:
+ description:
+ - The source of the task.
+ type: str
+ version:
+ description:
+ - The version number of the task.
+ type: str
+
+ # Settings options
+ allow_demand_start:
+ description:
+ - Whether the task can be started by using either the Run command or the
+ Context menu.
+ type: bool
+ allow_hard_terminate:
+ description:
+ - Whether the task can be terminated by using TerminateProcess.
+ type: bool
+ compatibility:
+ description:
+ - The integer value with indicates which version of Task Scheduler a task
+ is compatible with.
+ - C(0) means the task is compatible with the AT command.
+ - C(1) means the task is compatible with Task Scheduler 1.0(Windows Vista, Windows Server 2008 and older).
+ - C(2) means the task is compatible with Task Scheduler 2.0(Windows Vista, Windows Server 2008).
+ - C(3) means the task is compatible with Task Scheduler 2.0(Windows 7, Windows Server 2008 R2).
+ - C(4) means the task is compatible with Task Scheduler 2.0(Windows 10, Windows Server 2016, Windows Server 2019).
+ type: int
+ choices: [ 0, 1, 2, 3, 4 ]
+ delete_expired_task_after:
+ description:
+ - The amount of time that the Task Scheduler will wait before deleting the
+ task after it expires.
+ - A task expires after the end_boundary has been exceeded for all triggers
+ associated with the task.
+ - This is in the ISO 8601 Duration format C(P[n]Y[n]M[n]DT[n]H[n]M[n]S).
+ type: str
+ disallow_start_if_on_batteries:
+ description:
+ - Whether the task will not be started if the computer is running on
+ battery power.
+ type: bool
+ enabled:
+ description:
+ - Whether the task is enabled, the task can only run when C(yes).
+ type: bool
+ execution_time_limit:
+ description:
+ - The amount of time allowed to complete the task.
+ - When set to C(PT0S), the time limit is infinite.
+ - When omitted, the default time limit is 72 hours.
+ - This is in the ISO 8601 Duration format C(P[n]Y[n]M[n]DT[n]H[n]M[n]S).
+ type: str
+ hidden:
+ description:
+ - Whether the task will be hidden in the UI.
+ type: bool
+ multiple_instances:
+ description:
+ - An integer that indicates the behaviour when starting a task that is
+ already running.
+ - C(0) will start a new instance in parallel with existing instances of
+ that task.
+ - C(1) will wait until other instances of that task to finish running
+ before starting itself.
+ - C(2) will not start a new instance if another is running.
+ - C(3) will stop other instances of the task and start the new one.
+ type: int
+ choices: [ 0, 1, 2, 3 ]
+ priority:
+ description:
+ - The priority level (0-10) of the task.
+ - When creating a new task the default is C(7).
+ - See U(https://msdn.microsoft.com/en-us/library/windows/desktop/aa383512.aspx)
+ for details on the priority levels.
+ type: int
+ restart_count:
+ description:
+ - The number of times that the Task Scheduler will attempt to restart the
+ task.
+ type: int
+ restart_interval:
+ description:
+ - How long the Task Scheduler will attempt to restart the task.
+ - If this is set then C(restart_count) must also be set.
+ - The maximum allowed time is 31 days.
+ - The minimum allowed time is 1 minute.
+ - This is in the ISO 8601 Duration format C(P[n]Y[n]M[n]DT[n]H[n]M[n]S).
+ type: str
+ run_only_if_idle:
+ description:
+ - Whether the task will run the task only if the computer is in an idle
+ state.
+ type: bool
+ run_only_if_network_available:
+ description:
+ - Whether the task will run only when a network is available.
+ type: bool
+ start_when_available:
+ description:
+ - Whether the task can start at any time after its scheduled time has
+ passed.
+ type: bool
+ stop_if_going_on_batteries:
+ description:
+ - Whether the task will be stopped if the computer begins to run on battery
+ power.
+ type: bool
+ wake_to_run:
+ description:
+ - Whether the task will wake the computer when it is time to run the task.
+ type: bool
+notes:
+- The option names and structure for actions and triggers of a service follow
+ the C(RegisteredTask) naming standard and requirements, it would be useful to
+ read up on this guide if coming across any issues U(https://msdn.microsoft.com/en-us/library/windows/desktop/aa382542.aspx).
+- A Group Managed Service Account (gMSA) can be used by setting C(logon_type) to C(password)
+ and omitting the password parameter. For more information on gMSAs,
+ see U(https://techcommunity.microsoft.com/t5/Core-Infrastructure-and-Security/Windows-Server-2012-Group-Managed-Service-Accounts/ba-p/255910)
+seealso:
+- module: community.windows.win_scheduled_task_stat
+- module: ansible.windows.win_user_right
+author:
+- Peter Mounce (@petemounce)
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = r'''
+- name: Create a task to open 2 command prompts as SYSTEM
+ community.windows.win_scheduled_task:
+ name: TaskName
+ description: open command prompt
+ actions:
+ - path: cmd.exe
+ arguments: /c hostname
+ - path: cmd.exe
+ arguments: /c whoami
+ triggers:
+ - type: daily
+ start_boundary: '2017-10-09T09:00:00'
+ username: SYSTEM
+ state: present
+ enabled: yes
+
+- name: Create task to run a PS script as NETWORK service on boot
+ community.windows.win_scheduled_task:
+ name: TaskName2
+ description: Run a PowerShell script
+ actions:
+ - path: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
+ arguments: -ExecutionPolicy Unrestricted -NonInteractive -File C:\TestDir\Test.ps1
+ triggers:
+ - type: boot
+ username: NETWORK SERVICE
+ run_level: highest
+ state: present
+
+- name: Update Local Security Policy to allow users to run scheduled tasks
+ ansible.windows.win_user_right:
+ name: SeBatchLogonRight
+ users:
+ - LocalUser
+ - DOMAIN\NetworkUser
+ action: add
+
+- name: Change above task to run under a domain user account, storing the passwords
+ community.windows.win_scheduled_task:
+ name: TaskName2
+ username: DOMAIN\User
+ password: Password
+ logon_type: password
+
+- name: Change the above task again, choosing not to store the password
+ community.windows.win_scheduled_task:
+ name: TaskName2
+ username: DOMAIN\User
+ logon_type: s4u
+
+- name: Change above task to use a gMSA, where the password is managed automatically
+ community.windows.win_scheduled_task:
+ name: TaskName2
+ username: DOMAIN\gMsaSvcAcct$
+ logon_type: password
+
+- name: Create task with multiple triggers
+ community.windows.win_scheduled_task:
+ name: TriggerTask
+ path: \Custom
+ actions:
+ - path: cmd.exe
+ triggers:
+ - type: daily
+ - type: monthlydow
+ username: SYSTEM
+
+- name: Set logon type to password but don't force update the password
+ community.windows.win_scheduled_task:
+ name: TriggerTask
+ path: \Custom
+ actions:
+ - path: cmd.exe
+ username: Administrator
+ password: password
+ update_password: no
+
+- name: Disable a task that already exists
+ community.windows.win_scheduled_task:
+ name: TaskToDisable
+ enabled: no
+
+- name: Create a task that will be repeated every minute for five minutes
+ community.windows.win_scheduled_task:
+ name: RepeatedTask
+ description: open command prompt
+ actions:
+ - path: cmd.exe
+ arguments: /c hostname
+ triggers:
+ - type: registration
+ repetition:
+ interval: PT1M
+ duration: PT5M
+ stop_at_duration_end: yes
+
+- name: Create task to run a PS script in Windows 10 compatibility on boot with a delay of 1min
+ community.windows.win_scheduled_task:
+ name: TriggerTask
+ path: \Custom
+ actions:
+ - path: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
+ arguments: -ExecutionPolicy Unrestricted -NonInteractive -File C:\TestDir\Test.ps1
+ triggers:
+ - type: boot
+ delay: PT1M
+ username: SYSTEM
+ compatibility: 4
+'''
+
+RETURN = r'''
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_scheduled_task_stat.ps1 b/ansible_collections/community/windows/plugins/modules/win_scheduled_task_stat.ps1
new file mode 100644
index 00000000..eb119ae0
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_scheduled_task_stat.ps1
@@ -0,0 +1,398 @@
+#!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 -PowerShell Ansible.ModuleUtils.AddType
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#Requires -Module Ansible.ModuleUtils.CamelConversion
+
+$spec = @{
+ options = @{
+ path = @{ type = "str"; default = "\" }
+ name = @{ type = "str" }
+ }
+ supports_check_mode = $true
+}
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$path = $module.Params.path
+$name = $module.Params.name
+
+Function ConvertTo-NormalizedUser {
+ [CmdletBinding()]
+ param (
+ [Parameter(Mandatory = $true)]
+ [String]$InputObject
+ )
+
+ $candidates = @(if ($InputObject.Contains('\')) {
+ $nameSplit = $InputObject.Split('\', 2)
+
+ if ($nameSplit[0] -eq '.') {
+ # If the domain portion is . try using the hostname then falling back to just the username.
+ # Usually the hostname just works except when running on a DC where it's a domain account
+ # where looking up just the username should work.
+ , @($env:COMPUTERNAME, $nameSplit[1])
+ $nameSplit[1]
+ }
+ else {
+ , $nameSplit
+ }
+ }
+ else {
+ $InputObject
+ })
+
+ $sid = for ($i = 0; $i -lt $candidates.Length; $i++) {
+ $candidate = $candidates[$i]
+ $ntAccount = New-Object -TypeName System.Security.Principal.NTAccount -ArgumentList $candidate
+ try {
+ $ntAccount.Translate([System.Security.Principal.SecurityIdentifier])
+ break
+ }
+ catch [System.Security.Principal.IdentityNotMappedException] {
+ if ($i -eq ($candidates.Length - 1)) {
+ throw
+ }
+ continue
+ }
+ }
+
+ $sid.Translate([System.Security.Principal.NTAccount]).Value
+}
+
+Add-CSharpType -AnsibleModule $module -References @'
+public enum TASK_ACTION_TYPE
+{
+ TASK_ACTION_EXEC = 0,
+ // The below are not supported and are only kept for documentation purposes
+ TASK_ACTION_COM_HANDLER = 5,
+ TASK_ACTION_SEND_EMAIL = 6,
+ TASK_ACTION_SHOW_MESSAGE = 7
+}
+
+public enum TASK_LOGON_TYPE
+{
+ TASK_LOGON_NONE = 0,
+ TASK_LOGON_PASSWORD = 1,
+ TASK_LOGON_S4U = 2,
+ TASK_LOGON_INTERACTIVE_TOKEN = 3,
+ TASK_LOGON_GROUP = 4,
+ TASK_LOGON_SERVICE_ACCOUNT = 5,
+ TASK_LOGON_INTERACTIVE_TOKEN_OR_PASSWORD = 6
+}
+
+public enum TASK_RUN_LEVEL
+{
+ TASK_RUNLEVEL_LUA = 0,
+ TASK_RUNLEVEL_HIGHEST = 1
+}
+
+public enum TASK_STATE
+{
+ TASK_STATE_UNKNOWN = 0,
+ TASK_STATE_DISABLED = 1,
+ TASK_STATE_QUEUED = 2,
+ TASK_STATE_READY = 3,
+ TASK_STATE_RUNNING = 4
+}
+
+public enum TASK_TRIGGER_TYPE2
+{
+ TASK_TRIGGER_EVENT = 0,
+ TASK_TRIGGER_TIME = 1,
+ TASK_TRIGGER_DAILY = 2,
+ TASK_TRIGGER_WEEKLY = 3,
+ TASK_TRIGGER_MONTHLY = 4,
+ TASK_TRIGGER_MONTHLYDOW = 5,
+ TASK_TRIGGER_IDLE = 6,
+ TASK_TRIGGER_REGISTRATION = 7,
+ TASK_TRIGGER_BOOT = 8,
+ TASK_TRIGGER_LOGON = 9,
+ TASK_TRIGGER_SESSION_STATE_CHANGE = 11
+}
+
+public enum TASK_SESSION_STATE_CHANGE_TYPE
+{
+ TASK_CONSOLE_CONNECT = 1,
+ TASK_CONSOLE_DISCONNECT = 2,
+ TASK_REMOTE_CONNECT = 3,
+ TASK_REMOTE_DISCONNECT = 4,
+ TASK_SESSION_LOCK = 7,
+ TASK_SESSION_UNLOCK = 8
+}
+'@
+
+Function Get-PropertyValue($task_property, $com, $property) {
+ $raw_value = $com.$property
+
+ if ($null -eq $raw_value) {
+ return $null
+ }
+ elseif ($raw_value.GetType().Name -eq "__ComObject") {
+ $com_values = @{}
+ Get-Member -InputObject $raw_value -MemberType Property | ForEach-Object {
+ $com_value = Get-PropertyValue -task_property $property -com $raw_value -property $_.Name
+ $com_values.$($_.Name) = $com_value
+ }
+
+ return , $com_values
+ }
+
+ switch ($property) {
+ DaysOfWeek {
+ $value_list = @()
+ $map = @(
+ @{ day = "sunday"; bitwise = 0x01 }
+ @{ day = "monday"; bitwise = 0x02 }
+ @{ day = "tuesday"; bitwise = 0x04 }
+ @{ day = "wednesday"; bitwise = 0x08 }
+ @{ day = "thursday"; bitwise = 0x10 }
+ @{ day = "friday"; bitwise = 0x20 }
+ @{ day = "saturday"; bitwise = 0x40 }
+ )
+ foreach ($entry in $map) {
+ $day = $entry.day
+ $bitwise = $entry.bitwise
+ if ($raw_value -band $bitwise) {
+ $value_list += $day
+ }
+ }
+
+ $value = $value_list -join ","
+ break
+ }
+ DaysOfMonth {
+ $value_list = @()
+ $map = @(
+ @{ day = "1"; bitwise = 0x01 }
+ @{ day = "2"; bitwise = 0x02 }
+ @{ day = "3"; bitwise = 0x04 }
+ @{ day = "4"; bitwise = 0x08 }
+ @{ day = "5"; bitwise = 0x10 }
+ @{ day = "6"; bitwise = 0x20 }
+ @{ day = "7"; bitwise = 0x40 }
+ @{ day = "8"; bitwise = 0x80 }
+ @{ day = "9"; bitwise = 0x100 }
+ @{ day = "10"; bitwise = 0x200 }
+ @{ day = "11"; bitwise = 0x400 }
+ @{ day = "12"; bitwise = 0x800 }
+ @{ day = "13"; bitwise = 0x1000 }
+ @{ day = "14"; bitwise = 0x2000 }
+ @{ day = "15"; bitwise = 0x4000 }
+ @{ day = "16"; bitwise = 0x8000 }
+ @{ day = "17"; bitwise = 0x10000 }
+ @{ day = "18"; bitwise = 0x20000 }
+ @{ day = "19"; bitwise = 0x40000 }
+ @{ day = "20"; bitwise = 0x80000 }
+ @{ day = "21"; bitwise = 0x100000 }
+ @{ day = "22"; bitwise = 0x200000 }
+ @{ day = "23"; bitwise = 0x400000 }
+ @{ day = "24"; bitwise = 0x800000 }
+ @{ day = "25"; bitwise = 0x1000000 }
+ @{ day = "26"; bitwise = 0x2000000 }
+ @{ day = "27"; bitwise = 0x4000000 }
+ @{ day = "28"; bitwise = 0x8000000 }
+ @{ day = "29"; bitwise = 0x10000000 }
+ @{ day = "30"; bitwise = 0x20000000 }
+ @{ day = "31"; bitwise = 0x40000000 }
+ )
+
+ foreach ($entry in $map) {
+ $day = $entry.day
+ $bitwise = $entry.bitwise
+ if ($raw_value -band $bitwise) {
+ $value_list += $day
+ }
+ }
+
+ $value = $value_list -join ","
+ break
+ }
+ WeeksOfMonth {
+ $value_list = @()
+ $map = @(
+ @{ week = "1"; bitwise = 0x01 }
+ @{ week = "2"; bitwise = 0x02 }
+ @{ week = "3"; bitwise = 0x04 }
+ @{ week = "4"; bitwise = 0x04 }
+ )
+
+ foreach ($entry in $map) {
+ $week = $entry.week
+ $bitwise = $entry.bitwise
+ if ($raw_value -band $bitwise) {
+ $value_list += $week
+ }
+ }
+
+ $value = $value_list -join ","
+ break
+ }
+ MonthsOfYear {
+ $value_list = @()
+ $map = @(
+ @{ month = "january"; bitwise = 0x01 }
+ @{ month = "february"; bitwise = 0x02 }
+ @{ month = "march"; bitwise = 0x04 }
+ @{ month = "april"; bitwise = 0x08 }
+ @{ month = "may"; bitwise = 0x10 }
+ @{ month = "june"; bitwise = 0x20 }
+ @{ month = "july"; bitwise = 0x40 }
+ @{ month = "august"; bitwise = 0x80 }
+ @{ month = "september"; bitwise = 0x100 }
+ @{ month = "october"; bitwise = 0x200 }
+ @{ month = "november"; bitwise = 0x400 }
+ @{ month = "december"; bitwise = 0x800 }
+ )
+
+ foreach ($entry in $map) {
+ $month = $entry.month
+ $bitwise = $entry.bitwise
+ if ($raw_value -band $bitwise) {
+ $value_list += $month
+ }
+ }
+
+ $value = $value_list -join ","
+ break
+ }
+ Type {
+ if ($task_property -eq "actions") {
+ $value = [Enum]::ToObject([TASK_ACTION_TYPE], $raw_value).ToString()
+ }
+ elseif ($task_property -eq "triggers") {
+ $value = [Enum]::ToObject([TASK_TRIGGER_TYPE2], $raw_value).ToString()
+ }
+ break
+ }
+ RunLevel {
+ $value = [Enum]::ToObject([TASK_RUN_LEVEL], $raw_value).ToString()
+ break
+ }
+ LogonType {
+ $value = [Enum]::ToObject([TASK_LOGON_TYPE], $raw_value).ToString()
+ break
+ }
+ UserId {
+ $value = ConvertTo-NormalizedUser -InputObject $raw_value
+ }
+ GroupId {
+ $value = ConvertTo-NormalizedUser -InputObject $raw_value
+ }
+ default {
+ $value = $raw_value
+ break
+ }
+ }
+
+ return , $value
+}
+
+$service = New-Object -ComObject Schedule.Service
+try {
+ $service.Connect()
+}
+catch {
+ $module.FailJson("failed to connect to the task scheduler service: $($_.Exception.Message)", $_)
+}
+
+try {
+ $task_folder = $service.GetFolder($path)
+ $module.Result.folder_exists = $true
+}
+catch {
+ $module.Result.folder_exists = $false
+ if ($null -ne $name) {
+ $module.Result.task_exists = $false
+ }
+ $module.ExitJson()
+}
+
+$folder_tasks = $task_folder.GetTasks(1)
+$folder_task_names = @()
+$folder_task_count = 0
+$task = $null
+for ($i = 1; $i -le $folder_tasks.Count; $i++) {
+ $task_name = $folder_tasks.Item($i).Name
+ $folder_task_names += $task_name
+ $folder_task_count += 1
+
+ if ($null -ne $name -and $task_name -eq $name) {
+ $task = $folder_tasks.Item($i)
+ }
+}
+$module.Result.folder_task_names = $folder_task_names
+$module.Result.folder_task_count = $folder_task_count
+
+if ($null -ne $name) {
+ if ($null -ne $task) {
+ $module.Result.task_exists = $true
+
+ # task state
+ $module.Result.state = @{
+ last_run_time = (Get-Date $task.LastRunTime -Format s)
+ last_task_result = $task.LastTaskResult
+ next_run_time = (Get-Date $task.NextRunTime -Format s)
+ number_of_missed_runs = $task.NumberOfMissedRuns
+ status = [Enum]::ToObject([TASK_STATE], $task.State).ToString()
+ }
+
+ # task definition
+ $task_definition = $task.Definition
+ $ignored_properties = @("XmlText")
+ $properties = @("principal", "registration_info", "settings")
+ $collection_properties = @("actions", "triggers")
+
+ foreach ($property in $properties) {
+ $property_name = $property -replace "_"
+ $module.Result.$property = @{}
+ $values = $task_definition.$property_name
+ Get-Member -InputObject $values -MemberType Property | ForEach-Object {
+ if ($_.Name -notin $ignored_properties) {
+ $module.Result.$property.$($_.Name) = (Get-PropertyValue -task_property $property -com $values -property $_.Name)
+ }
+ }
+ }
+
+ foreach ($property in $collection_properties) {
+ $module.Result.$property = @()
+ $collection = $task_definition.$property
+ $collection_count = $collection.Count
+ for ($i = 1; $i -le $collection_count; $i++) {
+ $item = $collection.Item($i)
+ $item_info = @{}
+
+ Get-Member -InputObject $item -MemberType Property | ForEach-Object {
+ if ($_.Name -notin $ignored_properties) {
+ $value = (Get-PropertyValue -task_property $property -com $item -property $_.Name)
+ $item_info.$($_.Name) = $value
+
+ # This was added after StateChange was represented by the raw enum value so we include both
+ # for backwards compatibility.
+ if ($_.Name -eq 'StateChange') {
+ $item_info.StateChangeStr = if ($value) {
+ [Enum]::ToObject([TASK_SESSION_STATE_CHANGE_TYPE], $value).ToString()
+ }
+ }
+ }
+ }
+ $module.Result.$property += $item_info
+ }
+ }
+ }
+ else {
+ $module.Result.task_exists = $false
+ }
+}
+
+# Convert-DictToSnakeCase returns a Hashtable but Ansible.Basic expect a Dictionary. This is a hack until the snake
+# conversion code has been moved to this collection and updated to handle this.
+$new_result = [System.Collections.Generic.Dictionary[[String], [Object]]]@{}
+foreach ($kvp in (Convert-DictToSnakeCase -dict $module.Result).GetEnumerator()) {
+ $new_result[$kvp.Name] = $kvp.Value
+}
+$module.Result = $new_result
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_scheduled_task_stat.py b/ansible_collections/community/windows/plugins/modules/win_scheduled_task_stat.py
new file mode 100644
index 00000000..f1922967
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_scheduled_task_stat.py
@@ -0,0 +1,371 @@
+#!/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)
+
+DOCUMENTATION = r'''
+---
+module: win_scheduled_task_stat
+short_description: Get information about Windows Scheduled Tasks
+description:
+- Will return whether the folder and task exists.
+- Returns the names of tasks in the folder specified.
+- Use M(community.windows.win_scheduled_task) to configure a scheduled task.
+options:
+ path:
+ description: The folder path where the task lives.
+ type: str
+ default: \
+ name:
+ description:
+ - The name of the scheduled task to get information for.
+ - If C(name) is set and exists, will return information on the task itself.
+ type: str
+seealso:
+- module: community.windows.win_scheduled_task
+author:
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = r'''
+- name: Get information about a folder
+ community.windows.win_scheduled_task_stat:
+ path: \folder name
+ register: task_folder_stat
+
+- name: Get information about a task in the root folder
+ community.windows.win_scheduled_task_stat:
+ name: task name
+ register: task_stat
+
+- name: Get information about a task in a custom folder
+ community.windows.win_scheduled_task_stat:
+ path: \folder name
+ name: task name
+ register: task_stat
+'''
+
+RETURN = r'''
+actions:
+ description: A list of actions.
+ returned: name is specified and task exists
+ type: list
+ sample: [
+ {
+ "Arguments": "/c echo hi",
+ "Id": null,
+ "Path": "cmd.exe",
+ "Type": "TASK_ACTION_EXEC",
+ "WorkingDirectory": null
+ }
+ ]
+folder_exists:
+ description: Whether the folder set at path exists.
+ returned: always
+ type: bool
+ sample: true
+folder_task_count:
+ description: The number of tasks that exist in the folder.
+ returned: always
+ type: int
+ sample: 2
+folder_task_names:
+ description: A list of tasks that exist in the folder.
+ returned: always
+ type: list
+ sample: [ 'Task 1', 'Task 2' ]
+principal:
+ description: Details on the principal configured to run the task.
+ returned: name is specified and task exists
+ type: complex
+ contains:
+ display_name:
+ description: The name of the user/group that is displayed in the Task
+ Scheduler UI.
+ returned: ''
+ type: str
+ sample: Administrator
+ group_id:
+ description: The group that will run the task.
+ returned: ''
+ type: str
+ sample: BUILTIN\Administrators
+ id:
+ description: The ID for the principal.
+ returned: ''
+ type: str
+ sample: Author
+ logon_type:
+ description: The logon method that the task will run with.
+ returned: ''
+ type: str
+ sample: TASK_LOGON_INTERACTIVE_TOKEN
+ run_level:
+ description: The level of user rights used to run the task.
+ returned: ''
+ type: str
+ sample: TASK_RUNLEVEL_LUA
+ user_id:
+ description: The user that will run the task.
+ returned: ''
+ type: str
+ sample: SERVER\Administrator
+registration_info:
+ description: Details on the task registration info.
+ returned: name is specified and task exists
+ type: complex
+ contains:
+ author:
+ description: The author os the task.
+ returned: ''
+ type: str
+ sample: SERVER\Administrator
+ date:
+ description: The date when the task was register.
+ returned: ''
+ type: str
+ sample: '2017-01-01T10:00:00'
+ description:
+ description: The description of the task.
+ returned: ''
+ type: str
+ sample: task description
+ documentation:
+ description: The documentation of the task.
+ returned: ''
+ type: str
+ sample: task documentation
+ security_descriptor:
+ description: The security descriptor of the task.
+ returned: ''
+ type: str
+ sample: security descriptor
+ source:
+ description: The source of the task.
+ returned: ''
+ type: str
+ sample: source
+ uri:
+ description: The URI/path of the task.
+ returned: ''
+ type: str
+ sample: \task\task name
+ version:
+ description: The version of the task.
+ returned: ''
+ type: str
+ sample: 1.0
+settings:
+ description: Details on the task settings.
+ returned: name is specified and task exists
+ type: complex
+ contains:
+ allow_demand_start:
+ description: Whether the task can be started by using either the Run
+ command of the Context menu.
+ returned: ''
+ type: bool
+ sample: true
+ allow_hard_terminate:
+ description: Whether the task can terminated by using TerminateProcess.
+ returned: ''
+ type: bool
+ sample: true
+ compatibility:
+ description: The compatibility level of the task
+ returned: ''
+ type: int
+ sample: 2
+ delete_expired_task_after:
+ description: The amount of time the Task Scheduler will wait before
+ deleting the task after it expires.
+ returned: ''
+ type: str
+ sample: PT10M
+ disallow_start_if_on_batteries:
+ description: Whether the task will not be started if the computer is
+ running on battery power.
+ returned: ''
+ type: bool
+ sample: false
+ disallow_start_on_remote_app_session:
+ description: Whether the task will not be started when in a remote app
+ session.
+ returned: ''
+ type: bool
+ sample: true
+ enabled:
+ description: Whether the task is enabled.
+ returned: ''
+ type: bool
+ sample: true
+ execution_time_limit:
+ description: The amount of time allowed to complete the task.
+ returned: ''
+ type: str
+ sample: PT72H
+ hidden:
+ description: Whether the task is hidden in the UI.
+ returned: ''
+ type: bool
+ sample: false
+ idle_settings:
+ description: The idle settings of the task.
+ returned: ''
+ type: dict
+ sample: {
+ "idle_duration": "PT10M",
+ "restart_on_idle": false,
+ "stop_on_idle_end": true,
+ "wait_timeout": "PT1H"
+ }
+ maintenance_settings:
+ description: The maintenance settings of the task.
+ returned: ''
+ type: str
+ sample: null
+ mulitple_instances:
+ description: Indicates the behaviour when starting a task that is already
+ running.
+ returned: ''
+ type: int
+ sample: 2
+ network_settings:
+ description: The network settings of the task.
+ returned: ''
+ type: dict
+ sample: {
+ "id": null,
+ "name": null
+ }
+ priority:
+ description: The priority level of the task.
+ returned: ''
+ type: int
+ sample: 7
+ restart_count:
+ description: The number of times that the task will attempt to restart
+ on failures.
+ returned: ''
+ type: int
+ sample: 0
+ restart_interval:
+ description: How long the Task Scheduler will attempt to restart the
+ task.
+ returned: ''
+ type: str
+ sample: PT15M
+ run_only_id_idle:
+ description: Whether the task will run if the computer is in an idle
+ state.
+ returned: ''
+ type: bool
+ sample: true
+ run_only_if_network_available:
+ description: Whether the task will run only when a network is available.
+ returned: ''
+ type: bool
+ sample: false
+ start_when_available:
+ description: Whether the task can start at any time after its scheduled
+ time has passed.
+ returned: ''
+ type: bool
+ sample: false
+ stop_if_going_on_batteries:
+ description: Whether the task will be stopped if the computer begins to
+ run on battery power.
+ returned: ''
+ type: bool
+ sample: true
+ use_unified_scheduling_engine:
+ description: Whether the task will use the unified scheduling engine.
+ returned: ''
+ type: bool
+ sample: false
+ volatile:
+ description: Whether the task is volatile.
+ returned: ''
+ type: bool
+ sample: false
+ wake_to_run:
+ description: Whether the task will wake the computer when it is time to
+ run the task.
+ returned: ''
+ type: bool
+ sample: false
+state:
+ description: Details on the state of the task
+ returned: name is specified and task exists
+ type: complex
+ contains:
+ last_run_time:
+ description: The time the registered task was last run.
+ returned: ''
+ type: str
+ sample: '2017-09-20T20:50:00'
+ last_task_result:
+ description: The results that were returned the last time the task was
+ run.
+ returned: ''
+ type: int
+ sample: 267009
+ next_run_time:
+ description: The time when the task is next scheduled to run.
+ returned: ''
+ type: str
+ sample: '2017-09-20T22:50:00'
+ number_of_missed_runs:
+ description: The number of times a task has missed a scheduled run.
+ returned: ''
+ type: int
+ sample: 1
+ status:
+ description: The status of the task, whether it is running, stopped, etc.
+ returned: ''
+ type: str
+ sample: TASK_STATE_RUNNING
+task_exists:
+ description: Whether the task at the folder exists.
+ returned: name is specified
+ type: bool
+ sample: true
+triggers:
+ description: A list of triggers.
+ returned: name is specified and task exists
+ type: list
+ sample: [
+ {
+ "delay": "PT15M",
+ "enabled": true,
+ "end_boundary": null,
+ "execution_time_limit": null,
+ "id": null,
+ "repetition": {
+ "duration": null,
+ "interval": null,
+ "stop_at_duration_end": false
+ },
+ "start_boundary": null,
+ "type": "TASK_TRIGGER_BOOT"
+ },
+ {
+ "days_of_month": "5,15,30",
+ "enabled": true,
+ "end_boundary": null,
+ "execution_time_limit": null,
+ "id": null,
+ "months_of_year": "june,december",
+ "random_delay": null,
+ "repetition": {
+ "duration": null,
+ "interval": null,
+ "stop_at_duration_end": false
+ },
+ "run_on_last_day_of_month": true,
+ "start_boundary": "2017-09-20T03:44:38",
+ "type": "TASK_TRIGGER_MONTHLY"
+ }
+ ]
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_scoop.ps1 b/ansible_collections/community/windows/plugins/modules/win_scoop.ps1
new file mode 100644
index 00000000..e8383c82
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_scoop.ps1
@@ -0,0 +1,285 @@
+#!powershell
+
+# Copyright: (c) 2020, Jamie Magee <jamie.magee@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.ArgvParser
+#Requires -Module Ansible.ModuleUtils.CommandUtil
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+$spec = @{
+ options = @{
+ architecture = @{ type = "str"; choices = "32bit", "64bit"; aliases = @(, "arch") }
+ independent = @{ type = "bool"; default = $false }
+ global = @{ type = "bool"; default = $false }
+ name = @{ type = "list"; elements = "str"; required = $true }
+ no_cache = @{ type = "bool"; default = $false }
+ purge = @{ type = "bool"; default = $false }
+ skip_checksum = @{ type = "bool"; default = $false }
+ state = @{ type = "str"; default = "present"; choices = "present", "absent" }
+ }
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$architecture = $module.Params.architecture
+$independent = $module.Params.independent
+$global = $module.Params.global
+$name = $module.Params.name
+$no_cache = $module.Params.no_cache
+$purge = $module.Params.purge
+$skip_checksum = $module.Params.skip_checksum
+$state = $module.Params.state
+
+$module.Result.rc = 0
+
+function Install-Scoop {
+ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingInvokeExpression', '', Justification = 'This is one use case where we want to use iex')]
+ [CmdletBinding()]
+ param ()
+
+ # Scoop doesn't have refreshenv like Chocolatey
+ # Let's try to update PATH first
+ $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
+
+ $scoop_app = Get-Command -Name scoop.ps1 -Type ExternalScript -ErrorAction SilentlyContinue
+ if ($module.CheckMode -and $null -eq $scoop_app) {
+ $module.Result.skipped = $true
+ $module.Result.msg = "Skipped check mode run on win_scoop as scoop.ps1 cannot be found on the system"
+ $module.ExitJson()
+ }
+ elseif ($null -eq $scoop_app) {
+ # We need to install scoop
+ # We run this in a separate process to make it easier to get the result in a failure and capture any output that
+ # might be sent to the host. We also need to enable TLS 1.2 in that process and not here so it can download the
+ # install script and other components.
+ $install_script = {
+ # Enable TLS1.2 if it's available but disabled (eg. .NET 4.5)
+ $security_protocols = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::SystemDefault
+ if ([Net.SecurityProtocolType].GetMember("Tls12").Count -gt 0) {
+ $security_protocols = $security_protcols -bor [Net.SecurityProtocolType]::Tls12
+ }
+ [Net.ServicePointManager]::SecurityProtocol = $security_protocols
+
+ $script = (New-Object System.Net.WebClient).DownloadString('https://get.scoop.sh')
+ $installer = [ScriptBlock]::Create($script)
+ $params = @{}
+
+ $current_user = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
+ if ($current_user.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
+ $params.RunAsAdmin = $true
+ }
+ . $installer -RunAsAdmin
+ }
+
+ $enc_command = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($install_script.ToString()))
+ $cmd = "powershell.exe -NoProfile -NoLogo -EncodedCommand $enc_command"
+
+ # Scoops installer does weird things and the exit code will most likely be 0. Use the presence of the scoop
+ # command as the indicator as to whether it was installed or not.
+ $res = Run-Command -Command $cmd -environment $environment
+
+ # Refresh PATH
+ $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
+
+ # locate the newly installed scoop.ps1
+ $scoop_app = Get-Command -Name scoop.ps1 -Type ExternalScript -ErrorAction SilentlyContinue
+
+ if ($null -eq $scoop_app -or -not (Test-Path -LiteralPath $scoop_app.Path)) {
+ $module.Result.rc = $res.rc
+ $module.Result.stdout = $res.stdout
+ $module.Result.stderr = $res.stderr
+ $module.FailJson("Failed to bootstrap scoop.ps1")
+ }
+
+ $module.Warn("Scoop was missing from this system, so it was installed during this task run.")
+ $module.Result.changed = $true
+ }
+
+ return $scoop_app.Path
+}
+
+function Get-ScoopPackage {
+ param(
+ [Parameter(Mandatory = $true)] [string]$scoop_path
+ )
+
+ $command = Argv-ToString -arguments @("powershell.exe", $scoop_path, "export")
+ $res = Run-Command -Command $command
+ if ($res.rc -ne 0) {
+ $module.Result.command = $command
+ $module.Result.rc = $res.rc
+ $module.Result.stdout = $res.stdout
+ $module.Result.stderr = $res.stderr
+ $module.FailJson("Error checking installed packages")
+ }
+
+ # Scoop since 0.2.3 output as JSON, older versions use a custom format
+ # https://github.com/ScoopInstaller/Scoop/commit/c4d1b9c22f943a810bed7f9f74d7d4d5c42d9a74
+ try {
+ $exportObj = ConvertFrom-Json -InputObject $res.stdout -ErrorAction Stop
+ }
+ catch {
+ $res.stdout -split "`n" |
+ Select-String '(.*?) \(v:(.*?)\)( \*global\*)? \[(.*?)\](\{32bit\})?' |
+ ForEach-Object {
+ [PSCustomObject]@{
+ Package = $_.Matches[0].Groups[1].Value
+ Version = $_.Matches[0].Groups[2].Value
+ Global = -not ([string]::IsNullOrWhiteSpace($_.Matches[0].Groups[3].Value))
+ Bucket = $_.Matches[0].Groups[4].Value
+ x86 = -not ([string]::IsNullOrWhiteSpace($_.Matches[0].Groups[5].Value))
+ }
+ }
+ return
+ }
+
+ $exportObj.apps | ForEach-Object {
+ $options = @($_.Info -split ',' | ForEach-Object Trim | Where-Object { $_ })
+ if ('Install failed' -in $options) {
+ return
+ }
+
+ [PSCustomObject]@{
+ Package = $_.Name
+ Version = $_.Version
+ Global = 'Global install' -in $options
+ Bucket = $_.Source
+ x86 = '32bit' -in $options
+ }
+ }
+}
+
+function Get-InstallScoopPackageArgument {
+ $arguments = [System.Collections.Generic.List[String]]@()
+
+ if ($architecture) {
+ $arguments.Add("--arch")
+ $arguments.Add($architecture)
+ }
+ if ($global) {
+ $arguments.Add("--global")
+ }
+ if ($independent) {
+ $arguments.Add("--independent")
+ }
+ if ($no_cache) {
+ $arguments.Add("--no-cache")
+ }
+ if ($skip_checksum) {
+ $arguments.Add("--skip")
+ }
+
+ return , $arguments
+}
+
+function Install-ScoopPackage {
+ param(
+ [Parameter(Mandatory = $true)] [string]$scoop_path,
+ [Parameter(Mandatory = $true)] [String[]]$packages
+ )
+ $arguments = [System.Collections.Generic.List[String]]@("powershell.exe", $scoop_path, "install")
+ $arguments.AddRange($packages)
+
+ $common_args = Get-InstallScoopPackageArgument
+ $arguments.AddRange($common_args)
+
+ $command = Argv-ToString -arguments $arguments
+
+ if (-not $module.CheckMode) {
+ $res = Run-Command -Command $command
+ if ($res.rc -ne 0) {
+ $module.Result.command = $command
+ $module.Result.rc = $res.rc
+ $module.Result.stdout = $res.stdout
+ $module.Result.stderr = $res.stderr
+ $module.FailJson("Error installing $packages")
+ }
+
+ if ($module.Verbosity -gt 1) {
+ $module.Result.stdout = $res.stdout
+ }
+ }
+ $module.Result.changed = $true
+}
+
+function Get-UninstallScoopPackageArgument {
+ $arguments = [System.Collections.Generic.List[String]]@()
+
+ if ($global) {
+ $arguments.Add("--global")
+ }
+ if ($purge) {
+ $arguments.Add("--purge")
+ }
+
+ return , $arguments
+}
+
+function Uninstall-ScoopPackage {
+ param(
+ [Parameter(Mandatory = $true)] [string]$scoop_path,
+ [Parameter(Mandatory = $true)] [String[]]$packages
+ )
+
+ $arguments = [System.Collections.Generic.List[String]]@("powershell.exe", $scoop_path, "uninstall")
+ $arguments.AddRange($packages)
+
+ $common_args = Get-UninstallScoopPackageArgument
+ $arguments.AddRange($common_args)
+
+ $command = Argv-ToString -arguments $arguments
+
+ if (-not $module.CheckMode) {
+ $res = Run-Command -Command $command
+ if ($res.rc -ne 0) {
+ $module.Result.command = $command
+ $module.Result.rc = $res.rc
+ $module.Result.stdout = $res.stdout
+ $module.Result.stderr = $res.stderr
+ $module.FailJson("Error uninstalling $packages")
+ }
+
+ if ($module.Verbosity -gt 1) {
+ $module.Result.stdout = $res.stdout
+ }
+ if (-not ($res.stdout -match "ERROR '(.*?)' isn't installed.")) {
+ $module.Result.changed = $true
+ }
+ }
+ else {
+ $module.Result.changed = $true
+ }
+}
+
+$scoop_path = Install-Scoop
+
+$installed_packages = @(Get-ScoopPackage -scoop_path $scoop_path)
+
+if ($state -in @("absent")) {
+ # Always attempt uninstall
+ # Packages can be in a broken state where they don't in appear scoop export
+ Uninstall-ScoopPackage -scoop_path $scoop_path -packages $name
+}
+
+if ($state -in @("present")) {
+ $missing_packages = foreach ($package in $name) {
+ if (
+ ($installed_packages.Package -notcontains $package) -or
+ (($installed_packages.Package -contains $package) -and (
+ ((($installed_packages | Where-Object { $_.Package -eq $package }).Global -contains $true) -and -not $global) -or
+ ((($installed_packages | Where-Object { $_.Package -eq $package }).Global -notcontains $true) -and $global)
+ )
+ )
+ ) {
+ $package
+ }
+ }
+}
+
+if ($missing_packages) {
+ Install-ScoopPackage -scoop_path $scoop_path -packages $missing_packages
+}
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_scoop.py b/ansible_collections/community/windows/plugins/modules/win_scoop.py
new file mode 100644
index 00000000..288e75e2
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_scoop.py
@@ -0,0 +1,79 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2020, Jamie Magee <jamie.magee@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_scoop
+short_description: Manage packages using Scoop
+description:
+- Manage packages using Scoop.
+- If Scoop is missing from the system, the module will install it.
+options:
+ architecture:
+ description:
+ - Force Scoop to install the package of a specific process architecture.
+ type: str
+ choices: [ 32bit, 64bit ]
+ aliases: [ arch ]
+ global:
+ description:
+ - Install the app globally
+ type: bool
+ default: no
+ independent:
+ description:
+ - Don't install dependencies automatically
+ type: bool
+ default: no
+ name:
+ description:
+ - Name of the package(s) to be installed.
+ type: list
+ elements: str
+ required: yes
+ no_cache:
+ description:
+ - Don't use the download cache
+ type: bool
+ default: no
+ purge:
+ description:
+ - Remove all persistent data
+ type: bool
+ default: no
+ skip_checksum:
+ description:
+ - Skip hash validation
+ type: bool
+ default: no
+ state:
+ description:
+ - State of the package on the system.
+ - When C(absent), will ensure the package is not installed.
+ - When C(present), will ensure the package is installed.
+ type: str
+ choices: [ absent, present ]
+ default: present
+seealso:
+- module: chocolatey.chocolatey.win_chocolatey
+- name: Scoop website
+ description: More information about Scoop
+ link: https://scoop.sh
+- name: Scoop installer repository
+ description: GitHub repository for the Scoop installer
+ link: https://github.com/lukesampson/scoop
+- name: Scoop main bucket
+ description: GitHub repository for the main bucket
+ link: https://github.com/ScoopInstaller/Main
+author:
+- Jamie Magee (@JamieMagee)
+'''
+
+EXAMPLES = r'''
+- name: Install jq.
+ community.windows.win_scoop:
+ name: jq
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_scoop_bucket.ps1 b/ansible_collections/community/windows/plugins/modules/win_scoop_bucket.ps1
new file mode 100644
index 00000000..70c02cb0
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_scoop_bucket.ps1
@@ -0,0 +1,125 @@
+#!powershell
+
+# Copyright: (c) 2020, Jamie Magee <jamie.magee@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+$spec = @{
+ options = @{
+ name = @{ type = "str"; required = $true }
+ repo = @{ type = "str" }
+ state = @{ type = "str"; default = "present"; choices = "present", "absent" }
+ }
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$name = $module.Params.name
+$repo = $module.Params.repo
+$state = $module.Params.state
+
+# Kept for backwards compatibility
+$module.Result.rc = 0
+
+function Get-Scoop {
+ # Scoop doesn't have refreshenv like Chocolatey
+ # Let's try to update PATH first
+ $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
+
+ $scoop_app = Get-Command -Name scoop.ps1 -Type ExternalScript -ErrorAction SilentlyContinue
+
+ if ($module.CheckMode -and $null -eq $scoop_app) {
+ $module.Result.skipped = $true
+ $module.Result.msg = "Skipped check mode run on win_scoop_bucket as scoop.ps1 cannot be found on the system"
+ $module.ExitJson()
+ }
+
+ if ($null -eq $scoop_app -or -not (Test-Path -LiteralPath $scoop_app.Path)) {
+ $module.FailJson("Failed to find scoop.ps1, make sure it is added to the PATH")
+ }
+
+ return $scoop_app.Path
+}
+
+function Get-ScoopBucket {
+ param(
+ [Parameter(Mandatory = $true)] [string]$scoop_path
+ )
+
+ &$scoop_path bucket list
+}
+
+function Uninstall-ScoopBucket {
+ param(
+ [Parameter(Mandatory = $true)] [string]$scoop_path,
+ [Parameter(Mandatory = $true)] [String]$bucket
+ )
+
+ $arguments = @(
+ "bucket",
+ "rm"
+ $bucket
+ if ($repo) { $repo }
+ )
+ if (-not $module.CheckMode) {
+ $res = (&$scoop_path @arguments) -join "`n"
+ if (-not $?) {
+ $module.Result.stdout = $res
+ $module.Result.rc = 1
+ $module.FailJson("Failed to remove scoop bucket, see stdout for more details")
+ }
+ elseif ($module.Verbosity -gt 1) {
+ $module.Result.stdout = $res
+ }
+ }
+
+ $module.Result.changed = $true
+}
+
+function Install-ScoopBucket {
+ param(
+ [Parameter(Mandatory = $true)] [string]$scoop_path,
+ [Parameter(Mandatory = $true)] [String]$bucket,
+ [String]$repo
+ )
+
+ $arguments = @(
+ "bucket"
+ "add"
+ $bucket
+ if ($repo) { $repo }
+ )
+ if (-not $module.CheckMode) {
+ $res = (&$scoop_path @arguments) -join "`n"
+ if (-not $?) {
+ $module.Result.stdout = $res
+ $module.Result.rc = 1
+ $module.FailJson("Failed to add scoop bucket, see stdout for more details")
+ }
+ elseif ($module.Verbosity -gt 1) {
+ $module.Result.stdout = $res
+ }
+ }
+
+ $module.Result.changed = $true
+}
+
+$scoop_path = Get-Scoop
+
+$installed_buckets = Get-ScoopBucket -scoop_path $scoop_path
+
+if ($state -in @("absent")) {
+ if ($installed_buckets.Name -contains $name) {
+ Uninstall-ScoopBucket -scoop_path $scoop_path -bucket $name
+ }
+}
+
+if ($state -in @("present")) {
+ if ($installed_buckets.Name -notcontains $name) {
+ Install-ScoopBucket -scoop_path $scoop_path -bucket $name -repo $repo
+ }
+}
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_scoop_bucket.py b/ansible_collections/community/windows/plugins/modules/win_scoop_bucket.py
new file mode 100644
index 00000000..57bb9464
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_scoop_bucket.py
@@ -0,0 +1,73 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2020, Jamie Magee <jamie.magee@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_scoop_bucket
+version_added: 1.0.0
+short_description: Manage Scoop buckets
+description:
+- Manage Scoop buckets
+requirements:
+- git
+options:
+ name:
+ description:
+ - Name of the Scoop bucket.
+ type: str
+ required: yes
+ repo:
+ description:
+ - Git repository that contains the scoop bucket
+ type: str
+ state:
+ description:
+ - State of the Scoop bucket.
+ - When C(absent), will ensure the package is not installed.
+ - When C(present), will ensure the package is installed.
+ type: str
+ choices: [ absent, present ]
+ default: present
+seealso:
+- module: community.windows.win_scoop
+- name: Scoop website
+ description: More information about Scoop
+ link: https://scoop.sh
+- name: Scoop directory
+ description: A directory of buckets for the scoop package manager for Windows
+ link: https://rasa.github.io/scoop-directory/
+author:
+- Jamie Magee (@JamieMagee)
+'''
+
+EXAMPLES = r'''
+- name: Add the extras bucket
+ community.windows.win_scoop_bucket:
+ name: extras
+
+- name: Remove the versions bucket
+ community.windows.win_scoop_bucket:
+ name: versions
+ state: absent
+
+- name: Add a custom bucket
+ community.windows.win_scoop_bucket:
+ name: my-bucket
+ repo: https://github.com/example/my-bucket
+'''
+
+RETURN = r'''
+rc:
+ description: The result code of the scoop action
+ returned: always
+ type: int
+ sample: 0
+stdout:
+ description: The raw output from the scoop action
+ returned: on failure or when verbosity is greater than 1
+ type: str
+ sample: The test bucket was added successfully.
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_security_policy.ps1 b/ansible_collections/community/windows/plugins/modules/win_security_policy.ps1
new file mode 100644
index 00000000..dd7016fe
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_security_policy.ps1
@@ -0,0 +1,224 @@
+#!powershell
+
+# Copyright: (c) 2017, Jordan Borean <jborean93@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+$ErrorActionPreference = 'Stop'
+
+$params = Parse-Args $args -supports_check_mode $true
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
+$diff_mode = Get-AnsibleParam -obj $Params -name "_ansible_diff" -type "bool" -default $false
+
+$section = Get-AnsibleParam -obj $params -name "section" -type "str" -failifempty $true
+$key = Get-AnsibleParam -obj $params -name "key" -type "str" -failifempty $true
+$value = Get-AnsibleParam -obj $params -name "value" -failifempty $true
+
+$result = @{
+ changed = $false
+ section = $section
+ key = $key
+ value = $value
+}
+
+if ($diff_mode) {
+ $result.diff = @{}
+}
+
+Function Invoke-SecEdit($arguments) {
+ $stdout = $null
+ $stderr = $null
+ $log_path = [IO.Path]::GetTempFileName()
+ $arguments = $arguments + @("/log", $log_path)
+
+ try {
+ $stdout = &SecEdit.exe $arguments | Out-String
+ }
+ catch {
+ $stderr = $_.Exception.Message
+ }
+ $log = Get-Content -LiteralPath $log_path
+ Remove-Item -LiteralPath $log_path -Force
+
+ $return = @{
+ log = ($log -join "`n").Trim()
+ stdout = $stdout
+ stderr = $stderr
+ rc = $LASTEXITCODE
+ }
+
+ return $return
+}
+
+Function Export-SecEdit() {
+ # GetTempFileName() will create a file but it doesn't have any content. This is problematic as secedit uses the
+ # encoding of the file at /cfg if it exists and because there is no BOM it will export using the "ANSI" encoding.
+ # By making sure the file exists and has a UTF-16-LE BOM we can be sure our parser reads the bytes as a string
+ # correctly.
+ $secedit_ini_path = [IO.Path]::GetTempFileName()
+ Set-Content -LiteralPath $secedit_ini_path -Value '' -Encoding Unicode
+
+ # while this will technically make a change to the system in check mode by
+ # creating a new file, we need these values to be able to do anything
+ # substantial in check mode
+ $export_result = Invoke-SecEdit -arguments @("/export", "/cfg", $secedit_ini_path, "/quiet")
+
+ # check the return code and if the file has been populated, otherwise error out
+ if (($export_result.rc -ne 0) -or ((Get-Item -LiteralPath $secedit_ini_path).Length -eq 0)) {
+ Remove-Item -LiteralPath $secedit_ini_path -Force
+ $result.rc = $export_result.rc
+ $result.stdout = $export_result.stdout
+ $result.stderr = $export_result.stderr
+ Fail-Json $result "Failed to export secedit.ini file to $($secedit_ini_path)"
+ }
+ $secedit_ini = ConvertFrom-Ini -file_path $secedit_ini_path
+
+ return $secedit_ini
+}
+
+Function Import-SecEdit($ini) {
+ $secedit_ini_path = [IO.Path]::GetTempFileName()
+ $secedit_db_path = [IO.Path]::GetTempFileName()
+ Remove-Item -LiteralPath $secedit_db_path -Force # needs to be deleted for SecEdit.exe /import to work
+
+ $ini_contents = ConvertTo-Ini -ini $ini
+
+ # Use Unicode (UTF-16-LE) as that is the same across all PowerShell versions and we don't have to worry about
+ # changing ANSI encodings.
+ Set-Content -LiteralPath $secedit_ini_path -Value $ini_contents -Encoding Unicode
+ $result.changed = $true
+
+ $import_result = Invoke-SecEdit -arguments @("/configure", "/db", $secedit_db_path, "/cfg", $secedit_ini_path, "/quiet")
+ $result.import_log = $import_result.log
+ Remove-Item -LiteralPath $secedit_ini_path -Force
+ if ($import_result.rc -ne 0) {
+ $result.rc = $import_result.rc
+ $result.stdout = $import_result.stdout
+ $result.stderr = $import_result.stderr
+ Fail-Json $result "Failed to import secedit.ini file from $($secedit_ini_path)"
+ }
+
+ # https://github.com/ansible-collections/community.windows/issues/153
+ # The LegalNoticeText entry is stored in the ini with type 7 (REG_MULTI_SZ) where each comma entry is read as a
+ # newline. When secedit imports the value it sets LegalNoticeText in the registry to be a REG_SZ type with the
+ # newlines but it also adds the extra null char at the end that REG_MULTI_SZ uses to denote the end of an entry.
+ # We manually trim off that extra null char so the legal text does not contain the unknown character symbol.
+ $legalPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
+ $legalName = 'LegalNoticeText'
+ $prop = Get-ItemProperty -LiteralPath $legalPath
+ if ($legalName -in $prop.PSObject.Properties.Name) {
+ $existingText = $prop.LegalNoticeText.TrimEnd("`0")
+ Set-ItemProperty -LiteralPath $legalPath -Name $legalName -Value $existingText
+ }
+}
+
+Function ConvertTo-Ini($ini) {
+ $content = @()
+ foreach ($key in $ini.GetEnumerator()) {
+ $section = $key.Name
+ $values = $key.Value
+
+ $content += "[$section]"
+ foreach ($value in $values.GetEnumerator()) {
+ $value_key = $value.Name
+ $value_value = $value.Value
+
+ if ($null -ne $value_value) {
+ $content += "$value_key = $value_value"
+ }
+ }
+ }
+
+ return $content -join "`r`n"
+}
+
+Function ConvertFrom-Ini($file_path) {
+ $ini = @{}
+ switch -Regex -File $file_path {
+ "^\[(.+)\]" {
+ $section = $matches[1]
+ $ini.$section = @{}
+ }
+ "(.+?)\s*=(.*)" {
+ $name = $matches[1].Trim()
+ $value = $matches[2].Trim()
+ if ($value -match "^\d+$") {
+ $value = [int]$value
+ }
+ elseif ($value.StartsWith('"') -and $value.EndsWith('"')) {
+ $value = $value.Substring(1, $value.Length - 2)
+ }
+
+ $ini.$section.$name = $value
+ }
+ }
+
+ return $ini
+}
+
+if ($section -eq "Privilege Rights") {
+ Add-Warning -obj $result -message "Using this module to edit rights and privileges is error-prone, use the ansible.windows.win_user_right module instead"
+}
+
+$will_change = $false
+$secedit_ini = Export-SecEdit
+if (-not ($secedit_ini.ContainsKey($section))) {
+ Fail-Json $result "The section '$section' does not exist in SecEdit.exe output ini"
+}
+
+if ($secedit_ini.$section.ContainsKey($key)) {
+ $current_value = $secedit_ini.$section.$key
+
+ if ($current_value -cne $value) {
+ if ($diff_mode) {
+ $result.diff.prepared = @"
+[$section]
+-$key = $current_value
++$key = $value
+"@
+ }
+
+ $secedit_ini.$section.$key = $value
+ $will_change = $true
+ }
+}
+elseif ([string]$value -eq "") {
+ # Value is requested to be removed, and has already been removed, do nothing
+}
+else {
+ if ($diff_mode) {
+ $result.diff.prepared = @"
+[$section]
++$key = $value
+"@
+ }
+ $secedit_ini.$section.$key = $value
+ $will_change = $true
+}
+
+if ($will_change -eq $true) {
+ $result.changed = $true
+ if (-not $check_mode) {
+ Import-SecEdit -ini $secedit_ini
+
+ # secedit doesn't error out on improper entries, re-export and verify
+ # the changes occurred
+ $verification_ini = Export-SecEdit
+ $new_section_values = $verification_ini.$section
+ if ($new_section_values.ContainsKey($key)) {
+ $new_value = $new_section_values.$key
+ if ($new_value -cne $value) {
+ Fail-Json $result "Failed to change the value for key '$key' in section '$section', the value is still $new_value"
+ }
+ }
+ elseif ([string]$value -eq "") {
+ # Value was empty, so OK if no longer in the result
+ }
+ else {
+ Fail-Json $result "The key '$key' in section '$section' is not a valid key, cannot set this value"
+ }
+ }
+}
+
+Exit-Json $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_security_policy.py b/ansible_collections/community/windows/plugins/modules/win_security_policy.py
new file mode 100644
index 00000000..acc00f2d
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_security_policy.py
@@ -0,0 +1,118 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_security_policy
+short_description: Change local security policy settings
+description:
+- Allows you to set the local security policies that are configured by
+ SecEdit.exe.
+options:
+ section:
+ description:
+ - The ini section the key exists in.
+ - If the section does not exist then the module will return an error.
+ - Example sections to use are 'Account Policies', 'Local Policies',
+ 'Event Log', 'Restricted Groups', 'System Services', 'Registry' and
+ 'File System'
+ - If wanting to edit the C(Privilege Rights) section, use the
+ M(ansible.windows.win_user_right) module instead.
+ type: str
+ required: yes
+ key:
+ description:
+ - The ini key of the section or policy name to modify.
+ - The module will return an error if this key is invalid.
+ type: str
+ required: yes
+ value:
+ description:
+ - The value for the ini key or policy name.
+ - If the key takes in a boolean value then 0 = False and 1 = True.
+ type: str
+ required: yes
+notes:
+- This module uses the SecEdit.exe tool to configure the values, more details
+ of the areas and keys that can be configured can be found here
+ U(https://msdn.microsoft.com/en-us/library/bb742512.aspx).
+- If you are in a domain environment these policies may be set by a GPO policy,
+ this module can temporarily change these values but the GPO will override
+ it if the value differs.
+- You can also run C(SecEdit.exe /export /cfg C:\temp\output.ini) to view the
+ current policies set on your system.
+- When assigning user rights, use the M(ansible.windows.win_user_right) module instead.
+seealso:
+- module: ansible.windows.win_user_right
+author:
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = r'''
+- name: Change the guest account name
+ community.windows.win_security_policy:
+ section: System Access
+ key: NewGuestName
+ value: Guest Account
+
+- name: Set the maximum password age
+ community.windows.win_security_policy:
+ section: System Access
+ key: MaximumPasswordAge
+ value: 15
+
+- name: Do not store passwords using reversible encryption
+ community.windows.win_security_policy:
+ section: System Access
+ key: ClearTextPassword
+ value: 0
+
+- name: Enable system events
+ community.windows.win_security_policy:
+ section: Event Audit
+ key: AuditSystemEvents
+ value: 1
+'''
+
+RETURN = r'''
+rc:
+ description: The return code after a failure when running SecEdit.exe.
+ returned: failure with secedit calls
+ type: int
+ sample: -1
+stdout:
+ description: The output of the STDOUT buffer after a failure when running
+ SecEdit.exe.
+ returned: failure with secedit calls
+ type: str
+ sample: check log for error details
+stderr:
+ description: The output of the STDERR buffer after a failure when running
+ SecEdit.exe.
+ returned: failure with secedit calls
+ type: str
+ sample: failed to import security policy
+import_log:
+ description: The log of the SecEdit.exe /configure job that configured the
+ local policies. This is used for debugging purposes on failures.
+ returned: secedit.exe /import run and change occurred
+ type: str
+ sample: Completed 6 percent (0/15) \tProcess Privilege Rights area.
+key:
+ description: The key in the section passed to the module to modify.
+ returned: success
+ type: str
+ sample: NewGuestName
+section:
+ description: The section passed to the module to modify.
+ returned: success
+ type: str
+ sample: System Access
+value:
+ description: The value passed to the module to modify to.
+ returned: success
+ type: str
+ sample: Guest Account
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_shortcut.ps1 b/ansible_collections/community/windows/plugins/modules/win_shortcut.ps1
new file mode 100644
index 00000000..1b0ab650
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_shortcut.ps1
@@ -0,0 +1,381 @@
+#!powershell
+
+# Copyright: (c) 2016, Dag Wieers (@dagwieers) <dag@wieers.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Based on: http://powershellblogger.com/2016/01/create-shortcuts-lnk-or-url-files-with-powershell/
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#Requires -Module Ansible.ModuleUtils.AddType
+
+$spec = @{
+ options = @{
+ src = @{ type = 'str' }
+ dest = @{ type = 'path'; required = $true }
+ state = @{ type = 'str'; default = 'present'; choices = @( 'absent', 'present' ) }
+ arguments = @{ type = 'str'; aliases = @( 'args' ) }
+ directory = @{ type = 'path' }
+ hotkey = @{ type = 'str'; no_log = $false }
+ icon = @{ type = 'path' }
+ description = @{ type = 'str' }
+ windowstyle = @{ type = 'str'; choices = @( 'maximized', 'minimized', 'normal' ) }
+ run_as_admin = @{ type = 'bool'; default = $false }
+ }
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$src = $module.Params.src
+$dest = $module.Params.dest
+$state = $module.Params.state
+$arguments = $module.Params.arguments # NOTE: Variable $args is a special variable
+$directory = $module.Params.directory
+$hotkey = $module.Params.hotkey
+$icon = $module.Params.icon
+$description = $module.Params.description
+$windowstyle = $module.Params.windowstyle
+$run_as_admin = $module.Params.run_as_admin
+
+# Expand environment variables on non-path types
+if ($null -ne $src) {
+ $src = [System.Environment]::ExpandEnvironmentVariables($src)
+}
+if ($null -ne $arguments) {
+ $arguments = [System.Environment]::ExpandEnvironmentVariables($arguments)
+}
+if ($null -ne $description) {
+ $description = [System.Environment]::ExpandEnvironmentVariables($description)
+}
+
+$module.Result.changed = $false
+$module.Result.dest = $dest
+$module.Result.state = $state
+
+# TODO: look at consolidating other COM actions into the C# class for future compatibility
+Add-CSharpType -AnsibleModule $module -References @'
+using System;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.ComTypes;
+using System.Text;
+
+namespace Ansible.Shortcut
+{
+ [ComImport()]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ [Guid("000214F9-0000-0000-C000-000000000046")]
+ internal interface IShellLinkW
+ {
+ // We only care about GetPath and GetIDList, omit the other methods for now
+ void GetPath(StringBuilder pszFile, int cch, IntPtr pfd, UInt32 fFlags);
+ void GetIDList(out IntPtr ppidl);
+ }
+
+ [ComImport()]
+ [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+ [Guid("45E2b4AE-B1C3-11D0-B92F-00A0C90312E1")]
+ internal interface IShellLinkDataList
+ {
+ void AddDataBlock(IntPtr pDataBlock);
+ void CopyDataBlock(uint dwSig, out IntPtr ppDataBlock);
+ void RemoveDataBlock(uint dwSig);
+ void GetFlags(out ShellLinkFlags dwFlags);
+ void SetFlags(ShellLinkFlags dwFlags);
+ }
+
+ internal class NativeHelpers
+ {
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ public struct SHFILEINFO
+ {
+ public IntPtr hIcon;
+ public int iIcon;
+ public UInt32 dwAttributes;
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 260)] public char[] szDisplayName;
+ [MarshalAs(UnmanagedType.ByValArray, SizeConst = 80)] public char[] szTypeName;
+ }
+ }
+
+ internal class NativeMethods
+ {
+ [DllImport("shell32.dll")]
+ public static extern void ILFree(
+ IntPtr pidl);
+
+ [DllImport("shell32.dll")]
+ public static extern IntPtr SHGetFileInfoW(
+ IntPtr pszPath,
+ UInt32 dwFileAttributes,
+ ref NativeHelpers.SHFILEINFO psfi,
+ int sbFileInfo,
+ UInt32 uFlags);
+
+ [DllImport("shell32.dll")]
+ public static extern int SHParseDisplayName(
+ [MarshalAs(UnmanagedType.LPWStr)] string pszName,
+ IntPtr pbc,
+ out IntPtr ppidl,
+ UInt32 sfagoIn,
+ out UInt32 psfgaoOut);
+ }
+
+ [System.Flags]
+ public enum ShellLinkFlags : uint
+ {
+ Default = 0x00000000,
+ HasIdList = 0x00000001,
+ HasLinkInfo = 0x00000002,
+ HasName = 0x00000004,
+ HasRelPath = 0x00000008,
+ HasWorkingDir = 0x00000010,
+ HasArgs = 0x00000020,
+ HasIconLocation = 0x00000040,
+ Unicode = 0x00000080,
+ ForceNoLinkInfo = 0x00000100,
+ HasExpSz = 0x00000200,
+ RunInSeparate = 0x00000400,
+ HasLogo3Id = 0x00000800,
+ HasDarwinId = 0x00001000,
+ RunAsUser = 0x00002000,
+ HasExpIconSz = 0x00004000,
+ NoPidlAlias = 0x00008000,
+ ForceUncName = 0x00010000,
+ RunWithShimLayer = 0x00020000,
+ ForceNoLinkTrack = 0x00040000,
+ EnableTargetMetadata = 0x00080000,
+ DisableLinkPathTracking = 0x00100000,
+ DisableKnownFolderRelativeTracking = 0x00200000,
+ NoKfAlias = 0x00400000,
+ AllowLinkToLink = 0x00800000,
+ UnAliasOnSave = 0x01000000,
+ PreferEnvironmentPath = 0x02000000,
+ KeepLocalIdListForUncTarget = 0x04000000,
+ PersistVolumeIdToRelative = 0x08000000,
+ Valid = 0x0FFFF7FF,
+ Reserved = 0x80000000
+ }
+
+ public class ShellLink
+ {
+ private static Guid CLSID_ShellLink = new Guid("00021401-0000-0000-C000-000000000046");
+
+ public static ShellLinkFlags GetFlags(string path)
+ {
+ IShellLinkW link = InitialiseObj(path);
+ ShellLinkFlags dwFlags;
+ ((IShellLinkDataList)link).GetFlags(out dwFlags);
+ return dwFlags;
+ }
+
+ public static void SetFlags(string path, ShellLinkFlags flags)
+ {
+ IShellLinkW link = InitialiseObj(path);
+ ((IShellLinkDataList)link).SetFlags(flags);
+ ((IPersistFile)link).Save(null, false);
+ }
+
+ public static string GetTargetPath(string path)
+ {
+ IShellLinkW link = InitialiseObj(path);
+
+ StringBuilder pathSb = new StringBuilder(260);
+ link.GetPath(pathSb, pathSb.Capacity, IntPtr.Zero, 0);
+ string linkPath = pathSb.ToString();
+
+ // If the path wasn't set, try and get the path from the ItemIDList
+ ShellLinkFlags flags = GetFlags(path);
+ if (String.IsNullOrEmpty(linkPath) && ((uint)flags & (uint)ShellLinkFlags.HasIdList) == (uint)ShellLinkFlags.HasIdList)
+ {
+ IntPtr idList = IntPtr.Zero;
+ try
+ {
+ link.GetIDList(out idList);
+ linkPath = GetDisplayNameFromPidl(idList);
+ }
+ finally
+ {
+ NativeMethods.ILFree(idList);
+ }
+ }
+ return linkPath;
+ }
+
+ public static string GetDisplayNameFromPath(string path)
+ {
+ UInt32 sfgaoOut;
+ IntPtr pidl = IntPtr.Zero;
+ try
+ {
+ int res = NativeMethods.SHParseDisplayName(path, IntPtr.Zero, out pidl, 0, out sfgaoOut);
+ Marshal.ThrowExceptionForHR(res);
+ return GetDisplayNameFromPidl(pidl);
+ }
+ finally
+ {
+ NativeMethods.ILFree(pidl);
+ }
+ }
+
+ private static string GetDisplayNameFromPidl(IntPtr pidl)
+ {
+ NativeHelpers.SHFILEINFO shFileInfo = new NativeHelpers.SHFILEINFO();
+ UInt32 uFlags = 0x000000208; // SHGFI_DISPLAYNAME | SHGFI_PIDL
+ NativeMethods.SHGetFileInfoW(pidl, 0, ref shFileInfo, Marshal.SizeOf(typeof(NativeHelpers.SHFILEINFO)), uFlags);
+ return new string(shFileInfo.szDisplayName).TrimEnd('\0');
+ }
+
+ private static IShellLinkW InitialiseObj(string path)
+ {
+ IShellLinkW link = Activator.CreateInstance(Type.GetTypeFromCLSID(CLSID_ShellLink)) as IShellLinkW;
+ ((IPersistFile)link).Load(path, 0);
+ return link;
+ }
+ }
+}
+'@
+
+# Convert from window style name to window style id
+$windowstyles = @{
+ normal = 1
+ maximized = 3
+ minimized = 7
+}
+
+# Convert from window style id to window style name
+$windowstyleids = @( "", "normal", "", "maximized", "", "", "", "minimized" )
+
+If ($state -eq "absent") {
+ If (Test-Path -LiteralPath $dest) {
+ # If the shortcut exists, try to remove it
+ Try {
+ Remove-Item -LiteralPath $dest -WhatIf:$module.CheckMode
+ }
+ Catch {
+ # Report removal failure
+ $module.FailJson("Failed to remove shortcut '$dest'. ($($_.Exception.Message))", $_)
+ }
+ # Report removal success
+ $module.Result.changed = $true
+ }
+ Else {
+ # Nothing to report, everything is fine already
+ }
+}
+ElseIf ($state -eq "present") {
+ # Create an in-memory object based on the existing shortcut (if any)
+ $Shell = New-Object -ComObject ("WScript.Shell")
+ $ShortCut = $Shell.CreateShortcut($dest)
+
+ # Compare existing values with new values, report as changed if required
+
+ If ($null -ne $src) {
+ # Windows translates executables to absolute path, so do we
+ If (Get-Command -Name $src -Type Application -ErrorAction SilentlyContinue) {
+ $src = (Get-Command -Name $src -Type Application).Definition
+ }
+ If (-not (Test-Path -LiteralPath $src -IsValid)) {
+ If (-not (Split-Path -Path $src -IsAbsolute)) {
+ $module.FailJson("Source '$src' is not found in PATH and not a valid or absolute path.")
+ }
+ }
+ }
+
+ # Determine if we have a WshShortcut or WshUrlShortcut by checking the Arguments property
+ # A WshUrlShortcut objects only consists of a TargetPath property
+
+ $file_shortcut = $false
+ If (Get-Member -InputObject $ShortCut -Name Arguments) {
+ # File ShortCut, compare multiple properties
+ $file_shortcut = $true
+
+ $target_path = $ShortCut.TargetPath
+ If (($null -ne $src) -and ($ShortCut.TargetPath -ne $src)) {
+ if ((Test-Path -LiteralPath $dest) -and (-not $ShortCut.TargetPath)) {
+ # If the shortcut already exists but not on the COM object, we
+ # are dealing with a shell path like 'shell:RecycleBinFolder'.
+ $expanded_src = [Ansible.Shortcut.ShellLink]::GetDisplayNameFromPath($src)
+ $actual_src = [Ansible.Shortcut.ShellLink]::GetTargetPath($dest)
+ if ($expanded_src -ne $actual_src) {
+ $module.Result.changed = $true
+ $ShortCut.TargetPath = $src
+ }
+ }
+ else {
+ $module.Result.changed = $true
+ $ShortCut.TargetPath = $src
+ }
+ $target_path = $src
+ }
+
+ # This is a full-featured application shortcut !
+ If (($null -ne $arguments) -and ($ShortCut.Arguments -ne $arguments)) {
+ $module.Result.changed = $true
+ $ShortCut.Arguments = $arguments
+ }
+ $module.Result.args = $ShortCut.Arguments
+
+ If (($null -ne $directory) -and ($ShortCut.WorkingDirectory -ne $directory)) {
+ $module.Result.changed = $true
+ $ShortCut.WorkingDirectory = $directory
+ }
+ $module.Result.directory = $ShortCut.WorkingDirectory
+
+ # FIXME: Not all values are accepted here ! Improve docs too.
+ If (($null -ne $hotkey) -and ($ShortCut.Hotkey -ne $hotkey)) {
+ $module.Result.changed = $true
+ $ShortCut.Hotkey = $hotkey
+ }
+ $module.Result.hotkey = $ShortCut.Hotkey
+
+ If (($null -ne $icon) -and ($ShortCut.IconLocation -ne $icon)) {
+ $module.Result.changed = $true
+ $ShortCut.IconLocation = $icon
+ }
+ $module.Result.icon = $ShortCut.IconLocation
+
+ If (($null -ne $description) -and ($ShortCut.Description -ne $description)) {
+ $module.Result.changed = $true
+ $ShortCut.Description = $description
+ }
+ $module.Result.description = $ShortCut.Description
+
+ If (($null -ne $windowstyle) -and ($ShortCut.WindowStyle -ne $windowstyles.$windowstyle)) {
+ $module.Result.changed = $true
+ $ShortCut.WindowStyle = $windowstyles.$windowstyle
+ }
+ $module.Result.windowstyle = $windowstyleids[$ShortCut.WindowStyle]
+ }
+ else {
+ # URL Shortcut, just compare the TargetPath
+ if (($null -ne $src) -and ($ShortCut.TargetPath -ne $src)) {
+ $module.Result.changed = $true
+ $ShortCut.TargetPath = $src
+ }
+ $target_path = $ShortCut.TargetPath
+ }
+ $module.Result.src = $target_path
+
+ If (($module.Result.changed -eq $true) -and ($module.CheckMode -ne $true)) {
+ Try {
+ $ShortCut.Save()
+ }
+ Catch {
+ $module.FailJson("Failed to create shortcut '$dest'. ($($_.Exception.Message))", $_)
+ }
+ }
+
+ if ((Test-Path -LiteralPath $dest) -and $file_shortcut) {
+ # Only control the run_as_admin flag if using a File Shortcut
+ $flags = [Ansible.Shortcut.ShellLink]::GetFlags($dest)
+ if ($run_as_admin -and (-not $flags.HasFlag([Ansible.Shortcut.ShellLinkFlags]::RunAsUser))) {
+ [Ansible.Shortcut.ShellLink]::SetFlags($dest, ($flags -bor [Ansible.Shortcut.ShellLinkFlags]::RunAsUser))
+ $module.Result.changed = $true
+ }
+ elseif (-not $run_as_admin -and ($flags.HasFlag([Ansible.Shortcut.ShellLinkFlags]::RunAsUser))) {
+ [Ansible.Shortcut.ShellLink]::SetFlags($dest, ($flags -bxor [Ansible.Shortcut.ShellLinkFlags]::RunAsUser))
+ $module.Result.changed = $true
+ }
+ }
+}
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_shortcut.py b/ansible_collections/community/windows/plugins/modules/win_shortcut.py
new file mode 100644
index 00000000..8fcc55fd
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_shortcut.py
@@ -0,0 +1,116 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2016, Dag Wieers (@dagwieers) <dag@wieers.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_shortcut
+short_description: Manage shortcuts on Windows
+description:
+- Create, manage and delete Windows shortcuts
+options:
+ src:
+ description:
+ - Executable or URL the shortcut points to.
+ - The executable needs to be in your PATH, or has to be an absolute
+ path to the executable.
+ type: str
+ description:
+ description:
+ - Description for the shortcut.
+ - This is usually shown when hoovering the icon.
+ type: str
+ dest:
+ description:
+ - Destination file for the shortcuting file.
+ - File name should have a C(.lnk) or C(.url) extension.
+ type: path
+ required: yes
+ arguments:
+ description:
+ - Additional arguments for the executable defined in C(src).
+ type: str
+ aliases: [ args ]
+ directory:
+ description:
+ - Working directory for executable defined in C(src).
+ type: path
+ icon:
+ description:
+ - Icon used for the shortcut.
+ - File name should have a C(.ico) extension.
+ - The file name is followed by a comma and the number in the library file (.dll) or use 0 for an image file.
+ type: path
+ hotkey:
+ description:
+ - Key combination for the shortcut.
+ - This is a combination of one or more modifiers and a key.
+ - Possible modifiers are Alt, Ctrl, Shift, Ext.
+ - Possible keys are [A-Z] and [0-9].
+ type: str
+ windowstyle:
+ description:
+ - Influences how the application is displayed when it is launched.
+ type: str
+ choices: [ maximized, minimized, normal ]
+ state:
+ description:
+ - When C(absent), removes the shortcut if it exists.
+ - When C(present), creates or updates the shortcut.
+ type: str
+ choices: [ absent, present ]
+ default: present
+ run_as_admin:
+ description:
+ - When C(src) is an executable, this can control whether the shortcut will be opened as an administrator or not.
+ type: bool
+ default: no
+notes:
+- 'The following options can include Windows environment variables: C(dest), C(args), C(description), C(dest), C(directory), C(icon) C(src)'
+- 'Windows has two types of shortcuts: Application and URL shortcuts. URL shortcuts only consists of C(dest) and C(src)'
+seealso:
+- module: ansible.windows.win_file
+author:
+- Dag Wieers (@dagwieers)
+'''
+
+EXAMPLES = r'''
+- name: Create an application shortcut on the desktop
+ community.windows.win_shortcut:
+ src: C:\Program Files\Mozilla Firefox\Firefox.exe
+ dest: C:\Users\Public\Desktop\Mozilla Firefox.lnk
+ icon: C:\Program Files\Mozilla Firefox\Firefox.exe,0
+
+- name: Create the same shortcut using environment variables
+ community.windows.win_shortcut:
+ description: The Mozilla Firefox web browser
+ src: '%ProgramFiles%\Mozilla Firefox\Firefox.exe'
+ dest: '%Public%\Desktop\Mozilla Firefox.lnk'
+ icon: '%ProgramFiles\Mozilla Firefox\Firefox.exe,0'
+ directory: '%ProgramFiles%\Mozilla Firefox'
+ hotkey: Ctrl+Alt+F
+
+- name: Create an application shortcut for an executable in PATH to your desktop
+ community.windows.win_shortcut:
+ src: cmd.exe
+ dest: Desktop\Command prompt.lnk
+
+- name: Create an application shortcut for the Ansible website
+ community.windows.win_shortcut:
+ src: '%ProgramFiles%\Google\Chrome\Application\chrome.exe'
+ dest: '%UserProfile%\Desktop\Ansible website.lnk'
+ arguments: --new-window https://ansible.com/
+ directory: '%ProgramFiles%\Google\Chrome\Application'
+ icon: '%ProgramFiles%\Google\Chrome\Application\chrome.exe,0'
+ hotkey: Ctrl+Alt+A
+
+- name: Create a URL shortcut for the Ansible website
+ community.windows.win_shortcut:
+ src: https://ansible.com/
+ dest: '%Public%\Desktop\Ansible website.url'
+'''
+
+RETURN = r'''
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_snmp.ps1 b/ansible_collections/community/windows/plugins/modules/win_snmp.ps1
new file mode 100644
index 00000000..7e4b9af2
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_snmp.ps1
@@ -0,0 +1,132 @@
+#!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
+
+$params = Parse-Args -arguments $args -supports_check_mode $true
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
+$managers = Get-AnsibleParam -obj $params -name "permitted_managers" -type "list" -default $null
+$communities = Get-AnsibleParam -obj $params -name "community_strings" -type "list" -default $null
+$action_in = Get-AnsibleParam -obj $params -name "action" -type "str" -default "set" -ValidateSet @("set", "add", "remove")
+$action = $action_in.ToLower()
+
+$result = @{
+ failed = $False
+ changed = $False
+ community_strings = [System.Collections.ArrayList]@()
+ permitted_managers = [System.Collections.ArrayList]@()
+}
+
+# Make sure lists are modifyable
+[System.Collections.ArrayList]$managers = $managers
+[System.Collections.ArrayList]$communities = $communities
+[System.Collections.ArrayList]$indexes = @()
+
+# Type checking
+# You would think that "$null -ne $managers" would work, but it doesn't.
+# A proper type check is required. If a user provides an empty list then $managers
+# is still of the correct type. If a user provides no option then $managers is $null.
+If ($managers -Is [System.Collections.ArrayList] -And $managers.Count -gt 0 -And $managers[0] -IsNot [String]) {
+ Fail-Json $result "Permitted managers must be an array of strings"
+}
+
+If ($communities -Is [System.Collections.ArrayList] -And $communities.Count -gt 0 -And $communities[0] -IsNot [String]) {
+ Fail-Json $result "SNMP communities must be an array of strings"
+}
+
+$Managers_reg_key = "HKLM:\System\CurrentControlSet\services\SNMP\Parameters\PermittedManagers"
+$Communities_reg_key = "HKLM:\System\CurrentControlSet\services\SNMP\Parameters\ValidCommunities"
+
+ForEach ($idx in (Get-Item -LiteralPath $Managers_reg_key).Property) {
+ $manager = (Get-ItemProperty -LiteralPath $Managers_reg_key).$idx
+ If ($idx.ToLower() -eq '(default)') {
+ continue
+ }
+
+ $remove = $False
+ If ($managers -Is [System.Collections.ArrayList] -And $managers.Contains($manager)) {
+ If ($action -eq "remove") {
+ $remove = $True
+ }
+ Else {
+ # Remove manager from list to add since it already exists
+ $managers.Remove($manager)
+ }
+ }
+ ElseIf ($action -eq "set" -And $managers -Is [System.Collections.ArrayList]) {
+ # Will remove this manager since it is not in the set list
+ $remove = $True
+ }
+
+ If ($remove) {
+ $result.changed = $True
+ Remove-ItemProperty -LiteralPath $Managers_reg_key -Name $idx -WhatIf:$check_mode
+ }
+ Else {
+ # Remember that this index is in use
+ $indexes.Add([int]$idx) | Out-Null
+ $result.permitted_managers.Add($manager) | Out-Null
+ }
+}
+
+ForEach ($community in (Get-Item -LiteralPath $Communities_reg_key).Property) {
+ If ($community.ToLower() -eq '(default)') {
+ continue
+ }
+
+ $remove = $False
+ If ($communities -Is [System.Collections.ArrayList] -And $communities.Contains($community)) {
+ If ($action -eq "remove") {
+ $remove = $True
+ }
+ Else {
+ # Remove community from list to add since it already exists
+ $communities.Remove($community)
+ }
+ }
+ ElseIf ($action -eq "set" -And $communities -Is [System.Collections.ArrayList]) {
+ # Will remove this community since it is not in the set list
+ $remove = $True
+ }
+
+ If ($remove) {
+ $result.changed = $True
+ Remove-ItemProperty -LiteralPath $Communities_reg_key -Name $community -WhatIf:$check_mode
+ }
+ Else {
+ $result.community_strings.Add($community) | Out-Null
+ }
+}
+
+If ($action -eq "remove") {
+ Exit-Json $result
+}
+
+# Add managers that don't already exist
+$next_index = 0
+If ($managers -Is [System.Collections.ArrayList]) {
+ ForEach ($manager in $managers) {
+ While ($True) {
+ $next_index = $next_index + 1
+ If (-Not $indexes.Contains($next_index)) {
+ $result.changed = $True
+ New-ItemProperty -LiteralPath $Managers_reg_key -Name $next_index -Value "$manager" -WhatIf:$check_mode | Out-Null
+ $result.permitted_managers.Add($manager) | Out-Null
+ break
+ }
+ }
+ }
+}
+
+# Add communities that don't already exist
+If ($communities -Is [System.Collections.ArrayList]) {
+ ForEach ($community in $communities) {
+ $result.changed = $True
+ New-ItemProperty -LiteralPath $Communities_reg_key -Name $community -PropertyType DWord -Value 4 -WhatIf:$check_mode | Out-Null
+ $result.community_strings.Add($community) | Out-Null
+ }
+}
+
+Exit-Json $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_snmp.py b/ansible_collections/community/windows/plugins/modules/win_snmp.py
new file mode 100644
index 00000000..0068e214
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_snmp.py
@@ -0,0 +1,70 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Ansible, inc
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_snmp
+short_description: Configures the Windows SNMP service
+description:
+ - This module configures the Windows SNMP service.
+options:
+ permitted_managers:
+ description:
+ - The list of permitted SNMP managers.
+ type: list
+ elements: str
+ community_strings:
+ description:
+ - The list of read-only SNMP community strings.
+ type: list
+ elements: str
+ action:
+ description:
+ - C(add) will add new SNMP community strings and/or SNMP managers
+ - C(set) will replace SNMP community strings and/or SNMP managers. An
+ empty list for either C(community_strings) or C(permitted_managers)
+ will result in the respective lists being removed entirely.
+ - C(remove) will remove SNMP community strings and/or SNMP managers
+ type: str
+ choices: [ add, set, remove ]
+ default: set
+author:
+ - Michael Cassaniti (@mcassaniti)
+'''
+
+EXAMPLES = r'''
+- name: Replace SNMP communities and managers
+ community.windows.win_snmp:
+ community_strings:
+ - public
+ permitted_managers:
+ - 192.168.1.2
+ action: set
+
+- name: Replace SNMP communities and clear managers
+ community.windows.win_snmp:
+ community_strings:
+ - public
+ permitted_managers: []
+ action: set
+'''
+
+RETURN = r'''
+community_strings:
+ description: The list of community strings for this machine.
+ type: list
+ returned: always
+ sample:
+ - public
+ - snmp-ro
+permitted_managers:
+ description: The list of permitted managers for this machine.
+ type: list
+ returned: always
+ sample:
+ - 192.168.1.1
+ - 192.168.1.2
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_timezone.ps1 b/ansible_collections/community/windows/plugins/modules/win_timezone.ps1
new file mode 100644
index 00000000..c2f3b4a9
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_timezone.ps1
@@ -0,0 +1,73 @@
+#!powershell
+
+# Copyright: (c) 2015, Phil Schwartz <schwartzmx@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
+
+$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
+
+$timezone = Get-AnsibleParam -obj $params -name "timezone" -type "str" -failifempty $true
+
+$result = @{
+ changed = $false
+ previous_timezone = $timezone
+ timezone = $timezone
+}
+
+Try {
+ # Get the current timezone set
+ $result.previous_timezone = $(tzutil.exe /g)
+ If ($LASTEXITCODE -ne 0) {
+ Throw "An error occurred when getting the current machine's timezone setting."
+ }
+
+ if ( $result.previous_timezone -eq $timezone ) {
+ Exit-Json $result "Timezone '$timezone' is already set on this machine"
+ }
+ Else {
+ # Check that timezone is listed as an available timezone to the machine
+ $tzList = $(tzutil.exe /l).ToLower()
+ If ($LASTEXITCODE -ne 0) {
+ Throw "An error occurred when listing the available timezones."
+ }
+
+ $tzExists = $tzList.Contains(($timezone -Replace '_dstoff').ToLower())
+ if (-not $tzExists) {
+ Fail-Json $result "The specified timezone: $timezone isn't supported on the machine."
+ }
+
+ if ($check_mode) {
+ $result.changed = $true
+ }
+ else {
+ tzutil.exe /s "$timezone"
+ if ($LASTEXITCODE -ne 0) {
+ Throw "An error occurred when setting the specified timezone with tzutil."
+ }
+
+ $new_timezone = $(tzutil.exe /g)
+ if ($LASTEXITCODE -ne 0) {
+ Throw "An error occurred when getting the current machine's timezone setting."
+ }
+
+ if ($timezone -eq $new_timezone) {
+ $result.changed = $true
+ }
+ }
+
+ if ($diff_support) {
+ $result.diff = @{
+ before = "$($result.previous_timezone)`n"
+ after = "$timezone`n"
+ }
+ }
+ }
+}
+Catch {
+ Fail-Json $result "Error setting timezone to: $timezone."
+}
+
+Exit-Json $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_timezone.py b/ansible_collections/community/windows/plugins/modules/win_timezone.py
new file mode 100644
index 00000000..d7f6adba
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_timezone.py
@@ -0,0 +1,63 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2015, Phil Schwartz <schwartzmx@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_timezone
+short_description: Sets Windows machine timezone
+description:
+- Sets machine time to the specified timezone.
+options:
+ timezone:
+ description:
+ - Timezone to set to.
+ - 'Example: Central Standard Time'
+ - To disable Daylight Saving time, add the suffix C(_dstoff) on timezones that support this.
+ type: str
+ required: yes
+notes:
+- The module will check if the provided timezone is supported on the machine.
+- A list of possible timezones is available from C(tzutil.exe /l) and from
+ U(https://msdn.microsoft.com/en-us/library/ms912391.aspx)
+- If running on Server 2008 the hotfix
+ U(https://support.microsoft.com/en-us/help/2556308/tzutil-command-line-tool-is-added-to-windows-vista-and-to-windows-server-2008)
+ needs to be installed to be able to run this module.
+seealso:
+- module: community.windows.win_region
+author:
+- Phil Schwartz (@schwartzmx)
+'''
+
+EXAMPLES = r'''
+- name: Set timezone to 'Romance Standard Time' (GMT+01:00)
+ community.windows.win_timezone:
+ timezone: Romance Standard Time
+
+- name: Set timezone to 'GMT Standard Time' (GMT)
+ community.windows.win_timezone:
+ timezone: GMT Standard Time
+
+- name: Set timezone to 'Central Standard Time' (GMT-06:00)
+ community.windows.win_timezone:
+ timezone: Central Standard Time
+
+- name: Set timezime to Pacific Standard time and disable Daylight Saving time adjustments
+ community.windows.win_timezone:
+ timezone: Pacific Standard Time_dstoff
+'''
+
+RETURN = r'''
+previous_timezone:
+ description: The previous timezone if it was changed, otherwise the existing timezone.
+ returned: success
+ type: str
+ sample: Central Standard Time
+timezone:
+ description: The current timezone (possibly changed).
+ returned: success
+ type: str
+ sample: Central Standard Time
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_toast.ps1 b/ansible_collections/community/windows/plugins/modules/win_toast.ps1
new file mode 100644
index 00000000..5cb1cf26
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_toast.ps1
@@ -0,0 +1,93 @@
+#!powershell
+
+# Copyright: (c) 2017, Jon Hawkesworth (@jhawkesworth) <jhawkesworth@protonmail.com>
+# 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"
+
+# version check
+$osversion = [Environment]::OSVersion
+$lowest_version = 10
+if ($osversion.Version.Major -lt $lowest_version ) {
+ $msg = "Sorry, this version of windows, $osversion, does not support Toast notifications. Toast notifications are available from version $lowest_version"
+ Fail-Json -obj $result -message $msg
+}
+
+$stopwatch = [system.diagnostics.stopwatch]::startNew()
+$now = [DateTime]::Now
+$default_title = "Notification: " + $now.ToShortTimeString()
+
+$params = Parse-Args $args -supports_check_mode $true
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
+
+$expire_seconds = Get-AnsibleParam -obj $params -name "expire" -type "int" -default 45
+$group = Get-AnsibleParam -obj $params -name "group" -type "str" -default "Powershell"
+$msg = Get-AnsibleParam -obj $params -name "msg" -type "str" -default "Hello world!"
+$popup = Get-AnsibleParam -obj $params -name "popup" -type "bool" -default $true
+$tag = Get-AnsibleParam -obj $params -name "tag" -type "str" -default "Ansible"
+$title = Get-AnsibleParam -obj $params -name "title" -type "str" -default $default_title
+
+$timespan = New-TimeSpan -Seconds $expire_seconds
+$expire_at = $now + $timespan
+$expire_at_utc = $($expire_at.ToUniversalTime() | Out-String).Trim()
+
+$result = @{
+ changed = $false
+ expire_at = $expire_at.ToString()
+ expire_at_utc = $expire_at_utc
+ toast_sent = $false
+}
+
+# If no logged in users, there is no notifications service,
+# and no-one to read the message, so exit but do not fail
+# if there are no logged in users to notify.
+
+if ((Get-Process -Name explorer -ErrorAction SilentlyContinue).Count -gt 0) {
+
+ [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null
+ $template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText01)
+
+ #Convert to .NET type for XML manipulation
+ $toastXml = [xml] $template.GetXml()
+ $toastXml.GetElementsByTagName("text").AppendChild($toastXml.CreateTextNode($title)) > $null
+ # TODO add subtitle
+
+ #Convert back to WinRT type
+ $xml = New-Object Windows.Data.Xml.Dom.XmlDocument
+ $xml.LoadXml($toastXml.OuterXml)
+
+ $toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
+ $toast.Tag = $tag
+ $toast.Group = $group
+ $toast.ExpirationTime = $expire_at
+ $toast.SuppressPopup = -not $popup
+
+ try {
+ $notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($msg)
+ if (-not $check_mode) {
+ $notifier.Show($toast)
+ $result.toast_sent = $true
+ Start-Sleep -Seconds $expire_seconds
+ }
+ }
+ catch {
+ $excep = $_
+ $result.exception = $excep.ScriptStackTrace
+ Fail-Json -obj $result -message "Failed to create toast notifier: $($excep.Exception.Message)"
+ }
+}
+else {
+ $result.toast_sent = $false
+ $result.no_toast_sent_reason = 'No logged in users to notify'
+}
+
+$endsend_at = Get-Date | Out-String
+$stopwatch.Stop()
+
+$result.time_taken = $stopwatch.Elapsed.TotalSeconds
+$result.sent_localtime = $endsend_at.Trim()
+
+Exit-Json -obj $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_toast.py b/ansible_collections/community/windows/plugins/modules/win_toast.py
new file mode 100644
index 00000000..e3fc1a07
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_toast.py
@@ -0,0 +1,94 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2017, Jon Hawkesworth (@jhawkesworth) <jhawkesworth@protonmail.com>
+# Copyright: (c) 2017, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_toast
+short_description: Sends Toast windows notification to logged in users on Windows 10 or later hosts
+description:
+ - Sends alerts which appear in the Action Center area of the windows desktop.
+options:
+ expire:
+ description:
+ - How long in seconds before the notification expires.
+ type: int
+ default: 45
+ group:
+ description:
+ - Which notification group to add the notification to.
+ type: str
+ default: Powershell
+ msg:
+ description:
+ - The message to appear inside the notification.
+ - May include \n to format the message to appear within the Action Center.
+ type: str
+ default: Hello, World!
+ popup:
+ description:
+ - If C(no), the notification will not pop up and will only appear in the Action Center.
+ type: bool
+ default: yes
+ tag:
+ description:
+ - The tag to add to the notification.
+ type: str
+ default: Ansible
+ title:
+ description:
+ - The notification title, which appears in the pop up..
+ type: str
+ default: Notification HH:mm
+notes:
+ - This module must run on a windows 10 or Server 2016 host, so ensure your play targets windows hosts, or delegates to a windows host.
+ - The module does not fail if there are no logged in users to notify.
+ - Messages are only sent to the local host where the module is run.
+ - You must run this module with async, otherwise it will hang until the expire period has passed.
+seealso:
+- module: community.windows.win_msg
+- module: community.windows.win_say
+author:
+- Jon Hawkesworth (@jhawkesworth)
+'''
+
+EXAMPLES = r'''
+- name: Warn logged in users of impending upgrade (note use of async to stop the module from waiting until notification expires).
+ community.windows.win_toast:
+ expire: 60
+ title: System Upgrade Notification
+ msg: Automated upgrade about to start. Please save your work and log off before {{ deployment_start_time }}
+ async: 60
+ poll: 0
+'''
+
+RETURN = r'''
+expire_at_utc:
+ description: Calculated utc date time when the notification expires.
+ returned: always
+ type: str
+ sample: 07 July 2017 04:50:54
+no_toast_sent_reason:
+ description: Text containing the reason why a notification was not sent.
+ returned: when no logged in users are detected
+ type: str
+ sample: No logged in users to notify
+sent_localtime:
+ description: local date time when the notification was sent.
+ returned: always
+ type: str
+ sample: 07 July 2017 05:45:54
+time_taken:
+ description: How long the module took to run on the remote windows host in seconds.
+ returned: always
+ type: float
+ sample: 0.3706631999999997
+toast_sent:
+ description: Whether the module was able to send a toast notification or not.
+ returned: always
+ type: bool
+ sample: false
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_unzip.ps1 b/ansible_collections/community/windows/plugins/modules/win_unzip.ps1
new file mode 100644
index 00000000..bd2133e2
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_unzip.ps1
@@ -0,0 +1,197 @@
+#!powershell
+
+# Copyright: (c) 2015, Phil Schwartz <schwartzmx@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
+
+# TODO: This module is not idempotent (it will always unzip and report change)
+
+$ErrorActionPreference = "Stop"
+
+$pcx_extensions = @('.bz2', '.gz', '.msu', '.tar', '.zip')
+
+$params = Parse-Args $args -supports_check_mode $true
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
+
+$src = Get-AnsibleParam -obj $params -name "src" -type "path" -failifempty $true
+$dest = Get-AnsibleParam -obj $params -name "dest" -type "path" -failifempty $true
+$creates = Get-AnsibleParam -obj $params -name "creates" -type "path"
+$recurse = Get-AnsibleParam -obj $params -name "recurse" -type "bool" -default $false
+$delete_archive = Get-AnsibleParam -obj $params -name "delete_archive" -type "bool" -default $false -aliases 'rm'
+$password = Get-AnsibleParam -obj $params -name "password" -type "str"
+
+# Fixes a fail error message (when the task actually succeeds) for a
+# "Convert-ToJson: The converted JSON string is in bad format"
+# This happens when JSON is parsing a string that ends with a "\",
+# which is possible when specifying a directory to download to.
+# This catches that possible error, before assigning the JSON $result
+$result = @{
+ changed = $false
+ dest = $dest -replace '\$', ''
+ removed = $false
+ src = $src -replace '\$', ''
+}
+
+Function Expand-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
+
+ $entry_target_path = [System.IO.Path]::Combine($dest, $archive_name)
+ $entry_dir = [System.IO.Path]::GetDirectoryName($entry_target_path)
+
+ # Normalize paths for further evaluation
+ $full_target_path = [System.IO.Path]::GetFullPath($entry_target_path)
+ $full_dest_path = [System.IO.Path]::GetFullPath($dest + [System.IO.Path]::DirectorySeparatorChar)
+
+ # Ensure file in the archive does not escape the extraction path
+ if (-not $full_target_path.StartsWith($full_dest_path)) {
+ $msg = "Error unzipping '$src' to '$dest'! Filename contains relative paths which would extract outside the destination: $entry_target_path"
+ Fail-Json -obj $result -message $msg
+ }
+
+ if (-not (Test-Path -LiteralPath $entry_dir)) {
+ New-Item -Path $entry_dir -ItemType Directory -WhatIf:$check_mode | Out-Null
+ $result.changed = $true
+ }
+
+ if ((-not ($entry_target_path.EndsWith("\") -or $entry_target_path.EndsWith("/"))) -and (-not $check_mode)) {
+ [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $entry_target_path, $true)
+ }
+ $result.changed = $true
+ }
+ $archive.Dispose()
+}
+
+Function Expand-ZipLegacy($src, $dest) {
+ # [System.IO.Compression.ZipFile] was only added in .net 4.5, this is used
+ # when .net is older than that.
+ $shell = New-Object -ComObject Shell.Application
+ $zip = $shell.NameSpace([IO.Path]::GetFullPath($src))
+ $dest_path = $shell.NameSpace([IO.Path]::GetFullPath($dest))
+
+ $shell = New-Object -ComObject Shell.Application
+
+ if (-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($zip.Items(), 1044)
+ }
+ $result.changed = $true
+}
+
+If ($creates -and (Test-Path -LiteralPath $creates)) {
+ $result.skipped = $true
+ $result.msg = "The file or directory '$creates' already exists."
+ Exit-Json -obj $result
+}
+
+If (-Not (Test-Path -LiteralPath $src)) {
+ Fail-Json -obj $result -message "File '$src' does not exist."
+}
+
+$ext = [System.IO.Path]::GetExtension($src)
+
+If (-Not (Test-Path -LiteralPath $dest -PathType Container)) {
+ Try {
+ New-Item -ItemType "directory" -path $dest -WhatIf:$check_mode | out-null
+ }
+ Catch {
+ Fail-Json -obj $result -message "Error creating '$dest' directory! Msg: $($_.Exception.Message)"
+ }
+}
+
+If ($ext -eq ".zip" -And $recurse -eq $false -And -Not $password) {
+ # TODO: PS v5 supports zip extraction, use that if available
+ $use_legacy = $false
+ try {
+ # determines if .net 4.5 is available, if this fails we need to fall
+ # back to the legacy COM Shell.Application to extract the zip
+ Add-Type -AssemblyName System.IO.Compression.FileSystem | Out-Null
+ Add-Type -AssemblyName System.IO.Compression | Out-Null
+ }
+ catch {
+ $use_legacy = $true
+ }
+
+ if ($use_legacy) {
+ try {
+ Expand-ZipLegacy -src $src -dest $dest
+ }
+ catch {
+ Fail-Json -obj $result -message "Error unzipping '$src' to '$dest'!. Method: COM Shell.Application, Exception: $($_.Exception.Message)"
+ }
+ }
+ else {
+ try {
+ Expand-Zip -src $src -dest $dest
+ }
+ catch {
+ Fail-Json -obj $result -message "Error unzipping '$src' to '$dest'!. Method: System.IO.Compression.ZipFile, Exception: $($_.Exception.Message)"
+ }
+ }
+}
+Else {
+ # Check if PSCX is installed
+ $list = Get-Module -ListAvailable
+
+ If (-Not ($list -match "PSCX")) {
+ Fail-Json -obj $result -message "PowerShellCommunityExtensions PowerShell Module (PSCX) is required for non-'.zip' compressed archive types."
+ }
+ Else {
+ $result.pscx_status = "present"
+ }
+
+ Try {
+ Import-Module PSCX
+ }
+ Catch {
+ Fail-Json $result "Error importing module PSCX"
+ }
+
+ $expand_params = @{
+ OutputPath = $dest
+ WhatIf = $check_mode
+ }
+ if ($null -ne $password) {
+ $expand_params.Password = ConvertTo-SecureString -String $password -AsPlainText -Force
+ }
+ Try {
+ Expand-Archive -Path $src @expand_params
+ }
+ Catch {
+ Fail-Json -obj $result -message "Error expanding '$src' to '$dest'! Msg: $($_.Exception.Message)"
+ }
+
+ If ($recurse) {
+ Get-ChildItem -LiteralPath $dest -recurse | Where-Object { $pcx_extensions -contains $_.extension } | ForEach-Object {
+ Try {
+ Expand-Archive -Path $_.FullName -Force @expand_params
+ }
+ Catch {
+ Fail-Json -obj $result -message "Error recursively expanding '$src' to '$dest'! Msg: $($_.Exception.Message)"
+ }
+ If ($delete_archive) {
+ Remove-Item -LiteralPath $_.FullName -Force -WhatIf:$check_mode
+ $result.removed = $true
+ }
+ }
+ }
+
+ $result.changed = $true
+}
+
+If ($delete_archive) {
+ try {
+ Remove-Item -LiteralPath $src -Recurse -Force -WhatIf:$check_mode
+ }
+ catch {
+ Fail-Json -obj $result -message "failed to delete archive at '$src': $($_.Exception.Message)"
+ }
+ $result.removed = $true
+}
+Exit-Json $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_unzip.py b/ansible_collections/community/windows/plugins/modules/win_unzip.py
new file mode 100644
index 00000000..f5c46c80
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_unzip.py
@@ -0,0 +1,112 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2015, Phil Schwartz <schwartzmx@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_unzip
+short_description: Unzips compressed files and archives on the Windows node
+description:
+- Unzips compressed files and archives.
+- Supports .zip files natively.
+- Supports other formats supported by the Powershell Community Extensions (PSCX) module (basically everything 7zip supports).
+- For non-Windows targets, use the M(ansible.builtin.unarchive) module instead.
+requirements:
+- PSCX
+options:
+ src:
+ description:
+ - File to be unzipped (provide absolute path).
+ type: path
+ required: yes
+ dest:
+ description:
+ - Destination of zip file (provide absolute path of directory). If it does not exist, the directory will be created.
+ type: path
+ required: yes
+ delete_archive:
+ description:
+ - Remove the zip file, after unzipping.
+ type: bool
+ default: no
+ aliases: [ rm ]
+ recurse:
+ description:
+ - Recursively expand zipped files within the src file.
+ - Setting to a value of C(yes) requires the PSCX module to be installed.
+ type: bool
+ default: no
+ creates:
+ description:
+ - If this file or directory exists the specified src will not be extracted.
+ type: path
+ password:
+ description:
+ - If a zip file is encrypted with password.
+ - Passing a value to a password parameter requires the PSCX module to be installed.
+notes:
+- This module is not really idempotent, it will extract the archive every time, and report a change.
+- For extracting any compression types other than .zip, the PowerShellCommunityExtensions (PSCX) Module is required. This module (in conjunction with PSCX)
+ has the ability to recursively unzip files within the src zip file provided and also functionality for many other compression types. If the destination
+ directory does not exist, it will be created before unzipping the file. Specifying rm parameter will force removal of the src file after extraction.
+seealso:
+- module: ansible.builtin.unarchive
+author:
+- Phil Schwartz (@schwartzmx)
+'''
+
+EXAMPLES = r'''
+# This unzips a library that was downloaded with win_get_url, and removes the file after extraction
+# $ ansible -i hosts -m win_unzip -a "src=C:\LibraryToUnzip.zip dest=C:\Lib remove=yes" all
+
+- name: Unzip a bz2 (BZip) file
+ community.windows.win_unzip:
+ src: C:\Users\Phil\Logs.bz2
+ dest: C:\Users\Phil\OldLogs
+ creates: C:\Users\Phil\OldLogs
+
+- name: Unzip gz log
+ community.windows.win_unzip:
+ src: C:\Logs\application-error-logs.gz
+ dest: C:\ExtractedLogs\application-error-logs
+
+# Unzip .zip file, recursively decompresses the contained .gz files and removes all unneeded compressed files after completion.
+- name: Recursively decompress GZ files in ApplicationLogs.zip
+ community.windows.win_unzip:
+ src: C:\Downloads\ApplicationLogs.zip
+ dest: C:\Application\Logs
+ recurse: yes
+ delete_archive: yes
+
+- name: Install PSCX
+ community.windows.win_psmodule:
+ name: Pscx
+ state: present
+
+- name: Unzip .7z file which is password encrypted
+ community.windows.win_unzip:
+ src: C:\Downloads\ApplicationLogs.7z
+ dest: C:\Application\Logs
+ password: abcd
+ delete_archive: yes
+'''
+
+RETURN = r'''
+dest:
+ description: The provided destination path
+ returned: always
+ type: str
+ sample: C:\ExtractedLogs\application-error-logs
+removed:
+ description: Whether the module did remove any files during task run
+ returned: always
+ type: bool
+ sample: true
+src:
+ description: The provided source path
+ returned: always
+ type: str
+ sample: C:\Logs\application-error-logs.gz
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_user_profile.ps1 b/ansible_collections/community/windows/plugins/modules/win_user_profile.ps1
new file mode 100644
index 00000000..babe627c
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_user_profile.ps1
@@ -0,0 +1,169 @@
+#!powershell
+
+# Copyright: (c) 2019, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+$spec = @{
+ options = @{
+ name = @{ type = "str" }
+ remove_multiple = @{ type = "bool"; default = $false }
+ state = @{ type = "str"; default = "present"; choices = @("absent", "present") }
+ username = @{ type = "sid"; }
+ }
+ required_if = @(
+ @("state", "present", @("username")),
+ @("state", "absent", @("name", "username"), $true)
+ )
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+$module.Result.path = $null
+
+$name = $module.Params.name
+$remove_multiple = $module.Params.remove_multiple
+$state = $module.Params.state
+$username = $module.Params.username
+
+Add-CSharpType -AnsibleModule $module -References @'
+using System;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Ansible.WinUserProfile
+{
+ public class NativeMethods
+ {
+ [DllImport("Userenv.dll", CharSet = CharSet.Unicode)]
+ public static extern int CreateProfile(
+ [MarshalAs(UnmanagedType.LPWStr)] string pszUserSid,
+ [MarshalAs(UnmanagedType.LPWStr)] string pszUserName,
+ [Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszProfilePath,
+ UInt32 cchProfilePath);
+
+ [DllImport("Userenv.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ public static extern bool DeleteProfileW(
+ [MarshalAs(UnmanagedType.LPWStr)] string lpSidString,
+ IntPtr lpProfile,
+ IntPtr lpComputerName);
+
+ [DllImport("Userenv.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ public static extern bool GetProfilesDirectoryW(
+ [Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder lpProfileDir,
+ ref UInt32 lpcchSize);
+ }
+}
+'@
+
+Function Get-LastWin32ExceptionMessage {
+ param([int]$ErrorCode)
+ $exp = New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList $ErrorCode
+ $exp_msg = "{0} (Win32 ErrorCode {1} - 0x{1:X8})" -f $exp.Message, $ErrorCode
+ return $exp_msg
+}
+
+Function Get-ExpectedProfilePath {
+ param([String]$BaseName)
+
+ # Environment.GetFolderPath does not have an enumeration to get the base profile dir, use PInvoke instead
+ # and combine with the base name to return back to the user - best efforts
+ $profile_path_length = 0
+ [Ansible.WinUserProfile.NativeMethods]::GetProfilesDirectoryW($null,
+ [ref]$profile_path_length) > $null
+
+ $raw_profile_path = New-Object -TypeName System.Text.StringBuilder -ArgumentList $profile_path_length
+ $res = [Ansible.WinUserProfile.NativeMethods]::GetProfilesDirectoryW($raw_profile_path,
+ [ref]$profile_path_length)
+
+ if ($res -eq $false) {
+ $msg = Get-LastWin32ExceptionMessage -Error ([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())
+ $module.FailJson("Failed to determine profile path with the base name '$BaseName': $msg")
+ }
+ $profile_path = Join-Path -Path $raw_profile_path.ToString() -ChildPath $BaseName
+
+ return $profile_path
+}
+
+$profiles = Get-ChildItem -LiteralPath "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList"
+
+if ($state -eq "absent") {
+ if ($null -ne $username) {
+ $user_profiles = $profiles | Where-Object { $_.PSChildName -eq $username.Value }
+ }
+ else {
+ # If the username was not provided, or we are removing a profile for a deleted user, we need to try and find
+ # the correct SID to delete. We just verify that the path matches based on the name passed in
+ $expected_profile_path = Get-ExpectedProfilePath -BaseName $name
+
+ $user_profiles = $profiles | Where-Object {
+ $profile_path = (Get-ItemProperty -LiteralPath $_.PSPath -Name ProfileImagePath).ProfileImagePath
+ $profile_path -eq $expected_profile_path
+ }
+
+ if ($user_profiles.Length -gt 1 -and -not $remove_multiple) {
+ $msg = "Found multiple profiles matching the path '$expected_profile_path', set 'remove_multiple=True' to remove all the profiles for this match"
+ $module.FailJson($msg)
+ }
+ }
+
+ foreach ($user_profile in $user_profiles) {
+ $profile_path = (Get-ItemProperty -LiteralPath $user_profile.PSPath -Name ProfileImagePath).ProfileImagePath
+ if (-not $module.CheckMode) {
+ $res = [Ansible.WinUserProfile.NativeMethods]::DeleteProfileW($user_profile.PSChildName, [IntPtr]::Zero,
+ [IntPtr]::Zero)
+ if ($res -eq $false) {
+ $msg = Get-LastWin32ExceptionMessage -Error ([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())
+ $module.FailJson("Failed to delete the profile for $($user_profile.PSChildName): $msg")
+ }
+ }
+
+ # While we may have multiple profiles when the name option was used, it will always be the same path due to
+ # how we match name to a profile so setting it mutliple time sis fine
+ $module.Result.path = $profile_path
+ $module.Result.changed = $true
+ }
+}
+elseif ($state -eq "present") {
+ # Now we know the SID, see if the profile already exists
+ $user_profile = $profiles | Where-Object { $_.PSChildName -eq $username.Value }
+ if ($null -eq $user_profile) {
+ # In case a SID was set as the username we still need to make sure the SID is mapped to a valid local account
+ try {
+ $account_name = $username.Translate([System.Security.Principal.NTAccount])
+ }
+ catch [System.Security.Principal.IdentityNotMappedException] {
+ $module.FailJson("Fail to map the account '$($username.Value)' to a valid user")
+ }
+
+ # If the basename was not provided, determine it from the actual username
+ if ($null -eq $name) {
+ $name = $account_name.Value.Split('\', 2)[-1]
+ }
+
+ if ($module.CheckMode) {
+ $profile_path = Get-ExpectedProfilePath -BaseName $name
+ }
+ else {
+ $raw_profile_path = New-Object -TypeName System.Text.StringBuilder -ArgumentList 260
+ $res = [Ansible.WinUserProfile.NativeMethods]::CreateProfile($username.Value, $name, $raw_profile_path,
+ $raw_profile_path.Capacity)
+
+ if ($res -ne 0) {
+ $exp = [System.Runtime.InteropServices.Marshal]::GetExceptionForHR($res)
+ $module.FailJson("Failed to create profile for user '$username': $($exp.Message)")
+ }
+ $profile_path = $raw_profile_path.ToString()
+ }
+
+ $module.Result.changed = $true
+ $module.Result.path = $profile_path
+ }
+ else {
+ $module.Result.path = (Get-ItemProperty -LiteralPath $user_profile.PSPath -Name ProfileImagePath).ProfileImagePath
+ }
+}
+
+$module.ExitJson()
+
diff --git a/ansible_collections/community/windows/plugins/modules/win_user_profile.py b/ansible_collections/community/windows/plugins/modules/win_user_profile.py
new file mode 100644
index 00000000..280de155
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_user_profile.py
@@ -0,0 +1,109 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2019, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_user_profile
+short_description: Manages the Windows user profiles.
+description:
+- Used to create or remove user profiles on a Windows host.
+- This can be used to create a profile before a user logs on or delete a
+ profile when removing a user account.
+- A profile can be created for both a local or domain account.
+options:
+ name:
+ description:
+ - Specifies the base name for the profile path.
+ - When I(state) is C(present) this is used to create the profile for
+ I(username) at a specific path within the profile directory.
+ - This cannot be used to specify a path outside of the profile directory
+ but rather it specifies a folder(s) within this directory.
+ - If a profile for another user already exists at the same path, then a 3
+ digit incremental number is appended by Windows automatically.
+ - When I(state) is C(absent) and I(username) is not set, then the module
+ will remove all profiles that point to the profile path derived by this
+ value.
+ - This is useful if the account no longer exists but the profile still
+ remains.
+ type: str
+ remove_multiple:
+ description:
+ - When I(state) is C(absent) and the value for I(name) matches multiple
+ profiles the module will fail.
+ - Set this value to C(yes) to force the module to delete all the profiles
+ found.
+ default: no
+ type: bool
+ state:
+ description:
+ - Will ensure the profile exists when set to C(present).
+ - When creating a profile the I(username) option must be set to a valid
+ account.
+ - Will remove the profile(s) when set to C(absent).
+ - When removing a profile either I(username) must be set to a valid
+ account, or I(name) is set to the profile's base name.
+ default: present
+ choices:
+ - absent
+ - present
+ type: str
+ username:
+ description:
+ - The account name of security identifier (SID) for the profile.
+ - This must be set when I(state) is C(present) and must be a valid account
+ or the SID of a valid account.
+ - When I(state) is C(absent) then this must still be a valid account number
+ but the SID can be a deleted user's SID.
+ type: sid
+seealso:
+- module: ansible.windows.win_user
+- module: community.windows.win_domain_user
+author:
+- Jordan Borean (@jborean93)
+'''
+
+EXAMPLES = r'''
+- name: Create a profile for an account
+ community.windows.win_user_profile:
+ username: ansible-account
+ state: present
+
+- name: Create a profile for an account at C:\Users\ansible
+ community.windows.win_user_profile:
+ username: ansible-account
+ name: ansible
+ state: present
+
+- name: Remove a profile for a still valid account
+ community.windows.win_user_profile:
+ username: ansible-account
+ state: absent
+
+- name: Remove a profile for a deleted account
+ community.windows.win_user_profile:
+ name: ansible
+ state: absent
+
+- name: Remove a profile for a deleted account based on the SID
+ community.windows.win_user_profile:
+ username: S-1-5-21-3233007181-2234767541-1895602582-1305
+ state: absent
+
+- name: Remove multiple profiles that exist at the basename path
+ community.windows.win_user_profile:
+ name: ansible
+ state: absent
+ remove_multiple: yes
+'''
+
+RETURN = r'''
+path:
+ description: The full path to the profile for the account. This will be null
+ if C(state=absent) and no profile was deleted.
+ returned: always
+ type: str
+ sample: C:\Users\ansible
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_wait_for_process.ps1 b/ansible_collections/community/windows/plugins/modules/win_wait_for_process.ps1
new file mode 100644
index 00000000..fa9f5a63
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_wait_for_process.ps1
@@ -0,0 +1,176 @@
+#!powershell
+
+# Copyright: (c) 2017, Ansible Project
+# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+#Requires -Module Ansible.ModuleUtils.SID
+
+$spec = @{
+ options = @{
+ process_name_exact = @{ type = 'list'; elements = 'str' }
+ process_name_pattern = @{ type = 'str' }
+ pid = @{ type = 'int'; default = 0 }
+ owner = @{ type = 'str' }
+ sleep = @{ type = 'int'; default = 1 }
+ pre_wait_delay = @{ type = 'int'; default = 0 }
+ post_wait_delay = @{ type = 'int'; default = 0 }
+ process_min_count = @{ type = 'int'; default = 1 }
+ state = @{ type = 'str'; default = 'present'; choices = @( 'absent', 'present' ) }
+ timeout = @{ type = 'int'; default = 300 }
+ }
+ mutually_exclusive = @(
+ @( 'pid', 'process_name_exact' ),
+ @( 'pid', 'process_name_pattern' ),
+ @( 'process_name_exact', 'process_name_pattern' )
+ )
+ required_one_of = @(
+ , @( 'owner', 'pid', 'process_name_exact', 'process_name_pattern' )
+ )
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$process_name_exact = $module.Params.process_name_exact
+$process_name_pattern = $module.Params.process_name_pattern
+$process_id = $module.Params.pid # pid is a reserved variable in PowerShell, using process_id instead
+$owner = $module.Params.owner
+$sleep = $module.Params.sleep
+$pre_wait_delay = $module.Params.pre_wait_delay
+$post_wait_delay = $module.Params.post_wait_delay
+$process_min_count = $module.Params.process_min_count
+$state = $module.Params.state
+$timeout = $module.Params.timeout
+
+$module.Result.changed = $false
+$module.Result.elapsed = 0
+$module.Result.matched_processes = @()
+
+# Validate the input
+if ($state -eq "absent" -and $sleep -ne 1) {
+ $module.Warn("Parameter 'sleep' has no effect when waiting for a process to stop.")
+}
+
+if ($state -eq "absent" -and $process_min_count -ne 1) {
+ $module.Warn("Parameter 'process_min_count' has no effect when waiting for a process to stop.")
+}
+
+if ($owner -and ("IncludeUserName" -notin (Get-Command -Name Get-Process).Parameters.Keys)) {
+ $module.FailJson("This version of Powershell does not support filtering processes by 'owner'.")
+}
+
+Function Get-FilteredProcess {
+ [cmdletbinding()]
+ Param(
+ [String]
+ $Owner,
+ $ProcessNameExact,
+ $ProcessNamePattern,
+ [int]
+ $ProcessId
+ )
+
+ $FilteredProcesses = @()
+
+ try {
+ $Processes = Get-Process -IncludeUserName
+ $SupportsUserNames = $true
+ }
+ catch [System.Management.Automation.ParameterBindingException] {
+ $Processes = Get-Process
+ $SupportsUserNames = $false
+ }
+
+ foreach ($Process in $Processes) {
+
+ # If a process name was specified in the filter, validate that here.
+ if ($ProcessNamePattern) {
+ if ($Process.ProcessName -notmatch $ProcessNamePattern) {
+ continue
+ }
+ }
+
+ # If a process name was specified in the filter, validate that here.
+ if ($ProcessNameExact -and $ProcessNameExact -notcontains $Process.ProcessName) {
+ continue
+ }
+
+ # If a PID was specified in the filter, validate that here.
+ if ($ProcessId -and $ProcessId -ne 0) {
+ if ($ProcessId -ne $Process.Id) {
+ continue
+ }
+ }
+
+ # If an owner was specified in the filter, validate that here.
+ if ($Owner) {
+ if (-not $Process.UserName) {
+ continue
+ }
+ elseif ((Convert-ToSID($Owner)) -ne (Convert-ToSID($Process.UserName))) {
+ # NOTE: This is rather expensive
+ continue
+ }
+ }
+
+ if ($SupportsUserNames -eq $true) {
+ $FilteredProcesses += @{ name = $Process.ProcessName; pid = $Process.Id; owner = $Process.UserName }
+ }
+ else {
+ $FilteredProcesses += @{ name = $Process.ProcessName; pid = $Process.Id }
+ }
+ }
+
+ return , $FilteredProcesses
+}
+
+$module_start = Get-Date
+Start-Sleep -Seconds $pre_wait_delay
+
+if ($state -eq "present" ) {
+
+ # Wait for a process to start
+ do {
+
+ $Processes = Get-FilteredProcess -Owner $owner -ProcessNameExact $process_name_exact -ProcessNamePattern $process_name_pattern -ProcessId $process_id
+ $module.Result.matched_processes = $Processes
+
+ if ($Processes.count -ge $process_min_count) {
+ break
+ }
+
+ if (((Get-Date) - $module_start).TotalSeconds -gt $timeout) {
+ $module.Result.elapsed = ((Get-Date) - $module_start).TotalSeconds
+ $module.FailJson("Timed out while waiting for process(es) to start")
+ }
+
+ Start-Sleep -Seconds $sleep
+
+ } while ($true)
+
+}
+elseif ($state -eq "absent") {
+
+ # Wait for a process to stop
+ $Processes = Get-FilteredProcess -Owner $owner -ProcessNameExact $process_name_exact -ProcessNamePattern $process_name_pattern -ProcessId $process_id
+ $module.Result.matched_processes = $Processes
+
+ if ($Processes.count -gt 0 ) {
+ try {
+ # This may randomly fail when used on specially protected processes (think: svchost)
+ Wait-Process -Id $Processes.pid -Timeout $timeout
+ }
+ catch [System.TimeoutException] {
+ $module.Result.elapsed = ((Get-Date) - $module_start).TotalSeconds
+ $module.FailJson("Timeout while waiting for process(es) to stop")
+ }
+ }
+
+}
+
+Start-Sleep -Seconds $post_wait_delay
+$module.Result.elapsed = ((Get-Date) - $module_start).TotalSeconds
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_wait_for_process.py b/ansible_collections/community/windows/plugins/modules/win_wait_for_process.py
new file mode 100644
index 00000000..6f483d10
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_wait_for_process.py
@@ -0,0 +1,128 @@
+#!/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)
+
+DOCUMENTATION = r'''
+---
+module: win_wait_for_process
+short_description: Waits for a process to exist or not exist before continuing.
+description:
+- Waiting for a process to start or stop.
+- This is useful when Windows services behave poorly and do not enumerate external dependencies in their manifest.
+options:
+ process_name_exact:
+ description:
+ - The name of the process(es) for which to wait. The name of the process(es) should not include the file extension suffix.
+ type: list
+ elements: str
+ process_name_pattern:
+ description:
+ - RegEx pattern matching desired process(es).
+ type: str
+ sleep:
+ description:
+ - Number of seconds to sleep between checks.
+ - Only applies when waiting for a process to start. Waiting for a process to start
+ does not have a native non-polling mechanism. Waiting for a stop uses native PowerShell
+ and does not require polling.
+ type: int
+ default: 1
+ process_min_count:
+ description:
+ - Minimum number of process matching the supplied pattern to satisfy C(present) condition.
+ - Only applies to C(present).
+ type: int
+ default: 1
+ pid:
+ description:
+ - The PID of the process.
+ default: 0
+ type: int
+ owner:
+ description:
+ - The owner of the process.
+ - Requires PowerShell version 4.0 or newer.
+ type: str
+ pre_wait_delay:
+ description:
+ - Seconds to wait before checking processes.
+ type: int
+ default: 0
+ post_wait_delay:
+ description:
+ - Seconds to wait after checking for processes.
+ type: int
+ default: 0
+ state:
+ description:
+ - When checking for a running process C(present) will block execution
+ until the process exists, or until the timeout has been reached.
+ C(absent) will block execution until the process no longer exists,
+ or until the timeout has been reached.
+ - When waiting for C(present), the module will return changed only if
+ the process was not present on the initial check but became present on
+ subsequent checks.
+ - If, while waiting for C(absent), new processes matching the supplied
+ pattern are started, these new processes will not be included in the
+ action.
+ type: str
+ default: present
+ choices: [ absent, present ]
+ timeout:
+ description:
+ - The maximum number of seconds to wait for a for a process to start or stop
+ before erroring out.
+ type: int
+ default: 300
+seealso:
+- module: ansible.builtin.wait_for
+- module: ansible.windows.win_wait_for
+author:
+- Charles Crossan (@crossan007)
+'''
+
+EXAMPLES = r'''
+- name: Wait 300 seconds for all Oracle VirtualBox processes to stop. (VBoxHeadless, VirtualBox, VBoxSVC)
+ community.windows.win_wait_for_process:
+ process_name_pattern: 'v(irtual)?box(headless|svc)?'
+ state: absent
+ timeout: 500
+
+- name: Wait 300 seconds for 3 instances of cmd to start, waiting 5 seconds between each check
+ community.windows.win_wait_for_process:
+ process_name_exact: cmd
+ state: present
+ timeout: 500
+ sleep: 5
+ process_min_count: 3
+'''
+
+RETURN = r'''
+elapsed:
+ description: The elapsed seconds between the start of poll and the end of the module.
+ returned: always
+ type: float
+ sample: 3.14159265
+matched_processes:
+ description: List of matched processes (either stopped or started).
+ returned: always
+ type: complex
+ contains:
+ name:
+ description: The name of the matched process.
+ returned: always
+ type: str
+ sample: svchost
+ owner:
+ description: The owner of the matched process.
+ returned: when supported by PowerShell
+ type: str
+ sample: NT AUTHORITY\SYSTEM
+ pid:
+ description: The PID of the matched process.
+ returned: always
+ type: int
+ sample: 7908
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_wakeonlan.ps1 b/ansible_collections/community/windows/plugins/modules/win_wakeonlan.ps1
new file mode 100644
index 00000000..ea4a4a5e
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_wakeonlan.ps1
@@ -0,0 +1,52 @@
+#!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
+
+$spec = @{
+ options = @{
+ mac = @{ type = 'str'; required = $true }
+ broadcast = @{ type = 'str'; default = '255.255.255.255' }
+ port = @{ type = 'int'; default = 7 }
+ }
+ supports_check_mode = $true
+}
+
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$module.Result.changed = $false
+
+$mac = $module.Params.mac
+$mac_orig = $module.Params.mac
+$broadcast = $module.Params.broadcast
+$port = $module.Params.port
+
+$broadcast = [Net.IPAddress]::Parse($broadcast)
+
+# Remove possible separator from MAC address
+if ($mac.Length -eq (12 + 5)) {
+ $mac = $mac.Replace($mac.Substring(2, 1), "")
+}
+
+# If we don't end up with 12 hexadecimal characters, fail
+if ($mac.Length -ne 12) {
+ $module.FailJson("Incorrect MAC address: $mac_orig")
+}
+
+# Create payload for magic packet
+# TODO: Catch possible conversion errors
+$target = 0, 2, 4, 6, 8, 10 | ForEach-Object { [convert]::ToByte($mac.Substring($_, 2), 16) }
+$data = (, [byte]255 * 6) + ($target * 20)
+
+# Broadcast payload to network
+$udpclient = new-Object System.Net.Sockets.UdpClient
+if (-not $module.CheckMode) {
+ $udpclient.Connect($broadcast, $port)
+ [void] $udpclient.Send($data, 102)
+}
+
+$module.Result.changed = $true
+
+$module.ExitJson()
diff --git a/ansible_collections/community/windows/plugins/modules/win_wakeonlan.py b/ansible_collections/community/windows/plugins/modules/win_wakeonlan.py
new file mode 100644
index 00000000..b9ba920b
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_wakeonlan.py
@@ -0,0 +1,57 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2017, Dag Wieers <dag@wieers.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_wakeonlan
+short_description: Send a magic Wake-on-LAN (WoL) broadcast packet
+description:
+- The C(win_wakeonlan) module sends magic Wake-on-LAN (WoL) broadcast packets.
+- For non-Windows targets, use the M(community.general.wakeonlan) module instead.
+options:
+ mac:
+ description:
+ - MAC address to send Wake-on-LAN broadcast packet for.
+ type: str
+ required: yes
+ broadcast:
+ description:
+ - Network broadcast address to use for broadcasting magic Wake-on-LAN packet.
+ type: str
+ default: 255.255.255.255
+ port:
+ description:
+ - UDP port to use for magic Wake-on-LAN packet.
+ type: int
+ default: 7
+todo:
+- Does not have SecureOn password support
+notes:
+- This module sends a magic packet, without knowing whether it worked. It always report a change.
+- Only works if the target system was properly configured for Wake-on-LAN (in the BIOS and/or the OS).
+- Some BIOSes have a different (configurable) Wake-on-LAN boot order (i.e. PXE first).
+seealso:
+- module: community.general.wakeonlan
+author:
+- Dag Wieers (@dagwieers)
+'''
+
+EXAMPLES = r'''
+- name: Send a magic Wake-on-LAN packet to 00:00:5E:00:53:66
+ community.windows.win_wakeonlan:
+ mac: 00:00:5E:00:53:66
+ broadcast: 192.0.2.23
+
+- name: Send a magic Wake-On-LAN packet on port 9 to 00-00-5E-00-53-66
+ community.windows.win_wakeonlan:
+ mac: 00-00-5E-00-53-66
+ port: 9
+ delegate_to: remote_system
+'''
+
+RETURN = r'''
+# Default return values
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_webpicmd.ps1 b/ansible_collections/community/windows/plugins/modules/win_webpicmd.ps1
new file mode 100644
index 00000000..f888d7d0
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_webpicmd.ps1
@@ -0,0 +1,103 @@
+#!powershell
+
+# Copyright: (c) 2015, Peter Mounce <public@neverrunwithscissors.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#Requires -Module Ansible.ModuleUtils.Legacy
+
+$ErrorActionPreference = "Stop"
+
+Function Find-InstalledCommand {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory = $true, Position = 0)] [string] $command
+ )
+ $installed = get-command $command -erroraction Ignore
+ write-verbose "$installed"
+ if ($installed) {
+ return $installed
+ }
+ return $null
+}
+
+Function Find-WebPiCmd {
+ [CmdletBinding()]
+ param()
+ $p = Find-InstalledCommand "webpicmd.exe"
+ if ($null -ne $p) {
+ return $p
+ }
+ $a = Find-InstalledCommand "c:\programdata\chocolatey\bin\webpicmd.exe"
+ if ($null -ne $a) {
+ return $a
+ }
+ Throw "webpicmd.exe is not installed. It must be installed (use chocolatey)"
+}
+
+Function Test-IsInstalledFromWebPI {
+ [CmdletBinding()]
+
+ param(
+ [Parameter(Mandatory = $true, Position = 0)]
+ [string]$package
+ )
+
+ $results = &$executable /list /listoption:installed
+
+ if ($LastExitCode -ne 0) {
+ $result.webpicmd_error_cmd = $cmd
+ $result.webpicmd_error_log = "$results"
+
+ Throw "Error checking installation status for $package"
+ }
+ Write-Verbose "$results"
+
+ if ($results -match "^$package\s+") {
+ return $true
+ }
+
+ return $false
+}
+
+Function Install-WithWebPICmd {
+ [CmdletBinding()]
+
+ param(
+ [Parameter(Mandatory = $true, Position = 0)]
+ [string]$package
+ )
+
+ $results = &$executable /install /products:$package /accepteula /suppressreboot
+
+ if ($LastExitCode -ne 0) {
+ $result.webpicmd_error_cmd = $cmd
+ $result.webpicmd_error_log = "$results"
+ Throw "Error installing $package"
+ }
+
+ write-verbose "$results"
+
+ if ($results -match "Install of Products: SUCCESS") {
+ $result.changed = $true
+ }
+}
+
+$result = @{
+ changed = $false
+}
+
+$params = Parse-Args $args
+
+$package = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
+
+Try {
+ $script:executable = Find-WebPiCmd
+ if ((Test-IsInstalledFromWebPI -package $package) -eq $false) {
+ Install-WithWebPICmd -package $package
+ }
+
+ Exit-Json $result
+}
+Catch {
+ Fail-Json $result $_.Exception.Message
+}
diff --git a/ansible_collections/community/windows/plugins/modules/win_webpicmd.py b/ansible_collections/community/windows/plugins/modules/win_webpicmd.py
new file mode 100644
index 00000000..16745fb0
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_webpicmd.py
@@ -0,0 +1,35 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2015, Peter Mounce <public@neverrunwithscissors.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_webpicmd
+short_description: Installs packages using Web Platform Installer command-line
+description:
+ - Installs packages using Web Platform Installer command-line
+ (U(http://www.iis.net/learn/install/web-platform-installer/web-platform-installer-v4-command-line-webpicmdexe-rtw-release)).
+ - Must be installed and present in PATH (see M(chocolatey.chocolatey.win_chocolatey) module; 'webpicmd' is the package name, and you must install
+ 'lessmsi' first too)?
+ - Install IIS first (see M(ansible.windows.win_feature) module).
+notes:
+ - Accepts EULAs and suppresses reboot - you will need to check manage reboots yourself (see M(ansible.windows.win_reboot) module)
+options:
+ name:
+ description:
+ - Name of the package to be installed.
+ type: str
+ required: yes
+seealso:
+- module: ansible.windows.win_package
+author:
+- Peter Mounce (@petemounce)
+'''
+
+EXAMPLES = r'''
+- name: Install URLRewrite2.
+ community.windows.win_webpicmd:
+ name: URLRewrite2
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_xml.ps1 b/ansible_collections/community/windows/plugins/modules/win_xml.ps1
new file mode 100644
index 00000000..6f5db138
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_xml.ps1
@@ -0,0 +1,285 @@
+#!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
+#Requires -Module Ansible.ModuleUtils.Backup
+
+Set-StrictMode -Version 2
+
+function Copy-Xml($dest, $src, $xmlorig) {
+ if ($src.get_NodeType() -eq "Text") {
+ $dest.set_InnerText($src.get_InnerText())
+ }
+
+ if ($src.get_HasAttributes()) {
+ foreach ($attr in $src.get_Attributes()) {
+ $dest.SetAttribute($attr.get_Name(), $attr.get_Value())
+ }
+ }
+
+ if ($src.get_HasChildNodes()) {
+ foreach ($childnode in $src.get_ChildNodes()) {
+ if ($childnode.get_NodeType() -eq "Element") {
+ $newnode = $xmlorig.CreateElement($childnode.get_Name(), $xmlorig.get_DocumentElement().get_NamespaceURI())
+ Copy-Xml -dest $newnode -src $childnode -xmlorig $xmlorig
+ $dest.AppendChild($newnode) | Out-Null
+ }
+ elseif ($childnode.get_NodeType() -eq "Text") {
+ $dest.set_InnerText($childnode.get_InnerText())
+ }
+ }
+ }
+}
+
+function Compare-XmlDoc($actual, $expected) {
+ if ($actual.get_Name() -ne $expected.get_Name()) {
+ throw "Actual name not same as expected: actual=" + $actual.get_Name() + ", expected=" + $expected.get_Name()
+ }
+ ##attributes...
+
+ if (($actual.get_NodeType() -eq "Element") -and ($expected.get_NodeType() -eq "Element")) {
+ if ($actual.get_HasAttributes() -and $expected.get_HasAttributes()) {
+ if ($actual.get_Attributes().Count -ne $expected.get_Attributes().Count) {
+ throw "attribute mismatch for actual=" + $actual.get_Name()
+ }
+ for ($i = 0; $i -lt $expected.get_Attributes().Count; $i = $i + 1) {
+ if ($expected.get_Attributes()[$i].get_Name() -ne $actual.get_Attributes()[$i].get_Name()) {
+ throw "attribute name mismatch for actual=" + $actual.get_Name()
+ }
+ if ($expected.get_Attributes()[$i].get_Value() -ne $actual.get_Attributes()[$i].get_Value()) {
+ throw "attribute value mismatch for actual=" + $actual.get_Name()
+ }
+ }
+ }
+
+ if (($actual.get_HasAttributes() -and !$expected.get_HasAttributes()) -or (!$actual.get_HasAttributes() -and $expected.get_HasAttributes())) {
+ throw "attribute presence mismatch for actual=" + $actual.get_Name()
+ }
+ }
+
+ ##children
+ if ($expected.get_ChildNodes().Count -ne $actual.get_ChildNodes().Count) {
+ throw "child node mismatch. for actual=" + $actual.get_Name()
+ }
+
+ for ($i = 0; $i -lt $expected.get_ChildNodes().Count; $i = $i + 1) {
+ if (-not $actual.get_ChildNodes()[$i]) {
+ throw "actual missing child nodes. for actual=" + $actual.get_Name()
+ }
+ Compare-XmlDoc $expected.get_ChildNodes()[$i] $actual.get_ChildNodes()[$i]
+ }
+
+ if ($expected.get_InnerText()) {
+ if ($expected.get_InnerText() -ne $actual.get_InnerText()) {
+ throw "inner text mismatch for actual=" + $actual.get_Name()
+ }
+ }
+ elseif ($actual.get_InnerText()) {
+ throw "actual has inner text but expected does not for actual=" + $actual.get_Name()
+ }
+}
+
+
+function Save-ChangedXml($xmlorig, $result, $message, $check_mode, $backup) {
+ $result.changed = $true
+ if (-Not $check_mode) {
+ if ($backup) {
+ $result.backup_file = Backup-File -path $dest -WhatIf:$check_mode
+ # Ensure backward compatibility (deprecate in future)
+ $result.backup = $result.backup_file
+ }
+ $xmlorig.Save($dest)
+ $result.msg = $message
+ }
+ else {
+ $result.msg += " check mode"
+ }
+}
+
+$params = Parse-Args $args -supports_check_mode $true
+$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
+
+$debug_level = Get-AnsibleParam -obj $params -name "_ansible_verbosity" -type "int"
+$debug = $debug_level -gt 2
+
+$dest = Get-AnsibleParam $params "path" -type "path" -FailIfEmpty $true -aliases "dest", "file"
+$fragment = Get-AnsibleParam $params "fragment" -type "str" -aliases "xmlstring"
+$xpath = Get-AnsibleParam $params "xpath" -type "str" -FailIfEmpty $true
+$backup = Get-AnsibleParam $params "backup" -type "bool" -Default $false
+$type = Get-AnsibleParam $params "type" -type "str" -Default "element" -ValidateSet "element", "attribute", "text"
+$attribute = Get-AnsibleParam $params "attribute" -type "str" -FailIfEmpty ($type -eq "attribute")
+$state = Get-AnsibleParam $params "state" -type "str" -Default "present"
+$count = Get-AnsibleParam $params "count" -type "bool" -Default $false
+
+$result = @{
+ changed = $false
+}
+
+If (-Not (Test-Path -LiteralPath $dest -PathType Leaf)) {
+ Fail-Json $result "Specified path $dest does not exist or is not a file."
+}
+
+$xmlorig = New-Object -TypeName System.Xml.XmlDocument
+$xmlorig.XmlResolver = $null
+Try {
+ $xmlorig.Load($dest)
+}
+Catch {
+ Fail-Json $result "Failed to parse file at '$dest' as an XML document: $($_.Exception.Message)"
+}
+
+$namespaceMgr = New-Object System.Xml.XmlNamespaceManager $xmlorig.NameTable
+$namespace = $xmlorig.DocumentElement.NamespaceURI
+$localname = $xmlorig.DocumentElement.LocalName
+
+$namespaceMgr.AddNamespace($xmlorig.$localname.SchemaInfo.Prefix, $namespace)
+
+$nodeList = $xmlorig.SelectNodes($xpath, $namespaceMgr)
+$nodeListCount = $nodeList.get_Count()
+if ($count) {
+ $result.count = $nodeListCount
+ if (-not $fragment) {
+ Exit-Json $result
+ }
+}
+## Exit early if xpath did not match any nodes
+if ($nodeListCount -eq 0) {
+ $result.msg = "The supplied xpath did not match any nodes. If this is unexpected, check your xpath is valid for the xml file at supplied path."
+ Exit-Json $result
+}
+
+$changed = $false
+$result.msg = "not changed"
+
+if ($type -eq "element") {
+ if ($state -eq "absent") {
+
+ $removals = [System.Collections.Generic.List[String]]@()
+
+ foreach ($node in $nodeList) {
+ # there are some nodes that match xpath, delete without comparing them to fragment
+ if (-Not $check_mode) {
+ [void]$node.get_ParentNode().RemoveChild($node)
+ $changed = $true
+ }
+
+ if ($debug) {
+ $removals.Add($node.get_OuterXml())
+ }
+ }
+
+ if ($removals) {
+ $result.removed = $removals -join ", "
+ }
+ }
+ else {
+ # state -eq 'present'
+ $xmlfragment = $null
+ Try {
+ $xmlfragment = [xml]$fragment
+ }
+ Catch {
+ Fail-Json $result "Failed to parse fragment as XML: $($_.Exception.Message)"
+ }
+
+ foreach ($node in $nodeList) {
+ $candidate = $xmlorig.CreateElement($xmlfragment.get_DocumentElement().get_Name(), $xmlorig.get_DocumentElement().get_NamespaceURI())
+ Copy-Xml -dest $candidate -src $xmlfragment.DocumentElement -xmlorig $xmlorig
+
+ if ($node.get_NodeType() -eq "Document") {
+ $node = $node.get_DocumentElement()
+ }
+ $elements = $node.get_ChildNodes()
+ [bool]$present = $false
+ [bool]$changed = $false
+ if ($elements.get_Count()) {
+ if ($debug) {
+ $err = @()
+ $result.err = { $err }.Invoke()
+ }
+ foreach ($element in $elements) {
+ try {
+ Compare-XmlDoc $candidate $element
+ $present = $true
+ break
+ }
+ catch {
+ if ($debug) {
+ $result.err.Add($_.Exception.ToString())
+ }
+ }
+ }
+ if (-Not $present -and ($state -eq "present")) {
+ [void]$node.AppendChild($candidate)
+ $result.msg = $result.msg + "xml added "
+ $changed = $true
+ }
+ }
+ }
+ }
+}
+elseif ($type -eq "text") {
+ foreach ($node in $nodeList) {
+ if ($node.get_InnerText() -ne $fragment) {
+ $node.set_InnerText($fragment)
+ $changed = $true
+ }
+ }
+}
+elseif ($type -eq "attribute") {
+ foreach ($node in $nodeList) {
+ if ($state -eq 'present') {
+ if ($node.NodeType -eq 'Attribute') {
+ if ($node.Name -eq $attribute -and $node.Value -ne $fragment ) {
+ # this is already the attribute with the right name, so just set its value to match fragment
+ $node.Value = $fragment
+ $changed = $true
+ }
+ }
+ else {
+ # assume NodeType is Element
+ if (!$node.HasAttribute($attribute) -or ($node.$attribute -ne $fragment)) {
+ if (!$node.HasAttribute($attribute)) {
+ # add attribute to Element if missing
+ $node.SetAttributeNode($attribute, $xmlorig.get_DocumentElement().get_NamespaceURI())
+ }
+ #set the attribute into the element
+ $node.SetAttribute($attribute, $fragment)
+ $changed = $true
+ }
+ }
+ }
+ elseif ($state -eq 'absent') {
+ if ($node.NodeType -eq 'Attribute') {
+ $attrNode = [System.Xml.XmlAttribute]$node
+ $parent = $attrNode.OwnerElement
+ $parent.RemoveAttribute($attribute)
+ $changed = $true
+ }
+ else {
+ # element node processing
+ if ($node.Name -eq $attribute ) {
+ # note not caring about the state of 'fragment' at this point
+ $node.RemoveAttribute($attribute)
+ $changed = $true
+ }
+ }
+ }
+ else {
+ Add-Warning $result "Unexpected state when processing attribute $($attribute), add was $add, state was $state"
+ }
+ }
+}
+if ($changed) {
+ if ($state -eq "absent") {
+ $summary = "$type removed"
+ }
+ else {
+ $summary = "$type changed"
+ }
+ Save-ChangedXml -xmlorig $xmlorig -result $result -message $summary -check_mode $check_mode -backup $backup
+}
+
+Exit-Json $result
diff --git a/ansible_collections/community/windows/plugins/modules/win_xml.py b/ansible_collections/community/windows/plugins/modules/win_xml.py
new file mode 100644
index 00000000..a069c7b5
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_xml.py
@@ -0,0 +1,139 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2018, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_xml
+short_description: Manages XML file content on Windows hosts
+description:
+ - Manages XML nodes, attributes and text, using xpath to select which xml nodes need to be managed.
+ - XML fragments, formatted as strings, are used to specify the desired state of a part or parts of XML files on remote Windows servers.
+ - For non-Windows targets, use the M(community.general.xml) module instead.
+options:
+ attribute:
+ description:
+ - The attribute name if the type is 'attribute'.
+ - Required if C(type=attribute).
+ type: str
+ count:
+ description:
+ - When set to C(yes), return the number of nodes matched by I(xpath).
+ type: bool
+ default: false
+ 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
+ fragment:
+ description:
+ - The string representation of the XML fragment expected at xpath. Since ansible 2.9 not required when I(state=absent), or when I(count=yes).
+ type: str
+ required: false
+ aliases: [ xmlstring ]
+ path:
+ description:
+ - Path to the file to operate on.
+ type: path
+ required: true
+ aliases: [ dest, file ]
+ state:
+ description:
+ - Set or remove the nodes (or attributes) matched by I(xpath).
+ type: str
+ default: present
+ choices: [ present, absent ]
+ type:
+ description:
+ - The type of XML node you are working with.
+ type: str
+ default: element
+ choices: [ attribute, element, text ]
+ xpath:
+ description:
+ - Xpath to select the node or nodes to operate on.
+ type: str
+ required: true
+author:
+ - Richard Levenberg (@richardcs)
+ - Jon Hawkesworth (@jhawkesworth)
+notes:
+ - Only supports operating on xml elements, attributes and text.
+ - Namespace, processing-instruction, command and document node types cannot be modified with this module.
+seealso:
+ - module: community.general.xml
+ description: XML manipulation for Posix hosts.
+ - name: w3shools XPath tutorial
+ description: A useful tutorial on XPath
+ link: https://www.w3schools.com/xml/xpath_intro.asp
+'''
+
+EXAMPLES = r'''
+- name: Apply our filter to Tomcat web.xml
+ community.windows.win_xml:
+ path: C:\apache-tomcat\webapps\myapp\WEB-INF\web.xml
+ fragment: '<filter><filter-name>MyFilter</filter-name><filter-class>com.example.MyFilter</filter-class></filter>'
+ xpath: '/*'
+
+- name: Apply sslEnabledProtocols to Tomcat's server.xml
+ community.windows.win_xml:
+ path: C:\Tomcat\conf\server.xml
+ xpath: '//Server/Service[@name="Catalina"]/Connector[@port="9443"]'
+ attribute: 'sslEnabledProtocols'
+ fragment: 'TLSv1,TLSv1.1,TLSv1.2'
+ type: attribute
+
+- name: remove debug configuration nodes from nlog.conf
+ community.windows.win_xml:
+ path: C:\IISApplication\nlog.conf
+ xpath: /nlog/rules/logger[@name="debug"]/descendant::*
+ state: absent
+
+- name: count configured connectors in Tomcat's server.xml
+ community.windows.win_xml:
+ path: C:\Tomcat\conf\server.xml
+ xpath: //Server/Service/Connector
+ count: yes
+ register: connector_count
+
+- name: show connector count
+ debug:
+ msg="Connector count is {{connector_count.count}}"
+
+- name: ensure all lang=en attributes to lang=nl
+ community.windows.win_xml:
+ path: C:\Data\Books.xml
+ xpath: //@[lang="en"]
+ attribute: lang
+ fragment: nl
+ type: attribute
+
+'''
+
+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
+count:
+ description: Number of nodes matched by xpath.
+ returned: if count=yes
+ type: int
+ sample: 33
+msg:
+ description: What was done.
+ returned: always
+ type: str
+ sample: "xml added"
+err:
+ description: XML comparison exceptions.
+ returned: always, for type element and -vvv or more
+ type: list
+ sample: attribute mismatch for actual=string
+'''
diff --git a/ansible_collections/community/windows/plugins/modules/win_zip.ps1 b/ansible_collections/community/windows/plugins/modules/win_zip.ps1
new file mode 100644
index 00000000..08cedfb7
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_zip.ps1
@@ -0,0 +1,73 @@
+#!powershell
+
+# Copyright: (c) 2021, Kento Yagisawa <thel.vadam2485@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+#AnsibleRequires -CSharpUtil Ansible.Basic
+
+$spec = @{
+ options = @{
+ # Need to support \* which type='path' does not, the path is expanded further down.
+ src = @{ type = 'str'; required = $true }
+ dest = @{ type = 'path'; required = $true }
+ }
+ supports_check_mode = $true
+}
+$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
+
+$src = [Environment]::ExpandEnvironmentVariables($module.Params.src)
+$dest = $module.Params.dest
+
+$srcFile = [System.IO.Path]::GetFileName($src)
+$compressionLevel = [System.IO.Compression.CompressionLevel]::Optimal
+$encoding = New-Object -TypeName System.Text.UTF8Encoding -ArgumentList $false
+$srcWildcard = $false
+
+# If the path ends with '\*' we want to include the dir contents and not the dir itself
+If ($src -match '\\\*$') {
+ $srcWildcard = $true
+ $src = $src.Substring(0, $src.Length - 2)
+}
+
+If (-not (Test-Path -LiteralPath $src)) {
+ $module.FailJson("The source file or directory '$src' does not exist.")
+}
+
+If ($dest -notlike "*.zip") {
+ $module.FailJson("The destination zip file path '$dest' need to be zip file path.")
+}
+
+If (Test-Path -LiteralPath $dest) {
+ $module.Result.msg = "The destination zip file '$dest' already exists."
+ $module.ExitJson()
+}
+
+# Check .NET v4.5 or later version exists or not
+try {
+ Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction Stop
+}
+catch {
+ $module.FailJson(".NET Framework 4.5 or later version needs to be installed.", $_)
+}
+
+Function Compress-Zip($src, $dest) {
+ If (-not $module.CheckMode) {
+ If (Test-Path -LiteralPath $src -PathType Container) {
+ [System.IO.Compression.ZipFile]::CreateFromDirectory($src, $dest, $compressionLevel, (-not $srcWildcard), $encoding)
+ }
+ Else {
+ $zip = [System.IO.Compression.ZipFile]::Open($dest, 'Update')
+ try {
+ [void][System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, $src, $srcFile, $compressionLevel)
+ }
+ finally {
+ $zip.Dispose()
+ }
+ }
+ }
+ $module.Result.changed = $true
+}
+
+Compress-Zip -src $src -dest $dest
+
+$module.ExitJson() \ No newline at end of file
diff --git a/ansible_collections/community/windows/plugins/modules/win_zip.py b/ansible_collections/community/windows/plugins/modules/win_zip.py
new file mode 100644
index 00000000..53ea1458
--- /dev/null
+++ b/ansible_collections/community/windows/plugins/modules/win_zip.py
@@ -0,0 +1,53 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2021, Kento Yagisawa <thel.vadam2485@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+DOCUMENTATION = r'''
+---
+module: win_zip
+short_description: Compress file or directory as zip archive on the Windows node
+description:
+- Compress file or directory as zip archive.
+- For non-Windows targets, use the M(community.general.archive) module instead.
+notes:
+- The filenames in the zip are encoded using UTF-8.
+requirements:
+- .NET Framework 4.5 or later
+options:
+ src:
+ description:
+ - File or directory path to be zipped (provide absolute path on the target node).
+ - When a directory path the directory is zipped as the root entry in the archive.
+ - Specify C(\*) to the end of I(src) to zip the contents of the directory and not the directory itself.
+ type: str
+ required: yes
+ dest:
+ description:
+ - Destination path of zip file (provide absolute path of zip file on the target node).
+ type: path
+ required: yes
+seealso:
+- module: community.general.archive
+author:
+- Kento Yagisawa (@hiyoko_taisa)
+'''
+
+EXAMPLES = r'''
+- name: Compress a file
+ community.windows.win_zip:
+ src: C:\Users\hiyoko\log.txt
+ dest: C:\Users\hiyoko\log.zip
+
+- name: Compress a directory as the root of the archive
+ community.windows.win_zip:
+ src: C:\Users\hiyoko\log
+ dest: C:\Users\hiyoko\log.zip
+
+- name: Compress the directories contents
+ community.windows.win_zip:
+ src: C:\Users\hiyoko\log\*
+ dest: C:\Users\hiyoko\log.zip
+
+'''