mirror of https://github.com/ansible/ansible.git
Migrated to ansible.windows
parent
829eb0f1d1
commit
48d4e9370e
File diff suppressed because it is too large
Load Diff
@ -1,58 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
|
||||
$results = @{changed=$false}
|
||||
|
||||
$parsed_args = Parse-Args $args
|
||||
$jid = Get-AnsibleParam $parsed_args "jid" -failifempty $true -resultobj $results
|
||||
$mode = Get-AnsibleParam $parsed_args "mode" -Default "status" -ValidateSet "status","cleanup"
|
||||
|
||||
# parsed in from the async_status action plugin
|
||||
$async_dir = Get-AnsibleParam $parsed_args "_async_dir" -type "path" -failifempty $true
|
||||
|
||||
$log_path = [System.IO.Path]::Combine($async_dir, $jid)
|
||||
|
||||
If(-not $(Test-Path $log_path))
|
||||
{
|
||||
Fail-Json @{ansible_job_id=$jid; started=1; finished=1} "could not find job at '$async_dir'"
|
||||
}
|
||||
|
||||
If($mode -eq "cleanup") {
|
||||
Remove-Item $log_path -Recurse
|
||||
Exit-Json @{ansible_job_id=$jid; erased=$log_path}
|
||||
}
|
||||
|
||||
# NOT in cleanup mode, assume regular status mode
|
||||
# no remote kill mode currently exists, but probably should
|
||||
# consider log_path + ".pid" file and also unlink that above
|
||||
|
||||
$data = $null
|
||||
Try {
|
||||
$data_raw = Get-Content $log_path
|
||||
|
||||
# TODO: move this into module_utils/powershell.ps1?
|
||||
$jss = New-Object System.Web.Script.Serialization.JavaScriptSerializer
|
||||
$data = $jss.DeserializeObject($data_raw)
|
||||
}
|
||||
Catch {
|
||||
If(-not $data_raw) {
|
||||
# file not written yet? That means it is running
|
||||
Exit-Json @{results_file=$log_path; ansible_job_id=$jid; started=1; finished=0}
|
||||
}
|
||||
Else {
|
||||
Fail-Json @{ansible_job_id=$jid; results_file=$log_path; started=1; finished=1} "Could not parse job output: $data"
|
||||
}
|
||||
}
|
||||
|
||||
If (-not $data.ContainsKey("started")) {
|
||||
$data['finished'] = 1
|
||||
$data['ansible_job_id'] = $jid
|
||||
}
|
||||
ElseIf (-not $data.ContainsKey("finished")) {
|
||||
$data['finished'] = 0
|
||||
}
|
||||
|
||||
Exit-Json $data
|
||||
@ -1,516 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2018, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
|
||||
Function Get-CustomFacts {
|
||||
[cmdletBinding()]
|
||||
param (
|
||||
[Parameter(mandatory=$false)]
|
||||
$factpath = $null
|
||||
)
|
||||
|
||||
if (Test-Path -Path $factpath) {
|
||||
$FactsFiles = Get-ChildItem -Path $factpath | Where-Object -FilterScript {($PSItem.PSIsContainer -eq $false) -and ($PSItem.Extension -eq '.ps1')}
|
||||
|
||||
foreach ($FactsFile in $FactsFiles) {
|
||||
$out = & $($FactsFile.FullName)
|
||||
$result.ansible_facts.Add("ansible_$(($FactsFile.Name).Split('.')[0])", $out)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Add-Warning $result "Non existing path was set for local facts - $factpath"
|
||||
}
|
||||
}
|
||||
|
||||
Function Get-MachineSid {
|
||||
# The Machine SID is stored in HKLM:\SECURITY\SAM\Domains\Account and is
|
||||
# only accessible by the Local System account. This method get's the local
|
||||
# admin account (ends with -500) and lops it off to get the machine sid.
|
||||
|
||||
$machine_sid = $null
|
||||
|
||||
try {
|
||||
$admins_sid = "S-1-5-32-544"
|
||||
$admin_group = ([Security.Principal.SecurityIdentifier]$admins_sid).Translate([Security.Principal.NTAccount]).Value
|
||||
|
||||
Add-Type -AssemblyName System.DirectoryServices.AccountManagement
|
||||
$principal_context = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext([System.DirectoryServices.AccountManagement.ContextType]::Machine)
|
||||
$group_principal = New-Object -TypeName System.DirectoryServices.AccountManagement.GroupPrincipal($principal_context, $admin_group)
|
||||
$searcher = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalSearcher($group_principal)
|
||||
$groups = $searcher.FindOne()
|
||||
|
||||
foreach ($user in $groups.Members) {
|
||||
$user_sid = $user.Sid
|
||||
if ($user_sid.Value.EndsWith("-500")) {
|
||||
$machine_sid = $user_sid.AccountDomainSid.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
#can fail for any number of reasons, if it does just return the original null
|
||||
Add-Warning -obj $result -message "Error during machine sid retrieval: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
return $machine_sid
|
||||
}
|
||||
|
||||
$cim_instances = @{}
|
||||
|
||||
Function Get-LazyCimInstance([string]$instance_name, [string]$namespace="Root\CIMV2") {
|
||||
if(-not $cim_instances.ContainsKey($instance_name)) {
|
||||
$cim_instances[$instance_name] = $(Get-CimInstance -Namespace $namespace -ClassName $instance_name)
|
||||
}
|
||||
|
||||
return $cim_instances[$instance_name]
|
||||
}
|
||||
|
||||
$result = @{
|
||||
ansible_facts = @{ }
|
||||
changed = $false
|
||||
}
|
||||
|
||||
$grouped_subsets = @{
|
||||
min=[System.Collections.Generic.List[string]]@('date_time','distribution','dns','env','local','platform','powershell_version','user')
|
||||
network=[System.Collections.Generic.List[string]]@('all_ipv4_addresses','all_ipv6_addresses','interfaces','windows_domain', 'winrm')
|
||||
hardware=[System.Collections.Generic.List[string]]@('bios','memory','processor','uptime','virtual')
|
||||
external=[System.Collections.Generic.List[string]]@('facter')
|
||||
}
|
||||
|
||||
# build "all" set from everything mentioned in the group- this means every value must be in at least one subset to be considered legal
|
||||
$all_set = [System.Collections.Generic.HashSet[string]]@()
|
||||
|
||||
foreach($kv in $grouped_subsets.GetEnumerator()) {
|
||||
[void] $all_set.UnionWith($kv.Value)
|
||||
}
|
||||
|
||||
# dynamically create an "all" subset now that we know what should be in it
|
||||
$grouped_subsets['all'] = [System.Collections.Generic.List[string]]$all_set
|
||||
|
||||
# start with all, build up gather and exclude subsets
|
||||
$gather_subset = [System.Collections.Generic.HashSet[string]]$grouped_subsets.all
|
||||
$explicit_subset = [System.Collections.Generic.HashSet[string]]@()
|
||||
$exclude_subset = [System.Collections.Generic.HashSet[string]]@()
|
||||
|
||||
$params = Parse-Args $args -supports_check_mode $true
|
||||
$factpath = Get-AnsibleParam -obj $params -name "fact_path" -type "path"
|
||||
$gather_subset_source = Get-AnsibleParam -obj $params -name "gather_subset" -type "list" -default "all"
|
||||
|
||||
foreach($item in $gather_subset_source) {
|
||||
if(([string]$item).StartsWith("!")) {
|
||||
$item = ([string]$item).Substring(1)
|
||||
if($item -eq "all") {
|
||||
$all_minus_min = [System.Collections.Generic.HashSet[string]]@($all_set)
|
||||
[void] $all_minus_min.ExceptWith($grouped_subsets.min)
|
||||
[void] $exclude_subset.UnionWith($all_minus_min)
|
||||
}
|
||||
elseif($grouped_subsets.ContainsKey($item)) {
|
||||
[void] $exclude_subset.UnionWith($grouped_subsets[$item])
|
||||
}
|
||||
elseif($all_set.Contains($item)) {
|
||||
[void] $exclude_subset.Add($item)
|
||||
}
|
||||
# NB: invalid exclude values are ignored, since that's what posix setup does
|
||||
}
|
||||
else {
|
||||
if($grouped_subsets.ContainsKey($item)) {
|
||||
[void] $explicit_subset.UnionWith($grouped_subsets[$item])
|
||||
}
|
||||
elseif($all_set.Contains($item)) {
|
||||
[void] $explicit_subset.Add($item)
|
||||
}
|
||||
else {
|
||||
# NB: POSIX setup fails on invalid value; we warn, because we don't implement the same set as POSIX
|
||||
# and we don't have platform-specific config for this...
|
||||
Add-Warning $result "invalid value $item specified in gather_subset"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[void] $gather_subset.ExceptWith($exclude_subset)
|
||||
[void] $gather_subset.UnionWith($explicit_subset)
|
||||
|
||||
$ansible_facts = @{
|
||||
gather_subset=@($gather_subset_source)
|
||||
module_setup=$true
|
||||
}
|
||||
|
||||
$osversion = [Environment]::OSVersion
|
||||
|
||||
if ($osversion.Version -lt [version]"6.2") {
|
||||
# Server 2008, 2008 R2, and Windows 7 are not tested in CI and we want to let customers know about it before
|
||||
# removing support altogether.
|
||||
$version_string = "{0}.{1}" -f ($osversion.Version.Major, $osversion.Version.Minor)
|
||||
$msg = "Windows version '$version_string' will no longer be supported or tested in the next Ansible release"
|
||||
Add-DeprecationWarning -obj $result -message $msg -version "2.11"
|
||||
}
|
||||
|
||||
if($gather_subset.Contains('all_ipv4_addresses') -or $gather_subset.Contains('all_ipv6_addresses')) {
|
||||
$netcfg = Get-LazyCimInstance Win32_NetworkAdapterConfiguration
|
||||
|
||||
# TODO: split v4/v6 properly, return in separate keys
|
||||
$ips = @()
|
||||
Foreach ($ip in $netcfg.IPAddress) {
|
||||
If ($ip) {
|
||||
$ips += $ip
|
||||
}
|
||||
}
|
||||
|
||||
$ansible_facts += @{
|
||||
ansible_ip_addresses = $ips
|
||||
}
|
||||
}
|
||||
|
||||
if($gather_subset.Contains('bios')) {
|
||||
$win32_bios = Get-LazyCimInstance Win32_Bios
|
||||
$win32_cs = Get-LazyCimInstance Win32_ComputerSystem
|
||||
$ansible_facts += @{
|
||||
ansible_bios_date = $win32_bios.ReleaseDate.ToString("MM/dd/yyyy")
|
||||
ansible_bios_version = $win32_bios.SMBIOSBIOSVersion
|
||||
ansible_product_name = $win32_cs.Model.Trim()
|
||||
ansible_product_serial = $win32_bios.SerialNumber
|
||||
# ansible_product_version = ([string] $win32_cs.SystemFamily)
|
||||
}
|
||||
}
|
||||
|
||||
if($gather_subset.Contains('date_time')) {
|
||||
$datetime = (Get-Date)
|
||||
$datetime_utc = $datetime.ToUniversalTime()
|
||||
$date = @{
|
||||
date = $datetime.ToString("yyyy-MM-dd")
|
||||
day = $datetime.ToString("dd")
|
||||
epoch = (Get-Date -UFormat "%s")
|
||||
hour = $datetime.ToString("HH")
|
||||
iso8601 = $datetime_utc.ToString("yyyy-MM-ddTHH:mm:ssZ")
|
||||
iso8601_basic = $datetime.ToString("yyyyMMddTHHmmssffffff")
|
||||
iso8601_basic_short = $datetime.ToString("yyyyMMddTHHmmss")
|
||||
iso8601_micro = $datetime_utc.ToString("yyyy-MM-ddTHH:mm:ss.ffffffZ")
|
||||
minute = $datetime.ToString("mm")
|
||||
month = $datetime.ToString("MM")
|
||||
second = $datetime.ToString("ss")
|
||||
time = $datetime.ToString("HH:mm:ss")
|
||||
tz = ([System.TimeZoneInfo]::Local.Id)
|
||||
tz_offset = $datetime.ToString("zzzz")
|
||||
# Ensure that the weekday is in English
|
||||
weekday = $datetime.ToString("dddd", [System.Globalization.CultureInfo]::InvariantCulture)
|
||||
weekday_number = (Get-Date -UFormat "%w")
|
||||
weeknumber = (Get-Date -UFormat "%W")
|
||||
year = $datetime.ToString("yyyy")
|
||||
}
|
||||
|
||||
$ansible_facts += @{
|
||||
ansible_date_time = $date
|
||||
}
|
||||
}
|
||||
|
||||
if($gather_subset.Contains('distribution')) {
|
||||
$win32_os = Get-LazyCimInstance Win32_OperatingSystem
|
||||
$product_type = switch($win32_os.ProductType) {
|
||||
1 { "workstation" }
|
||||
2 { "domain_controller" }
|
||||
3 { "server" }
|
||||
default { "unknown" }
|
||||
}
|
||||
|
||||
$installation_type = $null
|
||||
$current_version_path = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion"
|
||||
if (Test-Path -LiteralPath $current_version_path) {
|
||||
$install_type_prop = Get-ItemProperty -LiteralPath $current_version_path -ErrorAction SilentlyContinue
|
||||
$installation_type = [String]$install_type_prop.InstallationType
|
||||
}
|
||||
|
||||
$ansible_facts += @{
|
||||
ansible_distribution = $win32_os.Caption
|
||||
ansible_distribution_version = $osversion.Version.ToString()
|
||||
ansible_distribution_major_version = $osversion.Version.Major.ToString()
|
||||
ansible_os_family = "Windows"
|
||||
ansible_os_name = ($win32_os.Name.Split('|')[0]).Trim()
|
||||
ansible_os_product_type = $product_type
|
||||
ansible_os_installation_type = $installation_type
|
||||
}
|
||||
}
|
||||
|
||||
if($gather_subset.Contains('env')) {
|
||||
$env_vars = @{ }
|
||||
foreach ($item in Get-ChildItem Env:) {
|
||||
$name = $item | Select-Object -ExpandProperty Name
|
||||
# Powershell ConvertTo-Json fails if string ends with \
|
||||
$value = ($item | Select-Object -ExpandProperty Value).TrimEnd("\")
|
||||
$env_vars.Add($name, $value)
|
||||
}
|
||||
|
||||
$ansible_facts += @{
|
||||
ansible_env = $env_vars
|
||||
}
|
||||
}
|
||||
|
||||
if($gather_subset.Contains('facter')) {
|
||||
# See if Facter is on the System Path
|
||||
Try {
|
||||
Get-Command facter -ErrorAction Stop > $null
|
||||
$facter_installed = $true
|
||||
} Catch {
|
||||
$facter_installed = $false
|
||||
}
|
||||
|
||||
# Get JSON from Facter, and parse it out.
|
||||
if ($facter_installed) {
|
||||
&facter -j | Tee-Object -Variable facter_output > $null
|
||||
$facts = "$facter_output" | ConvertFrom-Json
|
||||
ForEach($fact in $facts.PSObject.Properties) {
|
||||
$fact_name = $fact.Name
|
||||
$ansible_facts.Add("facter_$fact_name", $fact.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if($gather_subset.Contains('interfaces')) {
|
||||
$netcfg = Get-LazyCimInstance Win32_NetworkAdapterConfiguration
|
||||
$ActiveNetcfg = @()
|
||||
$ActiveNetcfg += $netcfg | Where-Object {$_.ipaddress -ne $null}
|
||||
|
||||
$namespaces = Get-LazyCimInstance __Namespace -namespace root
|
||||
if ($namespaces | Where-Object { $_.Name -eq "StandardCimv" }) {
|
||||
$net_adapters = Get-LazyCimInstance MSFT_NetAdapter -namespace Root\StandardCimv2
|
||||
$guid_key = "InterfaceGUID"
|
||||
$name_key = "Name"
|
||||
} else {
|
||||
$net_adapters = Get-LazyCimInstance Win32_NetworkAdapter
|
||||
$guid_key = "GUID"
|
||||
$name_key = "NetConnectionID"
|
||||
}
|
||||
|
||||
$formattednetcfg = @()
|
||||
foreach ($adapter in $ActiveNetcfg)
|
||||
{
|
||||
$thisadapter = @{
|
||||
default_gateway = $null
|
||||
connection_name = $null
|
||||
dns_domain = $adapter.dnsdomain
|
||||
interface_index = $adapter.InterfaceIndex
|
||||
interface_name = $adapter.description
|
||||
macaddress = $adapter.macaddress
|
||||
}
|
||||
|
||||
if ($adapter.defaultIPGateway)
|
||||
{
|
||||
$thisadapter.default_gateway = $adapter.DefaultIPGateway[0].ToString()
|
||||
}
|
||||
$net_adapter = $net_adapters | Where-Object { $_.$guid_key -eq $adapter.SettingID }
|
||||
if ($net_adapter) {
|
||||
$thisadapter.connection_name = $net_adapter.$name_key
|
||||
}
|
||||
|
||||
$formattednetcfg += $thisadapter
|
||||
}
|
||||
|
||||
$ansible_facts += @{
|
||||
ansible_interfaces = $formattednetcfg
|
||||
}
|
||||
}
|
||||
|
||||
if ($gather_subset.Contains("local") -and $null -ne $factpath) {
|
||||
# Get any custom facts; results are updated in the
|
||||
Get-CustomFacts -factpath $factpath
|
||||
}
|
||||
|
||||
if($gather_subset.Contains('memory')) {
|
||||
$win32_cs = Get-LazyCimInstance Win32_ComputerSystem
|
||||
$win32_os = Get-LazyCimInstance Win32_OperatingSystem
|
||||
$ansible_facts += @{
|
||||
# Win32_PhysicalMemory is empty on some virtual platforms
|
||||
ansible_memtotal_mb = ([math]::ceiling($win32_cs.TotalPhysicalMemory / 1024 / 1024))
|
||||
ansible_memfree_mb = ([math]::ceiling($win32_os.FreePhysicalMemory / 1024))
|
||||
ansible_swaptotal_mb = ([math]::round($win32_os.TotalSwapSpaceSize / 1024))
|
||||
ansible_pagefiletotal_mb = ([math]::round($win32_os.SizeStoredInPagingFiles / 1024))
|
||||
ansible_pagefilefree_mb = ([math]::round($win32_os.FreeSpaceInPagingFiles / 1024))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if($gather_subset.Contains('platform')) {
|
||||
$win32_cs = Get-LazyCimInstance Win32_ComputerSystem
|
||||
$win32_os = Get-LazyCimInstance Win32_OperatingSystem
|
||||
$domain_suffix = $win32_cs.Domain.Substring($win32_cs.Workgroup.length)
|
||||
$fqdn = $win32_cs.DNSHostname
|
||||
|
||||
if( $domain_suffix -ne "")
|
||||
{
|
||||
$fqdn = $win32_cs.DNSHostname + "." + $domain_suffix
|
||||
}
|
||||
|
||||
try {
|
||||
$ansible_reboot_pending = Get-PendingRebootStatus
|
||||
} catch {
|
||||
# fails for non-admin users, set to null in this case
|
||||
$ansible_reboot_pending = $null
|
||||
}
|
||||
|
||||
$ansible_facts += @{
|
||||
ansible_architecture = $win32_os.OSArchitecture
|
||||
ansible_domain = $domain_suffix
|
||||
ansible_fqdn = $fqdn
|
||||
ansible_hostname = $win32_cs.DNSHostname
|
||||
ansible_netbios_name = $win32_cs.Name
|
||||
ansible_kernel = $osversion.Version.ToString()
|
||||
ansible_nodename = $fqdn
|
||||
ansible_machine_id = Get-MachineSid
|
||||
ansible_owner_contact = ([string] $win32_cs.PrimaryOwnerContact)
|
||||
ansible_owner_name = ([string] $win32_cs.PrimaryOwnerName)
|
||||
# FUTURE: should this live in its own subset?
|
||||
ansible_reboot_pending = $ansible_reboot_pending
|
||||
ansible_system = $osversion.Platform.ToString()
|
||||
ansible_system_description = ([string] $win32_os.Description)
|
||||
ansible_system_vendor = $win32_cs.Manufacturer
|
||||
}
|
||||
}
|
||||
|
||||
if($gather_subset.Contains('powershell_version')) {
|
||||
$ansible_facts += @{
|
||||
ansible_powershell_version = ($PSVersionTable.PSVersion.Major)
|
||||
}
|
||||
}
|
||||
|
||||
if($gather_subset.Contains('processor')) {
|
||||
$win32_cs = Get-LazyCimInstance Win32_ComputerSystem
|
||||
$win32_cpu = Get-LazyCimInstance Win32_Processor
|
||||
if ($win32_cpu -is [array]) {
|
||||
# multi-socket, pick first
|
||||
$win32_cpu = $win32_cpu[0]
|
||||
}
|
||||
|
||||
$cpu_list = @( )
|
||||
for ($i=1; $i -le $win32_cs.NumberOfLogicalProcessors; $i++) {
|
||||
$cpu_list += $win32_cpu.Manufacturer
|
||||
$cpu_list += $win32_cpu.Name
|
||||
}
|
||||
|
||||
$ansible_facts += @{
|
||||
ansible_processor = $cpu_list
|
||||
ansible_processor_cores = $win32_cpu.NumberOfCores
|
||||
ansible_processor_count = $win32_cs.NumberOfProcessors
|
||||
ansible_processor_threads_per_core = ($win32_cpu.NumberOfLogicalProcessors / $win32_cpu.NumberofCores)
|
||||
ansible_processor_vcpus = $win32_cs.NumberOfLogicalProcessors
|
||||
}
|
||||
}
|
||||
|
||||
if($gather_subset.Contains('uptime')) {
|
||||
$win32_os = Get-LazyCimInstance Win32_OperatingSystem
|
||||
$ansible_facts += @{
|
||||
ansible_lastboot = $win32_os.lastbootuptime.ToString("u")
|
||||
ansible_uptime_seconds = $([System.Convert]::ToInt64($(Get-Date).Subtract($win32_os.lastbootuptime).TotalSeconds))
|
||||
}
|
||||
}
|
||||
|
||||
if($gather_subset.Contains('user')) {
|
||||
$user = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$ansible_facts += @{
|
||||
ansible_user_dir = $env:userprofile
|
||||
# Win32_UserAccount.FullName is probably the right thing here, but it can be expensive to get on large domains
|
||||
ansible_user_gecos = ""
|
||||
ansible_user_id = $env:username
|
||||
ansible_user_sid = $user.User.Value
|
||||
}
|
||||
}
|
||||
|
||||
if($gather_subset.Contains('windows_domain')) {
|
||||
$win32_cs = Get-LazyCimInstance Win32_ComputerSystem
|
||||
$domain_roles = @{
|
||||
0 = "Stand-alone workstation"
|
||||
1 = "Member workstation"
|
||||
2 = "Stand-alone server"
|
||||
3 = "Member server"
|
||||
4 = "Backup domain controller"
|
||||
5 = "Primary domain controller"
|
||||
}
|
||||
|
||||
$domain_role = $domain_roles.Get_Item([Int32]$win32_cs.DomainRole)
|
||||
|
||||
$ansible_facts += @{
|
||||
ansible_windows_domain = $win32_cs.Domain
|
||||
ansible_windows_domain_member = $win32_cs.PartOfDomain
|
||||
ansible_windows_domain_role = $domain_role
|
||||
}
|
||||
}
|
||||
|
||||
if($gather_subset.Contains('winrm')) {
|
||||
|
||||
$winrm_https_listener_parent_paths = Get-ChildItem -Path WSMan:\localhost\Listener -Recurse -ErrorAction SilentlyContinue | `
|
||||
Where-Object {$_.PSChildName -eq "Transport" -and $_.Value -eq "HTTPS"} | Select-Object PSParentPath
|
||||
if ($winrm_https_listener_parent_paths -isnot [array]) {
|
||||
$winrm_https_listener_parent_paths = @($winrm_https_listener_parent_paths)
|
||||
}
|
||||
|
||||
$winrm_https_listener_paths = @()
|
||||
foreach ($winrm_https_listener_parent_path in $winrm_https_listener_parent_paths) {
|
||||
$winrm_https_listener_paths += $winrm_https_listener_parent_path.PSParentPath.Substring($winrm_https_listener_parent_path.PSParentPath.LastIndexOf("\"))
|
||||
}
|
||||
|
||||
$https_listeners = @()
|
||||
foreach ($winrm_https_listener_path in $winrm_https_listener_paths) {
|
||||
$https_listeners += Get-ChildItem -Path "WSMan:\localhost\Listener$winrm_https_listener_path"
|
||||
}
|
||||
|
||||
$winrm_cert_thumbprints = @()
|
||||
foreach ($https_listener in $https_listeners) {
|
||||
$winrm_cert_thumbprints += $https_listener | Where-Object {$_.Name -EQ "CertificateThumbprint" } | Select-Object Value
|
||||
}
|
||||
|
||||
$winrm_cert_expiry = @()
|
||||
foreach ($winrm_cert_thumbprint in $winrm_cert_thumbprints) {
|
||||
Try {
|
||||
$winrm_cert_expiry += Get-ChildItem -Path Cert:\LocalMachine\My | Where-Object Thumbprint -EQ $winrm_cert_thumbprint.Value.ToString().ToUpper() | Select-Object NotAfter
|
||||
} Catch {
|
||||
Add-Warning -obj $result -message "Error during certificate expiration retrieval: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
$winrm_cert_expirations = $winrm_cert_expiry | Sort-Object NotAfter
|
||||
if ($winrm_cert_expirations) {
|
||||
# this fact was renamed from ansible_winrm_certificate_expires due to collision with ansible_winrm_X connection var pattern
|
||||
$ansible_facts.Add("ansible_win_rm_certificate_expires", $winrm_cert_expirations[0].NotAfter.ToString("yyyy-MM-dd HH:mm:ss"))
|
||||
}
|
||||
}
|
||||
|
||||
if($gather_subset.Contains('virtual')) {
|
||||
$machine_info = Get-LazyCimInstance Win32_ComputerSystem
|
||||
|
||||
switch ($machine_info.model) {
|
||||
"Virtual Machine" {
|
||||
$machine_type="Hyper-V"
|
||||
$machine_role="guest"
|
||||
}
|
||||
|
||||
"VMware Virtual Platform" {
|
||||
$machine_type="VMware"
|
||||
$machine_role="guest"
|
||||
}
|
||||
|
||||
"VirtualBox" {
|
||||
$machine_type="VirtualBox"
|
||||
$machine_role="guest"
|
||||
}
|
||||
|
||||
"HVM domU" {
|
||||
$machine_type="Xen"
|
||||
$machine_role="guest"
|
||||
}
|
||||
|
||||
default {
|
||||
$machine_type="NA"
|
||||
$machine_role="NA"
|
||||
}
|
||||
}
|
||||
|
||||
$ansible_facts += @{
|
||||
ansible_virtualization_role = $machine_role
|
||||
ansible_virtualization_type = $machine_type
|
||||
}
|
||||
}
|
||||
|
||||
$result.ansible_facts += $ansible_facts
|
||||
|
||||
Exit-Json $result
|
||||
@ -1,28 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
|
||||
$params = Parse-Args $args -supports_check_mode $true;
|
||||
$src = Get-AnsibleParam -obj $params -name "src" -type "path" -aliases "path" -failifempty $true;
|
||||
|
||||
$result = @{
|
||||
changed = $false;
|
||||
}
|
||||
|
||||
If (Test-Path -LiteralPath $src -PathType Leaf)
|
||||
{
|
||||
$bytes = [System.IO.File]::ReadAllBytes($src);
|
||||
$result.content = [System.Convert]::ToBase64String($bytes);
|
||||
$result.encoding = "base64";
|
||||
Exit-Json $result;
|
||||
}
|
||||
ElseIf (Test-Path -LiteralPath $src -PathType Container)
|
||||
{
|
||||
Fail-Json $result "Path $src is a directory";
|
||||
}
|
||||
Else
|
||||
{
|
||||
Fail-Json $result "Path $src is not found";
|
||||
}
|
||||
@ -1,225 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2015, Phil Schwartz <schwartzmx@gmail.com>
|
||||
# Copyright: (c) 2015, Trond Hindenes
|
||||
# Copyright: (c) 2015, Hans-Joachim Kliemeck <git@kliemeck.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
#Requires -Module Ansible.ModuleUtils.PrivilegeUtil
|
||||
#Requires -Module Ansible.ModuleUtils.SID
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# win_acl module (File/Resources Permission Additions/Removal)
|
||||
|
||||
#Functions
|
||||
function Get-UserSID {
|
||||
param(
|
||||
[String]$AccountName
|
||||
)
|
||||
|
||||
$userSID = $null
|
||||
$searchAppPools = $false
|
||||
|
||||
if ($AccountName.Split("\").Count -gt 1) {
|
||||
if ($AccountName.Split("\")[0] -eq "IIS APPPOOL") {
|
||||
$searchAppPools = $true
|
||||
$AccountName = $AccountName.Split("\")[1]
|
||||
}
|
||||
}
|
||||
|
||||
if ($searchAppPools) {
|
||||
Import-Module -Name WebAdministration
|
||||
$testIISPath = Test-Path -LiteralPath "IIS:"
|
||||
if ($testIISPath) {
|
||||
$appPoolObj = Get-ItemProperty -LiteralPath "IIS:\AppPools\$AccountName"
|
||||
$userSID = $appPoolObj.applicationPoolSid
|
||||
}
|
||||
}
|
||||
else {
|
||||
$userSID = Convert-ToSID -account_name $AccountName
|
||||
}
|
||||
|
||||
return $userSID
|
||||
}
|
||||
|
||||
$params = Parse-Args $args
|
||||
|
||||
Function SetPrivilegeTokens() {
|
||||
# Set privilege tokens only if admin.
|
||||
# Admins would have these privs or be able to set these privs in the UI Anyway
|
||||
|
||||
$adminRole=[System.Security.Principal.WindowsBuiltInRole]::Administrator
|
||||
$myWindowsID=[System.Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$myWindowsPrincipal=new-object System.Security.Principal.WindowsPrincipal($myWindowsID)
|
||||
|
||||
|
||||
if ($myWindowsPrincipal.IsInRole($adminRole)) {
|
||||
# Need to adjust token privs when executing Set-ACL in certain cases.
|
||||
# e.g. d:\testdir is owned by group in which current user is not a member and no perms are inherited from d:\
|
||||
# This also sets us up for setting the owner as a feature.
|
||||
# See the following for details of each privilege
|
||||
# https://msdn.microsoft.com/en-us/library/windows/desktop/bb530716(v=vs.85).aspx
|
||||
$privileges = @(
|
||||
"SeRestorePrivilege", # Grants all write access control to any file, regardless of ACL.
|
||||
"SeBackupPrivilege", # Grants all read access control to any file, regardless of ACL.
|
||||
"SeTakeOwnershipPrivilege" # Grants ability to take owernship of an object w/out being granted discretionary access
|
||||
)
|
||||
foreach ($privilege in $privileges) {
|
||||
$state = Get-AnsiblePrivilege -Name $privilege
|
||||
if ($state -eq $false) {
|
||||
Set-AnsiblePrivilege -Name $privilege -Value $true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$result = @{
|
||||
changed = $false
|
||||
}
|
||||
|
||||
$path = Get-AnsibleParam -obj $params -name "path" -type "str" -failifempty $true
|
||||
$user = Get-AnsibleParam -obj $params -name "user" -type "str" -failifempty $true
|
||||
$rights = Get-AnsibleParam -obj $params -name "rights" -type "str" -failifempty $true
|
||||
|
||||
$type = Get-AnsibleParam -obj $params -name "type" -type "str" -failifempty $true -validateset "allow","deny"
|
||||
$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent","present"
|
||||
|
||||
$inherit = Get-AnsibleParam -obj $params -name "inherit" -type "str"
|
||||
$propagation = Get-AnsibleParam -obj $params -name "propagation" -type "str" -default "None" -validateset "InheritOnly","None","NoPropagateInherit"
|
||||
|
||||
# We mount the HKCR, HKU, and HKCC registry hives so PS can access them.
|
||||
# Network paths have no qualifiers so we use -EA SilentlyContinue to ignore that
|
||||
$path_qualifier = Split-Path -Path $path -Qualifier -ErrorAction SilentlyContinue
|
||||
if ($path_qualifier -eq "HKCR:" -and (-not (Test-Path -LiteralPath HKCR:\))) {
|
||||
New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT > $null
|
||||
}
|
||||
if ($path_qualifier -eq "HKU:" -and (-not (Test-Path -LiteralPath HKU:\))) {
|
||||
New-PSDrive -Name HKU -PSProvider Registry -Root HKEY_USERS > $null
|
||||
}
|
||||
if ($path_qualifier -eq "HKCC:" -and (-not (Test-Path -LiteralPath HKCC:\))) {
|
||||
New-PSDrive -Name HKCC -PSProvider Registry -Root HKEY_CURRENT_CONFIG > $null
|
||||
}
|
||||
|
||||
If (-Not (Test-Path -LiteralPath $path)) {
|
||||
Fail-Json -obj $result -message "$path file or directory does not exist on the host"
|
||||
}
|
||||
|
||||
# Test that the user/group is resolvable on the local machine
|
||||
$sid = Get-UserSID -AccountName $user
|
||||
if (!$sid) {
|
||||
Fail-Json -obj $result -message "$user is not a valid user or group on the host machine or domain"
|
||||
}
|
||||
|
||||
If (Test-Path -LiteralPath $path -PathType Leaf) {
|
||||
$inherit = "None"
|
||||
}
|
||||
ElseIf ($null -eq $inherit) {
|
||||
$inherit = "ContainerInherit, ObjectInherit"
|
||||
}
|
||||
|
||||
# Bug in Set-Acl, Get-Acl where -LiteralPath only works for the Registry provider if the location is in that root
|
||||
# qualifier. We also don't have a qualifier for a network path so only change if not null
|
||||
if ($null -ne $path_qualifier) {
|
||||
Push-Location -LiteralPath $path_qualifier
|
||||
}
|
||||
|
||||
Try {
|
||||
SetPrivilegeTokens
|
||||
$path_item = Get-Item -LiteralPath $path -Force
|
||||
If ($path_item.PSProvider.Name -eq "Registry") {
|
||||
$colRights = [System.Security.AccessControl.RegistryRights]$rights
|
||||
}
|
||||
Else {
|
||||
$colRights = [System.Security.AccessControl.FileSystemRights]$rights
|
||||
}
|
||||
|
||||
$InheritanceFlag = [System.Security.AccessControl.InheritanceFlags]$inherit
|
||||
$PropagationFlag = [System.Security.AccessControl.PropagationFlags]$propagation
|
||||
|
||||
If ($type -eq "allow") {
|
||||
$objType =[System.Security.AccessControl.AccessControlType]::Allow
|
||||
}
|
||||
Else {
|
||||
$objType =[System.Security.AccessControl.AccessControlType]::Deny
|
||||
}
|
||||
|
||||
$objUser = New-Object System.Security.Principal.SecurityIdentifier($sid)
|
||||
If ($path_item.PSProvider.Name -eq "Registry") {
|
||||
$objACE = New-Object System.Security.AccessControl.RegistryAccessRule ($objUser, $colRights, $InheritanceFlag, $PropagationFlag, $objType)
|
||||
}
|
||||
Else {
|
||||
$objACE = New-Object System.Security.AccessControl.FileSystemAccessRule ($objUser, $colRights, $InheritanceFlag, $PropagationFlag, $objType)
|
||||
}
|
||||
$objACL = Get-ACL -LiteralPath $path
|
||||
|
||||
# Check if the ACE exists already in the objects ACL list
|
||||
$match = $false
|
||||
|
||||
ForEach($rule in $objACL.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])){
|
||||
|
||||
If ($path_item.PSProvider.Name -eq "Registry") {
|
||||
If (($rule.RegistryRights -eq $objACE.RegistryRights) -And ($rule.AccessControlType -eq $objACE.AccessControlType) -And ($rule.IdentityReference -eq $objACE.IdentityReference) -And ($rule.IsInherited -eq $objACE.IsInherited) -And ($rule.InheritanceFlags -eq $objACE.InheritanceFlags) -And ($rule.PropagationFlags -eq $objACE.PropagationFlags)) {
|
||||
$match = $true
|
||||
Break
|
||||
}
|
||||
} else {
|
||||
If (($rule.FileSystemRights -eq $objACE.FileSystemRights) -And ($rule.AccessControlType -eq $objACE.AccessControlType) -And ($rule.IdentityReference -eq $objACE.IdentityReference) -And ($rule.IsInherited -eq $objACE.IsInherited) -And ($rule.InheritanceFlags -eq $objACE.InheritanceFlags) -And ($rule.PropagationFlags -eq $objACE.PropagationFlags)) {
|
||||
$match = $true
|
||||
Break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
If ($state -eq "present" -And $match -eq $false) {
|
||||
Try {
|
||||
$objACL.AddAccessRule($objACE)
|
||||
If ($path_item.PSProvider.Name -eq "Registry") {
|
||||
Set-ACL -LiteralPath $path -AclObject $objACL
|
||||
} else {
|
||||
(Get-Item -LiteralPath $path).SetAccessControl($objACL)
|
||||
}
|
||||
$result.changed = $true
|
||||
}
|
||||
Catch {
|
||||
Fail-Json -obj $result -message "an exception occurred when adding the specified rule - $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
ElseIf ($state -eq "absent" -And $match -eq $true) {
|
||||
Try {
|
||||
$objACL.RemoveAccessRule($objACE)
|
||||
If ($path_item.PSProvider.Name -eq "Registry") {
|
||||
Set-ACL -LiteralPath $path -AclObject $objACL
|
||||
} else {
|
||||
(Get-Item -LiteralPath $path).SetAccessControl($objACL)
|
||||
}
|
||||
$result.changed = $true
|
||||
}
|
||||
Catch {
|
||||
Fail-Json -obj $result -message "an exception occurred when removing the specified rule - $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
Else {
|
||||
# A rule was attempting to be added but already exists
|
||||
If ($match -eq $true) {
|
||||
Exit-Json -obj $result -message "the specified rule already exists"
|
||||
}
|
||||
# A rule didn't exist that was trying to be removed
|
||||
Else {
|
||||
Exit-Json -obj $result -message "the specified rule does not exist"
|
||||
}
|
||||
}
|
||||
}
|
||||
Catch {
|
||||
Fail-Json -obj $result -message "an error occurred when attempting to $state $rights permission(s) on $path for $user - $($_.Exception.Message)"
|
||||
}
|
||||
Finally {
|
||||
# Make sure we revert the location stack to the original path just for cleanups sake
|
||||
if ($null -ne $path_qualifier) {
|
||||
Pop-Location
|
||||
}
|
||||
}
|
||||
|
||||
Exit-Json -obj $result
|
||||
@ -1,132 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2015, Phil Schwartz <schwartzmx@gmail.com>
|
||||
# Copyright: (c) 2015, Trond Hindenes
|
||||
# Copyright: (c) 2015, Hans-Joachim Kliemeck <git@kliemeck.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_acl
|
||||
version_added: "2.0"
|
||||
short_description: Set file/directory/registry permissions for a system user or group
|
||||
description:
|
||||
- Add or remove rights/permissions for a given user or group for the specified
|
||||
file, folder, registry key or AppPool identifies.
|
||||
options:
|
||||
path:
|
||||
description:
|
||||
- The path to the file or directory.
|
||||
type: str
|
||||
required: yes
|
||||
user:
|
||||
description:
|
||||
- User or Group to add specified rights to act on src file/folder or
|
||||
registry key.
|
||||
type: str
|
||||
required: yes
|
||||
state:
|
||||
description:
|
||||
- Specify whether to add C(present) or remove C(absent) the specified access rule.
|
||||
type: str
|
||||
choices: [ absent, present ]
|
||||
default: present
|
||||
type:
|
||||
description:
|
||||
- Specify whether to allow or deny the rights specified.
|
||||
type: str
|
||||
required: yes
|
||||
choices: [ allow, deny ]
|
||||
rights:
|
||||
description:
|
||||
- The rights/permissions that are to be allowed/denied for the specified
|
||||
user or group for the item at C(path).
|
||||
- If C(path) is a file or directory, rights can be any right under MSDN
|
||||
FileSystemRights U(https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.filesystemrights.aspx).
|
||||
- If C(path) is a registry key, rights can be any right under MSDN
|
||||
RegistryRights U(https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.registryrights.aspx).
|
||||
type: str
|
||||
required: yes
|
||||
inherit:
|
||||
description:
|
||||
- Inherit flags on the ACL rules.
|
||||
- Can be specified as a comma separated list, e.g. C(ContainerInherit),
|
||||
C(ObjectInherit).
|
||||
- For more information on the choices see MSDN InheritanceFlags enumeration
|
||||
at U(https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.inheritanceflags.aspx).
|
||||
- Defaults to C(ContainerInherit, ObjectInherit) for Directories.
|
||||
type: str
|
||||
choices: [ ContainerInherit, ObjectInherit ]
|
||||
propagation:
|
||||
description:
|
||||
- Propagation flag on the ACL rules.
|
||||
- For more information on the choices see MSDN PropagationFlags enumeration
|
||||
at U(https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.propagationflags.aspx).
|
||||
type: str
|
||||
choices: [ InheritOnly, None, NoPropagateInherit ]
|
||||
default: "None"
|
||||
notes:
|
||||
- If adding ACL's for AppPool identities (available since 2.3), the Windows
|
||||
Feature "Web-Scripting-Tools" must be enabled.
|
||||
seealso:
|
||||
- module: win_acl_inheritance
|
||||
- module: win_file
|
||||
- module: win_owner
|
||||
- module: win_stat
|
||||
author:
|
||||
- Phil Schwartz (@schwartzmx)
|
||||
- Trond Hindenes (@trondhindenes)
|
||||
- Hans-Joachim Kliemeck (@h0nIg)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Restrict write and execute access to User Fed-Phil
|
||||
win_acl:
|
||||
user: Fed-Phil
|
||||
path: C:\Important\Executable.exe
|
||||
type: deny
|
||||
rights: ExecuteFile,Write
|
||||
|
||||
- name: Add IIS_IUSRS allow rights
|
||||
win_acl:
|
||||
path: C:\inetpub\wwwroot\MySite
|
||||
user: IIS_IUSRS
|
||||
rights: FullControl
|
||||
type: allow
|
||||
state: present
|
||||
inherit: ContainerInherit, ObjectInherit
|
||||
propagation: 'None'
|
||||
|
||||
- name: Set registry key right
|
||||
win_acl:
|
||||
path: HKCU:\Bovine\Key
|
||||
user: BUILTIN\Users
|
||||
rights: EnumerateSubKeys
|
||||
type: allow
|
||||
state: present
|
||||
inherit: ContainerInherit, ObjectInherit
|
||||
propagation: 'None'
|
||||
|
||||
- name: Remove FullControl AccessRule for IIS_IUSRS
|
||||
win_acl:
|
||||
path: C:\inetpub\wwwroot\MySite
|
||||
user: IIS_IUSRS
|
||||
rights: FullControl
|
||||
type: allow
|
||||
state: absent
|
||||
inherit: ContainerInherit, ObjectInherit
|
||||
propagation: 'None'
|
||||
|
||||
- name: Deny Intern
|
||||
win_acl:
|
||||
path: C:\Administrator\Documents
|
||||
user: Intern
|
||||
rights: Read,Write,Modify,FullControl,Delete
|
||||
type: deny
|
||||
state: present
|
||||
'''
|
||||
@ -1,67 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2015, Hans-Joachim Kliemeck <git@kliemeck.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
|
||||
$params = Parse-Args $args -supports_check_mode $true
|
||||
$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -default $false
|
||||
|
||||
$result = @{
|
||||
changed = $false
|
||||
}
|
||||
|
||||
$path = Get-AnsibleParam -obj $params "path" -type "path" -failifempty $true
|
||||
$state = Get-AnsibleParam -obj $params "state" -type "str" -default "absent" -validateSet "present","absent" -resultobj $result
|
||||
$reorganize = Get-AnsibleParam -obj $params "reorganize" -type "bool" -default $false -resultobj $result
|
||||
|
||||
If (-Not (Test-Path -LiteralPath $path)) {
|
||||
Fail-Json $result "$path file or directory does not exist on the host"
|
||||
}
|
||||
|
||||
Try {
|
||||
$objACL = Get-ACL -LiteralPath $path
|
||||
# AreAccessRulesProtected - $false if inheritance is set ,$true if inheritance is not set
|
||||
$inheritanceDisabled = $objACL.AreAccessRulesProtected
|
||||
|
||||
If (($state -eq "present") -And $inheritanceDisabled) {
|
||||
# second parameter is ignored if first=$False
|
||||
$objACL.SetAccessRuleProtection($False, $False)
|
||||
|
||||
If ($reorganize) {
|
||||
# it wont work without intermediate save, state would be the same
|
||||
Set-ACL -LiteralPath $path -AclObject $objACL -WhatIf:$check_mode
|
||||
$result.changed = $true
|
||||
$objACL = Get-ACL -LiteralPath $path
|
||||
|
||||
# convert explicit ACE to inherited ACE
|
||||
ForEach($inheritedRule in $objACL.Access) {
|
||||
If (-not $inheritedRule.IsInherited) {
|
||||
Continue
|
||||
}
|
||||
|
||||
ForEach($explicitRrule in $objACL.Access) {
|
||||
If ($explicitRrule.IsInherited) {
|
||||
Continue
|
||||
}
|
||||
|
||||
If (($inheritedRule.FileSystemRights -eq $explicitRrule.FileSystemRights) -And ($inheritedRule.AccessControlType -eq $explicitRrule.AccessControlType) -And ($inheritedRule.IdentityReference -eq $explicitRrule.IdentityReference) -And ($inheritedRule.InheritanceFlags -eq $explicitRrule.InheritanceFlags) -And ($inheritedRule.PropagationFlags -eq $explicitRrule.PropagationFlags)) {
|
||||
$objACL.RemoveAccessRule($explicitRrule)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Set-ACL -LiteralPath $path -AclObject $objACL -WhatIf:$check_mode
|
||||
$result.changed = $true
|
||||
} Elseif (($state -eq "absent") -And (-not $inheritanceDisabled)) {
|
||||
$objACL.SetAccessRuleProtection($True, $reorganize)
|
||||
Set-ACL -LiteralPath $path -AclObject $objACL -WhatIf:$check_mode
|
||||
$result.changed = $true
|
||||
}
|
||||
} Catch {
|
||||
Fail-Json $result "an error occurred when attempting to disable inheritance: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
Exit-Json $result
|
||||
@ -1,70 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2015, Hans-Joachim Kliemeck <git@kliemeck.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
# this is a windows documentation stub. actual code lives in the .ps1
|
||||
# file of the same name
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_acl_inheritance
|
||||
version_added: "2.1"
|
||||
short_description: Change ACL inheritance
|
||||
description:
|
||||
- Change ACL (Access Control List) inheritance and optionally copy inherited ACE's (Access Control Entry) to dedicated ACE's or vice versa.
|
||||
options:
|
||||
path:
|
||||
description:
|
||||
- Path to be used for changing inheritance
|
||||
required: yes
|
||||
type: path
|
||||
state:
|
||||
description:
|
||||
- Specify whether to enable I(present) or disable I(absent) ACL inheritance.
|
||||
type: str
|
||||
choices: [ absent, present ]
|
||||
default: absent
|
||||
reorganize:
|
||||
description:
|
||||
- For P(state) = I(absent), indicates if the inherited ACE's should be copied from the parent directory.
|
||||
This is necessary (in combination with removal) for a simple ACL instead of using multiple ACE deny entries.
|
||||
- For P(state) = I(present), indicates if the inherited ACE's should be deduplicated compared to the parent directory.
|
||||
This removes complexity of the ACL structure.
|
||||
type: bool
|
||||
default: no
|
||||
seealso:
|
||||
- module: win_acl
|
||||
- module: win_file
|
||||
- module: win_stat
|
||||
author:
|
||||
- Hans-Joachim Kliemeck (@h0nIg)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Disable inherited ACE's
|
||||
win_acl_inheritance:
|
||||
path: C:\apache
|
||||
state: absent
|
||||
|
||||
- name: Disable and copy inherited ACE's
|
||||
win_acl_inheritance:
|
||||
path: C:\apache
|
||||
state: absent
|
||||
reorganize: yes
|
||||
|
||||
- name: Enable and remove dedicated ACE's
|
||||
win_acl_inheritance:
|
||||
path: C:\apache
|
||||
state: present
|
||||
reorganize: yes
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
|
||||
'''
|
||||
@ -1,260 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#AnsibleRequires -CSharpUtil Ansible.Basic
|
||||
|
||||
$store_name_values = ([System.Security.Cryptography.X509Certificates.StoreName]).GetEnumValues() | ForEach-Object { $_.ToString() }
|
||||
$store_location_values = ([System.Security.Cryptography.X509Certificates.StoreLocation]).GetEnumValues() | ForEach-Object { $_.ToString() }
|
||||
|
||||
$spec = @{
|
||||
options = @{
|
||||
state = @{ type = "str"; default = "present"; choices = "absent", "exported", "present" }
|
||||
path = @{ type = "path" }
|
||||
thumbprint = @{ type = "str" }
|
||||
store_name = @{ type = "str"; default = "My"; choices = $store_name_values }
|
||||
store_location = @{ type = "str"; default = "LocalMachine"; choices = $store_location_values }
|
||||
password = @{ type = "str"; no_log = $true }
|
||||
key_exportable = @{ type = "bool"; default = $true }
|
||||
key_storage = @{ type = "str"; default = "default"; choices = "default", "machine", "user" }
|
||||
file_type = @{ type = "str"; default = "der"; choices = "der", "pem", "pkcs12" }
|
||||
}
|
||||
required_if = @(
|
||||
@("state", "absent", @("path", "thumbprint"), $true),
|
||||
@("state", "exported", @("path", "thumbprint")),
|
||||
@("state", "present", @("path"))
|
||||
)
|
||||
supports_check_mode = $true
|
||||
}
|
||||
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||||
|
||||
Function Get-CertFile($module, $path, $password, $key_exportable, $key_storage) {
|
||||
# parses a certificate file and returns X509Certificate2Collection
|
||||
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
|
||||
$module.FailJson("File at '$path' either does not exist or is not a file")
|
||||
}
|
||||
|
||||
# must set at least the PersistKeySet flag so that the PrivateKey
|
||||
# is stored in a permanent container and not deleted once the handle
|
||||
# is gone.
|
||||
$store_flags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet
|
||||
|
||||
$key_storage = $key_storage.substring(0,1).ToUpper() + $key_storage.substring(1).ToLower()
|
||||
$store_flags = $store_flags -bor [Enum]::Parse([System.Security.Cryptography.X509Certificates.X509KeyStorageFlags], "$($key_storage)KeySet")
|
||||
if ($key_exportable) {
|
||||
$store_flags = $store_flags -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable
|
||||
}
|
||||
|
||||
# TODO: If I'm feeling adventurours, write code to parse PKCS#12 PEM encoded
|
||||
# file as .NET does not have an easy way to import this
|
||||
$certs = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2Collection
|
||||
|
||||
try {
|
||||
$certs.Import($path, $password, $store_flags)
|
||||
} catch {
|
||||
$module.FailJson("Failed to load cert from file: $($_.Exception.Message)", $_)
|
||||
}
|
||||
|
||||
return $certs
|
||||
}
|
||||
|
||||
Function New-CertFile($module, $cert, $path, $type, $password) {
|
||||
$content_type = switch ($type) {
|
||||
"pem" { [System.Security.Cryptography.X509Certificates.X509ContentType]::Cert }
|
||||
"der" { [System.Security.Cryptography.X509Certificates.X509ContentType]::Cert }
|
||||
"pkcs12" { [System.Security.Cryptography.X509Certificates.X509ContentType]::Pkcs12 }
|
||||
}
|
||||
if ($type -eq "pkcs12") {
|
||||
$missing_key = $false
|
||||
if ($null -eq $cert.PrivateKey) {
|
||||
$missing_key = $true
|
||||
} elseif ($cert.PrivateKey.CspKeyContainerInfo.Exportable -eq $false) {
|
||||
$missing_key = $true
|
||||
}
|
||||
if ($missing_key) {
|
||||
$module.FailJson("Cannot export cert with key as PKCS12 when the key is not marked as exportable or not accessible by the current user")
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $path) {
|
||||
Remove-Item -LiteralPath $path -Force
|
||||
$module.Result.changed = $true
|
||||
}
|
||||
try {
|
||||
$cert_bytes = $cert.Export($content_type, $password)
|
||||
} catch {
|
||||
$module.FailJson("Failed to export certificate as bytes: $($_.Exception.Message)", $_)
|
||||
}
|
||||
|
||||
# Need to manually handle a PEM file
|
||||
if ($type -eq "pem") {
|
||||
$cert_content = "-----BEGIN CERTIFICATE-----`r`n"
|
||||
$base64_string = [System.Convert]::ToBase64String($cert_bytes, [System.Base64FormattingOptions]::InsertLineBreaks)
|
||||
$cert_content += $base64_string
|
||||
$cert_content += "`r`n-----END CERTIFICATE-----"
|
||||
$file_encoding = [System.Text.Encoding]::ASCII
|
||||
$cert_bytes = $file_encoding.GetBytes($cert_content)
|
||||
} elseif ($type -eq "pkcs12") {
|
||||
$module.Result.key_exported = $false
|
||||
if ($null -ne $cert.PrivateKey) {
|
||||
$module.Result.key_exportable = $cert.PrivateKey.CspKeyContainerInfo.Exportable
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $module.CheckMode) {
|
||||
try {
|
||||
[System.IO.File]::WriteAllBytes($path, $cert_bytes)
|
||||
} catch [System.ArgumentNullException] {
|
||||
$module.FailJson("Failed to write cert to file, cert was null: $($_.Exception.Message)", $_)
|
||||
} catch [System.IO.IOException] {
|
||||
$module.FailJson("Failed to write cert to file due to IO Exception: $($_.Exception.Message)", $_)
|
||||
} catch [System.UnauthorizedAccessException] {
|
||||
$module.FailJson("Failed to write cert to file due to permissions: $($_.Exception.Message)", $_)
|
||||
} catch {
|
||||
$module.FailJson("Failed to write cert to file: $($_.Exception.Message)", $_)
|
||||
}
|
||||
}
|
||||
$module.Result.changed = $true
|
||||
}
|
||||
|
||||
Function Get-CertFileType($path, $password) {
|
||||
$certs = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2Collection
|
||||
try {
|
||||
$certs.Import($path, $password, 0)
|
||||
} catch [System.Security.Cryptography.CryptographicException] {
|
||||
# the file is a pkcs12 we just had the wrong password
|
||||
return "pkcs12"
|
||||
} catch {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
$file_contents = Get-Content -LiteralPath $path -Raw
|
||||
if ($file_contents.StartsWith("-----BEGIN CERTIFICATE-----")) {
|
||||
return "pem"
|
||||
} elseif ($file_contents.StartsWith("-----BEGIN PKCS7-----")) {
|
||||
return "pkcs7-ascii"
|
||||
} elseif ($certs.Count -gt 1) {
|
||||
# multiple certs must be pkcs7
|
||||
return "pkcs7-binary"
|
||||
} elseif ($certs[0].HasPrivateKey) {
|
||||
return "pkcs12"
|
||||
} elseif ($path.EndsWith(".pfx") -or $path.EndsWith(".p12")) {
|
||||
# no way to differenciate a pfx with a der file so we must rely on the
|
||||
# extension
|
||||
return "pkcs12"
|
||||
} else {
|
||||
return "der"
|
||||
}
|
||||
}
|
||||
|
||||
$state = $module.Params.state
|
||||
$path = $module.Params.path
|
||||
$thumbprint = $module.Params.thumbprint
|
||||
$store_name = [System.Security.Cryptography.X509Certificates.StoreName]"$($module.Params.store_name)"
|
||||
$store_location = [System.Security.Cryptography.X509Certificates.Storelocation]"$($module.Params.store_location)"
|
||||
$password = $module.Params.password
|
||||
$key_exportable = $module.Params.key_exportable
|
||||
$key_storage = $module.Params.key_storage
|
||||
$file_type = $module.Params.file_type
|
||||
|
||||
$module.Result.thumbprints = @()
|
||||
|
||||
$store = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $store_name, $store_location
|
||||
try {
|
||||
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
|
||||
} catch [System.Security.Cryptography.CryptographicException] {
|
||||
$module.FailJson("Unable to open the store as it is not readable: $($_.Exception.Message)", $_)
|
||||
} catch [System.Security.SecurityException] {
|
||||
$module.FailJson("Unable to open the store with the current permissions: $($_.Exception.Message)", $_)
|
||||
} catch {
|
||||
$module.FailJson("Unable to open the store: $($_.Exception.Message)", $_)
|
||||
}
|
||||
$store_certificates = $store.Certificates
|
||||
|
||||
try {
|
||||
if ($state -eq "absent") {
|
||||
$cert_thumbprints = @()
|
||||
|
||||
if ($null -ne $path) {
|
||||
$certs = Get-CertFile -module $module -path $path -password $password -key_exportable $key_exportable -key_storage $key_storage
|
||||
foreach ($cert in $certs) {
|
||||
$cert_thumbprints += $cert.Thumbprint
|
||||
}
|
||||
} elseif ($null -ne $thumbprint) {
|
||||
$cert_thumbprints += $thumbprint
|
||||
}
|
||||
|
||||
foreach ($cert_thumbprint in $cert_thumbprints) {
|
||||
$module.Result.thumbprints += $cert_thumbprint
|
||||
$found_certs = $store_certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $cert_thumbprint, $false)
|
||||
if ($found_certs.Count -gt 0) {
|
||||
foreach ($found_cert in $found_certs) {
|
||||
try {
|
||||
if (-not $module.CheckMode) {
|
||||
$store.Remove($found_cert)
|
||||
}
|
||||
} catch [System.Security.SecurityException] {
|
||||
$module.FailJson("Unable to remove cert with thumbprint '$cert_thumbprint' with current permissions: $($_.Exception.Message)", $_)
|
||||
} catch {
|
||||
$module.FailJson("Unable to remove cert with thumbprint '$cert_thumbprint': $($_.Exception.Message)", $_)
|
||||
}
|
||||
$module.Result.changed = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($state -eq "exported") {
|
||||
# TODO: Add support for PKCS7 and exporting a cert chain
|
||||
$module.Result.thumbprints += $thumbprint
|
||||
$export = $true
|
||||
if (Test-Path -LiteralPath $path -PathType Container) {
|
||||
$module.FailJson("Cannot export cert to path '$path' as it is a directory")
|
||||
} elseif (Test-Path -LiteralPath $path -PathType Leaf) {
|
||||
$actual_cert_type = Get-CertFileType -path $path -password $password
|
||||
if ($actual_cert_type -eq $file_type) {
|
||||
try {
|
||||
$certs = Get-CertFile -module $module -path $path -password $password -key_exportable $key_exportable -key_storage $key_storage
|
||||
} catch {
|
||||
# failed to load the file so we set the thumbprint to something
|
||||
# that will fail validation
|
||||
$certs = @{Thumbprint = $null}
|
||||
}
|
||||
|
||||
if ($certs.Thumbprint -eq $thumbprint) {
|
||||
$export = $false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($export) {
|
||||
$found_certs = $store_certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $thumbprint, $false)
|
||||
if ($found_certs.Count -ne 1) {
|
||||
$module.FailJson("Found $($found_certs.Count) certs when only expecting 1")
|
||||
}
|
||||
|
||||
New-CertFile -module $module -cert $found_certs -path $path -type $file_type -password $password
|
||||
}
|
||||
} else {
|
||||
$certs = Get-CertFile -module $module -path $path -password $password -key_exportable $key_exportable -key_storage $key_storage
|
||||
foreach ($cert in $certs) {
|
||||
$module.Result.thumbprints += $cert.Thumbprint
|
||||
$found_certs = $store_certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $cert.Thumbprint, $false)
|
||||
if ($found_certs.Count -eq 0) {
|
||||
try {
|
||||
if (-not $module.CheckMode) {
|
||||
$store.Add($cert)
|
||||
}
|
||||
} catch [System.Security.Cryptography.CryptographicException] {
|
||||
$module.FailJson("Unable to import certificate with thumbprint '$($cert.Thumbprint)' with the current permissions: $($_.Exception.Message)", $_)
|
||||
} catch {
|
||||
$module.FailJson("Unable to import certificate with thumbprint '$($cert.Thumbprint)': $($_.Exception.Message)", $_)
|
||||
}
|
||||
$module.Result.changed = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
$store.Close()
|
||||
}
|
||||
|
||||
$module.ExitJson()
|
||||
@ -1,208 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_certificate_store
|
||||
version_added: '2.5'
|
||||
short_description: Manages the certificate store
|
||||
description:
|
||||
- Used to import/export and remove certificates and keys from the local
|
||||
certificate store.
|
||||
- This module is not used to create certificates and will only manage existing
|
||||
certs as a file or in the store.
|
||||
- It can be used to import PEM, DER, P7B, PKCS12 (PFX) certificates and export
|
||||
PEM, DER and PKCS12 certificates.
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- If C(present), will ensure that the certificate at I(path) is imported
|
||||
into the certificate store specified.
|
||||
- If C(absent), will ensure that the certificate specified by I(thumbprint)
|
||||
or the thumbprint of the cert at I(path) is removed from the store
|
||||
specified.
|
||||
- If C(exported), will ensure the file at I(path) is a certificate
|
||||
specified by I(thumbprint).
|
||||
- When exporting a certificate, if I(path) is a directory then the module
|
||||
will fail, otherwise the file will be replaced if needed.
|
||||
type: str
|
||||
choices: [ absent, exported, present ]
|
||||
default: present
|
||||
path:
|
||||
description:
|
||||
- The path to a certificate file.
|
||||
- This is required when I(state) is C(present) or C(exported).
|
||||
- When I(state) is C(absent) and I(thumbprint) is not specified, the
|
||||
thumbprint is derived from the certificate at this path.
|
||||
type: path
|
||||
thumbprint:
|
||||
description:
|
||||
- The thumbprint as a hex string to either export or remove.
|
||||
- See the examples for how to specify the thumbprint.
|
||||
type: str
|
||||
store_name:
|
||||
description:
|
||||
- The store name to use when importing a certificate or searching for a
|
||||
certificate.
|
||||
- "C(AddressBook): The X.509 certificate store for other users"
|
||||
- "C(AuthRoot): The X.509 certificate store for third-party certificate authorities (CAs)"
|
||||
- "C(CertificateAuthority): The X.509 certificate store for intermediate certificate authorities (CAs)"
|
||||
- "C(Disallowed): The X.509 certificate store for revoked certificates"
|
||||
- "C(My): The X.509 certificate store for personal certificates"
|
||||
- "C(Root): The X.509 certificate store for trusted root certificate authorities (CAs)"
|
||||
- "C(TrustedPeople): The X.509 certificate store for directly trusted people and resources"
|
||||
- "C(TrustedPublisher): The X.509 certificate store for directly trusted publishers"
|
||||
type: str
|
||||
choices:
|
||||
- AddressBook
|
||||
- AuthRoot
|
||||
- CertificateAuthority
|
||||
- Disallowed
|
||||
- My
|
||||
- Root
|
||||
- TrustedPeople
|
||||
- TrustedPublisher
|
||||
default: My
|
||||
store_location:
|
||||
description:
|
||||
- The store location to use when importing a certificate or searching for a
|
||||
certificate.
|
||||
choices: [ CurrentUser, LocalMachine ]
|
||||
default: LocalMachine
|
||||
password:
|
||||
description:
|
||||
- The password of the pkcs12 certificate key.
|
||||
- This is used when reading a pkcs12 certificate file or the password to
|
||||
set when C(state=exported) and C(file_type=pkcs12).
|
||||
- If the pkcs12 file has no password set or no password should be set on
|
||||
the exported file, do not set this option.
|
||||
type: str
|
||||
key_exportable:
|
||||
description:
|
||||
- Whether to allow the private key to be exported.
|
||||
- If C(no), then this module and other process will only be able to export
|
||||
the certificate and the private key cannot be exported.
|
||||
- Used when C(state=present) only.
|
||||
type: bool
|
||||
default: yes
|
||||
key_storage:
|
||||
description:
|
||||
- Specifies where Windows will store the private key when it is imported.
|
||||
- When set to C(default), the default option as set by Windows is used, typically C(user).
|
||||
- When set to C(machine), the key is stored in a path accessible by various
|
||||
users.
|
||||
- When set to C(user), the key is stored in a path only accessible by the
|
||||
current user.
|
||||
- Used when C(state=present) only and cannot be changed once imported.
|
||||
- See U(https://msdn.microsoft.com/en-us/library/system.security.cryptography.x509certificates.x509keystorageflags.aspx)
|
||||
for more details.
|
||||
type: str
|
||||
choices: [ default, machine, user ]
|
||||
default: default
|
||||
file_type:
|
||||
description:
|
||||
- The file type to export the certificate as when C(state=exported).
|
||||
- C(der) is a binary ASN.1 encoded file.
|
||||
- C(pem) is a base64 encoded file of a der file in the OpenSSL form.
|
||||
- C(pkcs12) (also known as pfx) is a binary container that contains both
|
||||
the certificate and private key unlike the other options.
|
||||
- When C(pkcs12) is set and the private key is not exportable or accessible
|
||||
by the current user, it will throw an exception.
|
||||
type: str
|
||||
choices: [ der, pem, pkcs12 ]
|
||||
default: der
|
||||
notes:
|
||||
- Some actions on PKCS12 certificates and keys may fail with the error
|
||||
C(the specified network password is not correct), either use CredSSP or
|
||||
Kerberos with credential delegation, or use C(become) to bypass these
|
||||
restrictions.
|
||||
- The certificates must be located on the Windows host to be set with I(path).
|
||||
- When importing a certificate for usage in IIS, it is generally required
|
||||
to use the C(machine) key_storage option, as both C(default) and C(user)
|
||||
will make the private key unreadable to IIS APPPOOL identities and prevent
|
||||
binding the certificate to the https endpoint.
|
||||
author:
|
||||
- Jordan Borean (@jborean93)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Import a certificate
|
||||
win_certificate_store:
|
||||
path: C:\Temp\cert.pem
|
||||
state: present
|
||||
|
||||
- name: Import pfx certificate that is password protected
|
||||
win_certificate_store:
|
||||
path: C:\Temp\cert.pfx
|
||||
state: present
|
||||
password: VeryStrongPasswordHere!
|
||||
become: yes
|
||||
become_method: runas
|
||||
|
||||
- name: Import pfx certificate without password and set private key as un-exportable
|
||||
win_certificate_store:
|
||||
path: C:\Temp\cert.pfx
|
||||
state: present
|
||||
key_exportable: no
|
||||
# usually you don't set this here but it is for illustrative purposes
|
||||
vars:
|
||||
ansible_winrm_transport: credssp
|
||||
|
||||
- name: Remove a certificate based on file thumbprint
|
||||
win_certificate_store:
|
||||
path: C:\Temp\cert.pem
|
||||
state: absent
|
||||
|
||||
- name: Remove a certificate based on thumbprint
|
||||
win_certificate_store:
|
||||
thumbprint: BD7AF104CF1872BDB518D95C9534EA941665FD27
|
||||
state: absent
|
||||
|
||||
- name: Remove certificate based on thumbprint is CurrentUser/TrustedPublishers store
|
||||
win_certificate_store:
|
||||
thumbprint: BD7AF104CF1872BDB518D95C9534EA941665FD27
|
||||
state: absent
|
||||
store_location: CurrentUser
|
||||
store_name: TrustedPublisher
|
||||
|
||||
- name: Export certificate as der encoded file
|
||||
win_certificate_store:
|
||||
path: C:\Temp\cert.cer
|
||||
state: exported
|
||||
file_type: der
|
||||
|
||||
- name: Export certificate and key as pfx encoded file
|
||||
win_certificate_store:
|
||||
path: C:\Temp\cert.pfx
|
||||
state: exported
|
||||
file_type: pkcs12
|
||||
password: AnotherStrongPass!
|
||||
become: yes
|
||||
become_method: runas
|
||||
become_user: SYSTEM
|
||||
|
||||
- name: Import certificate be used by IIS
|
||||
win_certificate_store:
|
||||
path: C:\Temp\cert.pfx
|
||||
file_type: pkcs12
|
||||
password: StrongPassword!
|
||||
store_location: LocalMachine
|
||||
key_storage: machine
|
||||
state: present
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
thumbprints:
|
||||
description: A list of certificate thumbprints that were touched by the
|
||||
module.
|
||||
returned: success
|
||||
type: list
|
||||
sample: ["BC05633694E675449136679A658281F17A191087"]
|
||||
'''
|
||||
@ -1,78 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
#Requires -Module Ansible.ModuleUtils.CommandUtil
|
||||
#Requires -Module Ansible.ModuleUtils.FileUtil
|
||||
|
||||
# TODO: add check mode support
|
||||
|
||||
Set-StrictMode -Version 2
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$params = Parse-Args $args -supports_check_mode $false
|
||||
|
||||
$raw_command_line = Get-AnsibleParam -obj $params -name "_raw_params" -type "str" -failifempty $true
|
||||
$chdir = Get-AnsibleParam -obj $params -name "chdir" -type "path"
|
||||
$creates = Get-AnsibleParam -obj $params -name "creates" -type "path"
|
||||
$removes = Get-AnsibleParam -obj $params -name "removes" -type "path"
|
||||
$stdin = Get-AnsibleParam -obj $params -name "stdin" -type "str"
|
||||
$output_encoding_override = Get-AnsibleParam -obj $params -name "output_encoding_override" -type "str"
|
||||
|
||||
$raw_command_line = $raw_command_line.Trim()
|
||||
|
||||
$result = @{
|
||||
changed = $true
|
||||
cmd = $raw_command_line
|
||||
}
|
||||
|
||||
if ($creates -and $(Test-AnsiblePath -Path $creates)) {
|
||||
Exit-Json @{msg="skipped, since $creates exists";cmd=$raw_command_line;changed=$false;skipped=$true;rc=0}
|
||||
}
|
||||
|
||||
if ($removes -and -not $(Test-AnsiblePath -Path $removes)) {
|
||||
Exit-Json @{msg="skipped, since $removes does not exist";cmd=$raw_command_line;changed=$false;skipped=$true;rc=0}
|
||||
}
|
||||
|
||||
$command_args = @{
|
||||
command = $raw_command_line
|
||||
}
|
||||
if ($chdir) {
|
||||
$command_args['working_directory'] = $chdir
|
||||
}
|
||||
if ($stdin) {
|
||||
$command_args['stdin'] = $stdin
|
||||
}
|
||||
if ($output_encoding_override) {
|
||||
$command_args['output_encoding_override'] = $output_encoding_override
|
||||
}
|
||||
|
||||
$start_datetime = [DateTime]::UtcNow
|
||||
try {
|
||||
$command_result = Run-Command @command_args
|
||||
} catch {
|
||||
$result.changed = $false
|
||||
try {
|
||||
$result.rc = $_.Exception.NativeErrorCode
|
||||
} catch {
|
||||
$result.rc = 2
|
||||
}
|
||||
Fail-Json -obj $result -message $_.Exception.Message
|
||||
}
|
||||
|
||||
$result.stdout = $command_result.stdout
|
||||
$result.stderr = $command_result.stderr
|
||||
$result.rc = $command_result.rc
|
||||
|
||||
$end_datetime = [DateTime]::UtcNow
|
||||
$result.start = $start_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff")
|
||||
$result.end = $end_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff")
|
||||
$result.delta = $($end_datetime - $start_datetime).ToString("h\:mm\:ss\.ffffff")
|
||||
|
||||
If ($result.rc -ne 0) {
|
||||
Fail-Json -obj $result -message "non-zero return code"
|
||||
}
|
||||
|
||||
Exit-Json $result
|
||||
@ -1,136 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016, Ansible, inc
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_command
|
||||
short_description: Executes a command on a remote Windows node
|
||||
version_added: 2.2
|
||||
description:
|
||||
- The C(win_command) module takes the command name followed by a list of space-delimited arguments.
|
||||
- The given command will be executed on all selected nodes. It will not be
|
||||
processed through the shell, so variables like C($env:HOME) and operations
|
||||
like C("<"), C(">"), C("|"), and C(";") will not work (use the M(win_shell)
|
||||
module if you need these features).
|
||||
- For non-Windows targets, use the M(command) module instead.
|
||||
options:
|
||||
free_form:
|
||||
description:
|
||||
- The C(win_command) module takes a free form command to run.
|
||||
- There is no parameter actually named 'free form'. See the examples!
|
||||
type: str
|
||||
required: yes
|
||||
creates:
|
||||
description:
|
||||
- A path or path filter pattern; when the referenced path exists on the target host, the task will be skipped.
|
||||
type: path
|
||||
removes:
|
||||
description:
|
||||
- A path or path filter pattern; when the referenced path B(does not) exist on the target host, the task will be skipped.
|
||||
type: path
|
||||
chdir:
|
||||
description:
|
||||
- Set the specified path as the current working directory before executing a command.
|
||||
type: path
|
||||
stdin:
|
||||
description:
|
||||
- Set the stdin of the command directly to the specified value.
|
||||
type: str
|
||||
version_added: '2.5'
|
||||
output_encoding_override:
|
||||
description:
|
||||
- This option overrides the encoding of stdout/stderr output.
|
||||
- You can use this option when you need to run a command which ignore the console's codepage.
|
||||
- You should only need to use this option in very rare circumstances.
|
||||
- This value can be any valid encoding C(Name) based on the output of C([System.Text.Encoding]::GetEncodings()).
|
||||
See U(https://docs.microsoft.com/dotnet/api/system.text.encoding.getencodings).
|
||||
type: str
|
||||
version_added: '2.10'
|
||||
notes:
|
||||
- If you want to run a command through a shell (say you are using C(<),
|
||||
C(>), C(|), etc), you actually want the M(win_shell) module instead. The
|
||||
C(win_command) module is much more secure as it's not affected by the user's
|
||||
environment.
|
||||
- C(creates), C(removes), and C(chdir) can be specified after the command. For instance, if you only want to run a command if a certain file does not
|
||||
exist, use this.
|
||||
seealso:
|
||||
- module: command
|
||||
- module: psexec
|
||||
- module: raw
|
||||
- module: win_psexec
|
||||
- module: win_shell
|
||||
author:
|
||||
- Matt Davis (@nitzmahone)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Save the result of 'whoami' in 'whoami_out'
|
||||
win_command: whoami
|
||||
register: whoami_out
|
||||
|
||||
- name: Run command that only runs if folder exists and runs from a specific folder
|
||||
win_command: wbadmin -backupTarget:C:\backup\
|
||||
args:
|
||||
chdir: C:\somedir\
|
||||
creates: C:\backup\
|
||||
|
||||
- name: Run an executable and send data to the stdin for the executable
|
||||
win_command: powershell.exe -
|
||||
args:
|
||||
stdin: Write-Host test
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
msg:
|
||||
description: changed
|
||||
returned: always
|
||||
type: bool
|
||||
sample: true
|
||||
start:
|
||||
description: The command execution start time
|
||||
returned: always
|
||||
type: str
|
||||
sample: '2016-02-25 09:18:26.429568'
|
||||
end:
|
||||
description: The command execution end time
|
||||
returned: always
|
||||
type: str
|
||||
sample: '2016-02-25 09:18:26.755339'
|
||||
delta:
|
||||
description: The command execution delta time
|
||||
returned: always
|
||||
type: str
|
||||
sample: '0:00:00.325771'
|
||||
stdout:
|
||||
description: The command standard output
|
||||
returned: always
|
||||
type: str
|
||||
sample: 'Clustering node rabbit@slave1 with rabbit@master ...'
|
||||
stderr:
|
||||
description: The command standard error
|
||||
returned: always
|
||||
type: str
|
||||
sample: 'ls: cannot access foo: No such file or directory'
|
||||
cmd:
|
||||
description: The command executed by the task
|
||||
returned: always
|
||||
type: str
|
||||
sample: 'rabbitmqctl join_cluster rabbit@master'
|
||||
rc:
|
||||
description: The command return code (0 means success)
|
||||
returned: always
|
||||
type: int
|
||||
sample: 0
|
||||
stdout_lines:
|
||||
description: The command standard output split in lines
|
||||
returned: always
|
||||
type: list
|
||||
sample: [u'Clustering node rabbit@slave1 with rabbit@master ...']
|
||||
'''
|
||||
@ -1,403 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2015, Jon Hawkesworth (@jhawkesworth) <figs@unity.demon.co.uk>
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
#Requires -Module Ansible.ModuleUtils.Backup
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$params = Parse-Args -arguments $args -supports_check_mode $true
|
||||
$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
|
||||
$diff_mode = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false
|
||||
|
||||
# there are 4 modes to win_copy which are driven by the action plugins:
|
||||
# explode: src is a zip file which needs to be extracted to dest, for use with multiple files
|
||||
# query: win_copy action plugin wants to get the state of remote files to check whether it needs to send them
|
||||
# remote: all copy action is happening remotely (remote_src=True)
|
||||
# single: a single file has been copied, also used with template
|
||||
$copy_mode = Get-AnsibleParam -obj $params -name "_copy_mode" -type "str" -default "single" -validateset "explode","query","remote","single"
|
||||
|
||||
# used in explode, remote and single mode
|
||||
$src = Get-AnsibleParam -obj $params -name "src" -type "path" -failifempty ($copy_mode -in @("explode","process","single"))
|
||||
$dest = Get-AnsibleParam -obj $params -name "dest" -type "path" -failifempty $true
|
||||
$backup = Get-AnsibleParam -obj $params -name "backup" -type "bool" -default $false
|
||||
|
||||
# used in single mode
|
||||
$original_basename = Get-AnsibleParam -obj $params -name "_original_basename" -type "str"
|
||||
|
||||
# used in query and remote mode
|
||||
$force = Get-AnsibleParam -obj $params -name "force" -type "bool" -default $true
|
||||
|
||||
# used in query mode, contains the local files/directories/symlinks that are to be copied
|
||||
$files = Get-AnsibleParam -obj $params -name "files" -type "list"
|
||||
$directories = Get-AnsibleParam -obj $params -name "directories" -type "list"
|
||||
|
||||
$result = @{
|
||||
changed = $false
|
||||
}
|
||||
|
||||
if ($diff_mode) {
|
||||
$result.diff = @{}
|
||||
}
|
||||
|
||||
Function Copy-File($source, $dest) {
|
||||
$diff = ""
|
||||
$copy_file = $false
|
||||
$source_checksum = $null
|
||||
if ($force) {
|
||||
$source_checksum = Get-FileChecksum -path $source
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $dest -PathType Container) {
|
||||
Fail-Json -obj $result -message "cannot copy file from '$source' to '$dest': dest is already a folder"
|
||||
} elseif (Test-Path -LiteralPath $dest -PathType Leaf) {
|
||||
if ($force) {
|
||||
$target_checksum = Get-FileChecksum -path $dest
|
||||
if ($source_checksum -ne $target_checksum) {
|
||||
$copy_file = $true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$copy_file = $true
|
||||
}
|
||||
|
||||
if ($copy_file) {
|
||||
$file_dir = [System.IO.Path]::GetDirectoryName($dest)
|
||||
# validate the parent dir is not a file and that it exists
|
||||
if (Test-Path -LiteralPath $file_dir -PathType Leaf) {
|
||||
Fail-Json -obj $result -message "cannot copy file from '$source' to '$dest': object at dest parent dir is not a folder"
|
||||
} elseif (-not (Test-Path -LiteralPath $file_dir)) {
|
||||
# directory doesn't exist, need to create
|
||||
New-Item -Path $file_dir -ItemType Directory -WhatIf:$check_mode | Out-Null
|
||||
$diff += "+$file_dir\`n"
|
||||
}
|
||||
|
||||
if ($backup) {
|
||||
$result.backup_file = Backup-File -path $dest -WhatIf:$check_mode
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $dest -PathType Leaf) {
|
||||
Remove-Item -LiteralPath $dest -Force -Recurse -WhatIf:$check_mode | Out-Null
|
||||
$diff += "-$dest`n"
|
||||
}
|
||||
|
||||
if (-not $check_mode) {
|
||||
# cannot run with -WhatIf:$check_mode as if the parent dir didn't
|
||||
# exist and was created above would still not exist in check mode
|
||||
Copy-Item -LiteralPath $source -Destination $dest -Force | Out-Null
|
||||
}
|
||||
$diff += "+$dest`n"
|
||||
|
||||
$result.changed = $true
|
||||
}
|
||||
|
||||
# ugly but to save us from running the checksum twice, let's return it for
|
||||
# the main code to add it to $result
|
||||
return ,@{ diff = $diff; checksum = $source_checksum }
|
||||
}
|
||||
|
||||
Function Copy-Folder($source, $dest) {
|
||||
$diff = ""
|
||||
|
||||
if (-not (Test-Path -LiteralPath $dest -PathType Container)) {
|
||||
$parent_dir = [System.IO.Path]::GetDirectoryName($dest)
|
||||
if (Test-Path -LiteralPath $parent_dir -PathType Leaf) {
|
||||
Fail-Json -obj $result -message "cannot copy file from '$source' to '$dest': object at dest parent dir is not a folder"
|
||||
}
|
||||
if (Test-Path -LiteralPath $dest -PathType Leaf) {
|
||||
Fail-Json -obj $result -message "cannot copy folder from '$source' to '$dest': dest is already a file"
|
||||
}
|
||||
|
||||
New-Item -Path $dest -ItemType Container -WhatIf:$check_mode | Out-Null
|
||||
$diff += "+$dest\`n"
|
||||
$result.changed = $true
|
||||
}
|
||||
|
||||
$child_items = Get-ChildItem -LiteralPath $source -Force
|
||||
foreach ($child_item in $child_items) {
|
||||
$dest_child_path = Join-Path -Path $dest -ChildPath $child_item.Name
|
||||
if ($child_item.PSIsContainer) {
|
||||
$diff += (Copy-Folder -source $child_item.Fullname -dest $dest_child_path)
|
||||
} else {
|
||||
$diff += (Copy-File -source $child_item.Fullname -dest $dest_child_path).diff
|
||||
}
|
||||
}
|
||||
|
||||
return $diff
|
||||
}
|
||||
|
||||
Function Get-FileSize($path) {
|
||||
$file = Get-Item -LiteralPath $path -Force
|
||||
if ($file.PSIsContainer) {
|
||||
$size = (Get-ChildItem -Literalpath $file.FullName -Recurse -Force | `
|
||||
Where-Object { $_.PSObject.Properties.Name -contains 'Length' } | `
|
||||
Measure-Object -Property Length -Sum).Sum
|
||||
if ($null -eq $size) {
|
||||
$size = 0
|
||||
}
|
||||
} else {
|
||||
$size = $file.Length
|
||||
}
|
||||
|
||||
$size
|
||||
}
|
||||
|
||||
Function Extract-Zip($src, $dest) {
|
||||
$archive = [System.IO.Compression.ZipFile]::Open($src, [System.IO.Compression.ZipArchiveMode]::Read, [System.Text.Encoding]::UTF8)
|
||||
foreach ($entry in $archive.Entries) {
|
||||
$archive_name = $entry.FullName
|
||||
|
||||
# FullName may be appended with / or \, determine if it is padded and remove it
|
||||
$padding_length = $archive_name.Length % 4
|
||||
if ($padding_length -eq 0) {
|
||||
$is_dir = $false
|
||||
$base64_name = $archive_name
|
||||
} elseif ($padding_length -eq 1) {
|
||||
$is_dir = $true
|
||||
if ($archive_name.EndsWith("/") -or $archive_name.EndsWith("`\")) {
|
||||
$base64_name = $archive_name.Substring(0, $archive_name.Length - 1)
|
||||
} else {
|
||||
throw "invalid base64 archive name '$archive_name'"
|
||||
}
|
||||
} else {
|
||||
throw "invalid base64 length '$archive_name'"
|
||||
}
|
||||
|
||||
# to handle unicode character, win_copy action plugin has encoded the filename
|
||||
$decoded_archive_name = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($base64_name))
|
||||
# re-add the / to the entry full name if it was a directory
|
||||
if ($is_dir) {
|
||||
$decoded_archive_name = "$decoded_archive_name/"
|
||||
}
|
||||
$entry_target_path = [System.IO.Path]::Combine($dest, $decoded_archive_name)
|
||||
$entry_dir = [System.IO.Path]::GetDirectoryName($entry_target_path)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $entry_dir)) {
|
||||
New-Item -Path $entry_dir -ItemType Directory -WhatIf:$check_mode | Out-Null
|
||||
}
|
||||
|
||||
if ($is_dir -eq $false) {
|
||||
if (-not $check_mode) {
|
||||
[System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $entry_target_path, $true)
|
||||
}
|
||||
}
|
||||
}
|
||||
$archive.Dispose() # release the handle of the zip file
|
||||
}
|
||||
|
||||
Function Extract-ZipLegacy($src, $dest) {
|
||||
if (-not (Test-Path -LiteralPath $dest)) {
|
||||
New-Item -Path $dest -ItemType Directory -WhatIf:$check_mode | Out-Null
|
||||
}
|
||||
$shell = New-Object -ComObject Shell.Application
|
||||
$zip = $shell.NameSpace($src)
|
||||
$dest_path = $shell.NameSpace($dest)
|
||||
|
||||
foreach ($entry in $zip.Items()) {
|
||||
$is_dir = $entry.IsFolder
|
||||
$encoded_archive_entry = $entry.Name
|
||||
# to handle unicode character, win_copy action plugin has encoded the filename
|
||||
$decoded_archive_entry = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($encoded_archive_entry))
|
||||
if ($is_dir) {
|
||||
$decoded_archive_entry = "$decoded_archive_entry/"
|
||||
}
|
||||
|
||||
$entry_target_path = [System.IO.Path]::Combine($dest, $decoded_archive_entry)
|
||||
$entry_dir = [System.IO.Path]::GetDirectoryName($entry_target_path)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $entry_dir)) {
|
||||
New-Item -Path $entry_dir -ItemType Directory -WhatIf:$check_mode | Out-Null
|
||||
}
|
||||
|
||||
if ($is_dir -eq $false -and (-not $check_mode)) {
|
||||
# https://msdn.microsoft.com/en-us/library/windows/desktop/bb787866.aspx
|
||||
# From Folder.CopyHere documentation, 1044 means:
|
||||
# - 1024: do not display a user interface if an error occurs
|
||||
# - 16: respond with "yes to all" for any dialog box that is displayed
|
||||
# - 4: do not display a progress dialog box
|
||||
$dest_path.CopyHere($entry, 1044)
|
||||
|
||||
# once file is extraced, we need to rename it with non base64 name
|
||||
$combined_encoded_path = [System.IO.Path]::Combine($dest, $encoded_archive_entry)
|
||||
Move-Item -LiteralPath $combined_encoded_path -Destination $entry_target_path -Force | Out-Null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($copy_mode -eq "query") {
|
||||
# we only return a list of files/directories that need to be copied over
|
||||
# the source of the local file will be the key used
|
||||
$changed_files = @()
|
||||
$changed_directories = @()
|
||||
$changed_symlinks = @()
|
||||
|
||||
foreach ($file in $files) {
|
||||
$filename = $file.dest
|
||||
$local_checksum = $file.checksum
|
||||
|
||||
$filepath = Join-Path -Path $dest -ChildPath $filename
|
||||
if (Test-Path -LiteralPath $filepath -PathType Leaf) {
|
||||
if ($force) {
|
||||
$checksum = Get-FileChecksum -path $filepath
|
||||
if ($checksum -ne $local_checksum) {
|
||||
$changed_files += $file
|
||||
}
|
||||
}
|
||||
} elseif (Test-Path -LiteralPath $filepath -PathType Container) {
|
||||
Fail-Json -obj $result -message "cannot copy file to dest '$filepath': object at path is already a directory"
|
||||
} else {
|
||||
$changed_files += $file
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($directory in $directories) {
|
||||
$dirname = $directory.dest
|
||||
|
||||
$dirpath = Join-Path -Path $dest -ChildPath $dirname
|
||||
$parent_dir = [System.IO.Path]::GetDirectoryName($dirpath)
|
||||
if (Test-Path -LiteralPath $parent_dir -PathType Leaf) {
|
||||
Fail-Json -obj $result -message "cannot copy folder to dest '$dirpath': object at parent directory path is already a file"
|
||||
}
|
||||
if (Test-Path -LiteralPath $dirpath -PathType Leaf) {
|
||||
Fail-Json -obj $result -message "cannot copy folder to dest '$dirpath': object at path is already a file"
|
||||
} elseif (-not (Test-Path -LiteralPath $dirpath -PathType Container)) {
|
||||
$changed_directories += $directory
|
||||
}
|
||||
}
|
||||
|
||||
# TODO: Handle symlinks
|
||||
|
||||
$result.files = $changed_files
|
||||
$result.directories = $changed_directories
|
||||
$result.symlinks = $changed_symlinks
|
||||
} elseif ($copy_mode -eq "explode") {
|
||||
# a single zip file containing the files and directories needs to be
|
||||
# expanded this will always result in a change as the calculation is done
|
||||
# on the win_copy action plugin and is only run if a change needs to occur
|
||||
if (-not (Test-Path -LiteralPath $src -PathType Leaf)) {
|
||||
Fail-Json -obj $result -message "Cannot expand src zip file: '$src' as it does not exist"
|
||||
}
|
||||
|
||||
# Detect if the PS zip assemblies are available or whether to use Shell
|
||||
$use_legacy = $false
|
||||
try {
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem | Out-Null
|
||||
Add-Type -AssemblyName System.IO.Compression | Out-Null
|
||||
} catch {
|
||||
$use_legacy = $true
|
||||
}
|
||||
if ($use_legacy) {
|
||||
Extract-ZipLegacy -src $src -dest $dest
|
||||
} else {
|
||||
Extract-Zip -src $src -dest $dest
|
||||
}
|
||||
|
||||
$result.changed = $true
|
||||
} elseif ($copy_mode -eq "remote") {
|
||||
# all copy actions are happening on the remote side (windows host), need
|
||||
# too copy source and dest using PS code
|
||||
$result.src = $src
|
||||
$result.dest = $dest
|
||||
|
||||
if (-not (Test-Path -LiteralPath $src)) {
|
||||
Fail-Json -obj $result -message "Cannot copy src file: '$src' as it does not exist"
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $src -PathType Container) {
|
||||
# we are copying a directory or the contents of a directory
|
||||
$result.operation = 'folder_copy'
|
||||
if ($src.EndsWith("/") -or $src.EndsWith("`\")) {
|
||||
# copying the folder's contents to dest
|
||||
$diff = ""
|
||||
$child_files = Get-ChildItem -LiteralPath $src -Force
|
||||
foreach ($child_file in $child_files) {
|
||||
$dest_child_path = Join-Path -Path $dest -ChildPath $child_file.Name
|
||||
if ($child_file.PSIsContainer) {
|
||||
$diff += Copy-Folder -source $child_file.FullName -dest $dest_child_path
|
||||
} else {
|
||||
$diff += (Copy-File -source $child_file.FullName -dest $dest_child_path).diff
|
||||
}
|
||||
}
|
||||
} else {
|
||||
# copying the folder and it's contents to dest
|
||||
$dest = Join-Path -Path $dest -ChildPath (Get-Item -LiteralPath $src -Force).Name
|
||||
$result.dest = $dest
|
||||
$diff = Copy-Folder -source $src -dest $dest
|
||||
}
|
||||
} else {
|
||||
# we are just copying a single file to dest
|
||||
$result.operation = 'file_copy'
|
||||
|
||||
$source_basename = (Get-Item -LiteralPath $src -Force).Name
|
||||
$result.original_basename = $source_basename
|
||||
|
||||
if ($dest.EndsWith("/") -or $dest.EndsWith("`\")) {
|
||||
$dest = Join-Path -Path $dest -ChildPath (Get-Item -LiteralPath $src -Force).Name
|
||||
$result.dest = $dest
|
||||
} else {
|
||||
# check if the parent dir exists, this is only done if src is a
|
||||
# file and dest if the path to a file (doesn't end with \ or /)
|
||||
$parent_dir = Split-Path -LiteralPath $dest
|
||||
if (Test-Path -LiteralPath $parent_dir -PathType Leaf) {
|
||||
Fail-Json -obj $result -message "object at destination parent dir '$parent_dir' is currently a file"
|
||||
} elseif (-not (Test-Path -LiteralPath $parent_dir -PathType Container)) {
|
||||
Fail-Json -obj $result -message "Destination directory '$parent_dir' does not exist"
|
||||
}
|
||||
}
|
||||
$copy_result = Copy-File -source $src -dest $dest
|
||||
$diff = $copy_result.diff
|
||||
$result.checksum = $copy_result.checksum
|
||||
}
|
||||
|
||||
# the file might not exist if running in check mode
|
||||
if (-not $check_mode -or (Test-Path -LiteralPath $dest -PathType Leaf)) {
|
||||
$result.size = Get-FileSize -path $dest
|
||||
} else {
|
||||
$result.size = $null
|
||||
}
|
||||
if ($diff_mode) {
|
||||
$result.diff.prepared = $diff
|
||||
}
|
||||
} elseif ($copy_mode -eq "single") {
|
||||
# a single file is located in src and we need to copy to dest, this will
|
||||
# always result in a change as the calculation is done on the Ansible side
|
||||
# before this is run. This should also never run in check mode
|
||||
if (-not (Test-Path -LiteralPath $src -PathType Leaf)) {
|
||||
Fail-Json -obj $result -message "Cannot copy src file: '$src' as it does not exist"
|
||||
}
|
||||
|
||||
# the dest parameter is a directory, we need to append original_basename
|
||||
if ($dest.EndsWith("/") -or $dest.EndsWith("`\") -or (Test-Path -LiteralPath $dest -PathType Container)) {
|
||||
$remote_dest = Join-Path -Path $dest -ChildPath $original_basename
|
||||
$parent_dir = Split-Path -LiteralPath $remote_dest
|
||||
|
||||
# when dest ends with /, we need to create the destination directories
|
||||
if (Test-Path -LiteralPath $parent_dir -PathType Leaf) {
|
||||
Fail-Json -obj $result -message "object at destination parent dir '$parent_dir' is currently a file"
|
||||
} elseif (-not (Test-Path -LiteralPath $parent_dir -PathType Container)) {
|
||||
New-Item -Path $parent_dir -ItemType Directory | Out-Null
|
||||
}
|
||||
} else {
|
||||
$remote_dest = $dest
|
||||
$parent_dir = Split-Path -LiteralPath $remote_dest
|
||||
|
||||
# check if the dest parent dirs exist, need to fail if they don't
|
||||
if (Test-Path -LiteralPath $parent_dir -PathType Leaf) {
|
||||
Fail-Json -obj $result -message "object at destination parent dir '$parent_dir' is currently a file"
|
||||
} elseif (-not (Test-Path -LiteralPath $parent_dir -PathType Container)) {
|
||||
Fail-Json -obj $result -message "Destination directory '$parent_dir' does not exist"
|
||||
}
|
||||
}
|
||||
|
||||
if ($backup) {
|
||||
$result.backup_file = Backup-File -path $remote_dest -WhatIf:$check_mode
|
||||
}
|
||||
|
||||
Copy-Item -LiteralPath $src -Destination $remote_dest -Force | Out-Null
|
||||
$result.changed = $true
|
||||
}
|
||||
|
||||
Exit-Json -obj $result
|
||||
@ -1,207 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2015, Jon Hawkesworth (@jhawkesworth) <figs@unity.demon.co.uk>
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['stableinterface'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_copy
|
||||
version_added: '1.9.2'
|
||||
short_description: Copies files to remote locations on windows hosts
|
||||
description:
|
||||
- The C(win_copy) module copies a file on the local box to remote windows locations.
|
||||
- For non-Windows targets, use the M(copy) module instead.
|
||||
options:
|
||||
content:
|
||||
description:
|
||||
- When used instead of C(src), sets the contents of a file directly to the
|
||||
specified value.
|
||||
- This is for simple values, for anything complex or with formatting please
|
||||
switch to the M(template) module.
|
||||
type: str
|
||||
version_added: '2.3'
|
||||
decrypt:
|
||||
description:
|
||||
- This option controls the autodecryption of source files using vault.
|
||||
type: bool
|
||||
default: yes
|
||||
version_added: '2.5'
|
||||
dest:
|
||||
description:
|
||||
- Remote absolute path where the file should be copied to.
|
||||
- If C(src) is a directory, this must be a directory too.
|
||||
- Use \ for path separators or \\ when in "double quotes".
|
||||
- If C(dest) ends with \ then source or the contents of source will be
|
||||
copied to the directory without renaming.
|
||||
- If C(dest) is a nonexistent path, it will only be created if C(dest) ends
|
||||
with "/" or "\", or C(src) is a directory.
|
||||
- If C(src) and C(dest) are files and if the parent directory of C(dest)
|
||||
doesn't exist, then the task will fail.
|
||||
type: path
|
||||
required: yes
|
||||
backup:
|
||||
description:
|
||||
- Determine whether a backup should be created.
|
||||
- When set to C(yes), create a backup file including the timestamp information
|
||||
so you can get the original file back if you somehow clobbered it incorrectly.
|
||||
- No backup is taken when C(remote_src=False) and multiple files are being
|
||||
copied.
|
||||
type: bool
|
||||
default: no
|
||||
version_added: '2.8'
|
||||
force:
|
||||
description:
|
||||
- If set to C(yes), the file will only be transferred if the content
|
||||
is different than destination.
|
||||
- If set to C(no), the file will only be transferred if the
|
||||
destination does not exist.
|
||||
- If set to C(no), no checksuming of the content is performed which can
|
||||
help improve performance on larger files.
|
||||
type: bool
|
||||
default: yes
|
||||
version_added: '2.3'
|
||||
local_follow:
|
||||
description:
|
||||
- This flag indicates that filesystem links in the source tree, if they
|
||||
exist, should be followed.
|
||||
type: bool
|
||||
default: yes
|
||||
version_added: '2.4'
|
||||
remote_src:
|
||||
description:
|
||||
- If C(no), it will search for src at originating/master machine.
|
||||
- If C(yes), it will go to the remote/target machine for the src.
|
||||
type: bool
|
||||
default: no
|
||||
version_added: '2.3'
|
||||
src:
|
||||
description:
|
||||
- Local path to a file to copy to the remote server; can be absolute or
|
||||
relative.
|
||||
- If path is a directory, it is copied (including the source folder name)
|
||||
recursively to C(dest).
|
||||
- If path is a directory and ends with "/", only the inside contents of
|
||||
that directory are copied to the destination. Otherwise, if it does not
|
||||
end with "/", the directory itself with all contents is copied.
|
||||
- If path is a file and dest ends with "\", the file is copied to the
|
||||
folder with the same filename.
|
||||
- Required unless using C(content).
|
||||
type: path
|
||||
notes:
|
||||
- Currently win_copy does not support copying symbolic links from both local to
|
||||
remote and remote to remote.
|
||||
- It is recommended that backslashes C(\) are used instead of C(/) when dealing
|
||||
with remote paths.
|
||||
- Because win_copy runs over WinRM, it is not a very efficient transfer
|
||||
mechanism. If sending large files consider hosting them on a web service and
|
||||
using M(win_get_url) instead.
|
||||
seealso:
|
||||
- module: assemble
|
||||
- module: copy
|
||||
- module: win_get_url
|
||||
- module: win_robocopy
|
||||
author:
|
||||
- Jon Hawkesworth (@jhawkesworth)
|
||||
- Jordan Borean (@jborean93)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Copy a single file
|
||||
win_copy:
|
||||
src: /srv/myfiles/foo.conf
|
||||
dest: C:\Temp\renamed-foo.conf
|
||||
|
||||
- name: Copy a single file, but keep a backup
|
||||
win_copy:
|
||||
src: /srv/myfiles/foo.conf
|
||||
dest: C:\Temp\renamed-foo.conf
|
||||
backup: yes
|
||||
|
||||
- name: Copy a single file keeping the filename
|
||||
win_copy:
|
||||
src: /src/myfiles/foo.conf
|
||||
dest: C:\Temp\
|
||||
|
||||
- name: Copy folder to C:\Temp (results in C:\Temp\temp_files)
|
||||
win_copy:
|
||||
src: files/temp_files
|
||||
dest: C:\Temp
|
||||
|
||||
- name: Copy folder contents recursively
|
||||
win_copy:
|
||||
src: files/temp_files/
|
||||
dest: C:\Temp
|
||||
|
||||
- name: Copy a single file where the source is on the remote host
|
||||
win_copy:
|
||||
src: C:\Temp\foo.txt
|
||||
dest: C:\ansible\foo.txt
|
||||
remote_src: yes
|
||||
|
||||
- name: Copy a folder recursively where the source is on the remote host
|
||||
win_copy:
|
||||
src: C:\Temp
|
||||
dest: C:\ansible
|
||||
remote_src: yes
|
||||
|
||||
- name: Set the contents of a file
|
||||
win_copy:
|
||||
content: abc123
|
||||
dest: C:\Temp\foo.txt
|
||||
|
||||
- name: Copy a single file as another user
|
||||
win_copy:
|
||||
src: NuGet.config
|
||||
dest: '%AppData%\NuGet\NuGet.config'
|
||||
vars:
|
||||
ansible_become_user: user
|
||||
ansible_become_password: pass
|
||||
# The tmp dir must be set when using win_copy as another user
|
||||
# This ensures the become user will have permissions for the operation
|
||||
# Make sure to specify a folder both the ansible_user and the become_user have access to (i.e not %TEMP% which is user specific and requires Admin)
|
||||
ansible_remote_tmp: 'c:\tmp'
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
backup_file:
|
||||
description: Name of the backup file that was created.
|
||||
returned: if backup=yes
|
||||
type: str
|
||||
sample: C:\Path\To\File.txt.11540.20150212-220915.bak
|
||||
dest:
|
||||
description: Destination file/path.
|
||||
returned: changed
|
||||
type: str
|
||||
sample: C:\Temp\
|
||||
src:
|
||||
description: Source file used for the copy on the target machine.
|
||||
returned: changed
|
||||
type: str
|
||||
sample: /home/httpd/.ansible/tmp/ansible-tmp-1423796390.97-147729857856000/source
|
||||
checksum:
|
||||
description: SHA1 checksum of the file after running copy.
|
||||
returned: success, src is a file
|
||||
type: str
|
||||
sample: 6e642bb8dd5c2e027bf21dd923337cbb4214f827
|
||||
size:
|
||||
description: Size of the target, after execution.
|
||||
returned: changed, src is a file
|
||||
type: int
|
||||
sample: 1220
|
||||
operation:
|
||||
description: Whether a single file copy took place or a folder copy.
|
||||
returned: success
|
||||
type: str
|
||||
sample: file_copy
|
||||
original_basename:
|
||||
description: Basename of the copied file.
|
||||
returned: changed, src is a file
|
||||
type: str
|
||||
sample: foo.txt
|
||||
'''
|
||||
@ -1,360 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
|
||||
Set-StrictMode -Version 2
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ConfirmPreference = "None"
|
||||
|
||||
Set-Variable -Visibility Public -Option ReadOnly,AllScope,Constant -Name "AddressFamilies" -Value @{
|
||||
[System.Net.Sockets.AddressFamily]::InterNetworkV6 = 'IPv6'
|
||||
[System.Net.Sockets.AddressFamily]::InterNetwork = 'IPv4'
|
||||
}
|
||||
|
||||
$result = @{changed=$false}
|
||||
|
||||
$params = Parse-Args -arguments $args -supports_check_mode $true
|
||||
Set-Variable -Visibility Public -Option ReadOnly,AllScope,Constant -Name "log_path" -Value (
|
||||
Get-AnsibleParam $params "log_path"
|
||||
)
|
||||
$adapter_names = Get-AnsibleParam $params "adapter_names" -Default "*"
|
||||
$dns_servers = Get-AnsibleParam $params "dns_servers" -aliases "ipv4_addresses","ip_addresses","addresses" -FailIfEmpty $result
|
||||
$check_mode = Get-AnsibleParam $params "_ansible_check_mode" -Default $false
|
||||
|
||||
|
||||
Function Write-DebugLog {
|
||||
Param(
|
||||
[string]$msg
|
||||
)
|
||||
|
||||
$DebugPreference = "Continue"
|
||||
$ErrorActionPreference = "Continue"
|
||||
$date_str = Get-Date -Format u
|
||||
$msg = "$date_str $msg"
|
||||
|
||||
Write-Debug $msg
|
||||
if($log_path) {
|
||||
Add-Content $log_path $msg
|
||||
}
|
||||
}
|
||||
|
||||
Function Get-OptionalProperty {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Retreives a property that may not exist from an object that may be null.
|
||||
Optionally returns a default value.
|
||||
Optionally coalesces to a new type with -as.
|
||||
May return null, but will not throw.
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(ValueFromPipeline=$true)]
|
||||
[Object]
|
||||
$InputObject ,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[String]
|
||||
$Name ,
|
||||
|
||||
[Parameter()]
|
||||
[AllowNull()]
|
||||
[Object]
|
||||
$Default ,
|
||||
|
||||
[Parameter()]
|
||||
[System.Type]
|
||||
$As
|
||||
)
|
||||
|
||||
Process {
|
||||
if ($null -eq $InputObject) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$value = if ($InputObject.PSObject.Properties.Name -contains $Name) {
|
||||
$InputObject.$Name
|
||||
} else {
|
||||
$Default
|
||||
}
|
||||
|
||||
if ($As) {
|
||||
return $value -as $As
|
||||
}
|
||||
|
||||
return $value
|
||||
}
|
||||
}
|
||||
|
||||
Function Get-NetAdapterInfo {
|
||||
[CmdletBinding()]
|
||||
Param (
|
||||
[Parameter(ValueFromPipeline=$true)]
|
||||
[String]$Name = "*"
|
||||
)
|
||||
|
||||
Process {
|
||||
if (Get-Command -Name Get-NetAdapter -ErrorAction SilentlyContinue) {
|
||||
$adapter_info = Get-NetAdapter @PSBoundParameters | Select-Object -Property Name, InterfaceIndex
|
||||
} else {
|
||||
# Older hosts 2008/2008R2 don't have Get-NetAdapter, fallback to deprecated Win32_NetworkAdapter
|
||||
$cim_params = @{
|
||||
ClassName = "Win32_NetworkAdapter"
|
||||
Property = "InterfaceIndex", "NetConnectionID"
|
||||
}
|
||||
|
||||
if ($Name.Contains("*")) {
|
||||
$cim_params.Filter = "NetConnectionID LIKE '$($Name.Replace("*", "%"))'"
|
||||
} else {
|
||||
$cim_params.Filter = "NetConnectionID = '$Name'"
|
||||
}
|
||||
|
||||
$adapter_info = Get-CimInstance @cim_params | Select-Object -Property @(
|
||||
@{Name="Name"; Expression={$_.NetConnectionID}},
|
||||
@{Name="InterfaceIndex"; Expression={$_.InterfaceIndex}}
|
||||
)
|
||||
}
|
||||
|
||||
# Need to filter the adapter that are not IPEnabled, while we are at it, also get the DNS config.
|
||||
$net_info = $adapter_info | ForEach-Object -Process {
|
||||
$cim_params = @{
|
||||
ClassName = "Win32_NetworkAdapterConfiguration"
|
||||
Filter = "InterfaceIndex = $($_.InterfaceIndex)"
|
||||
Property = "DNSServerSearchOrder", "IPEnabled", "SettingID"
|
||||
}
|
||||
$adapter_config = Get-CimInstance @cim_params |
|
||||
Select-Object -Property DNSServerSearchOrder, IPEnabled, @{
|
||||
Name = 'InterfaceGuid'
|
||||
Expression = { $_.SettingID }
|
||||
}
|
||||
|
||||
if ($adapter_config.IPEnabled -eq $false) {
|
||||
return
|
||||
}
|
||||
|
||||
$reg_info = $adapter_config | Get-RegistryNameServerInfo
|
||||
|
||||
[PSCustomObject]@{
|
||||
Name = $_.Name
|
||||
InterfaceIndex = $_.InterfaceIndex
|
||||
InterfaceGuid = $adapter_config.InterfaceGuid
|
||||
RegInfo = $reg_info
|
||||
}
|
||||
}
|
||||
|
||||
if (@($net_info).Count -eq 0 -and -not $Name.Contains("*")) {
|
||||
throw "Get-NetAdapterInfo: Failed to find network adapter(s) that are IP enabled with the name '$Name'"
|
||||
}
|
||||
|
||||
$net_info
|
||||
}
|
||||
}
|
||||
|
||||
Function Get-RegistryNameServerInfo {
|
||||
[CmdletBinding()]
|
||||
Param (
|
||||
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
|
||||
[System.Guid]
|
||||
$InterfaceGuid
|
||||
)
|
||||
|
||||
Begin {
|
||||
$protoItems = @{
|
||||
[System.Net.Sockets.AddressFamily]::InterNetwork = @{
|
||||
Interface = 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\{{{0}}}'
|
||||
StaticNameServer = 'NameServer'
|
||||
DhcpNameServer = 'DhcpNameServer'
|
||||
EnableDhcp = 'EnableDHCP'
|
||||
}
|
||||
|
||||
[System.Net.Sockets.AddressFamily]::InterNetworkV6 = @{
|
||||
Interface = 'HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters\Interfaces\{{{0}}}'
|
||||
StaticNameServer = 'NameServer'
|
||||
DhcpNameServer = 'Dhcpv6DNSServers'
|
||||
EnableDhcp = 'EnableDHCP'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
foreach ($addrFamily in $AddressFamilies.Keys) {
|
||||
$items = $protoItems[$addrFamily]
|
||||
$regPath = $items.Interface -f $InterfaceGuid
|
||||
|
||||
if (($iface = Get-Item -LiteralPath $regPath -ErrorAction Ignore)) {
|
||||
$iprop = $iface | Get-ItemProperty
|
||||
$famInfo = @{
|
||||
AddressFamily = $addrFamily
|
||||
UsingDhcp = Get-OptionalProperty -InputObject $iprop -Name $items.EnableDhcp -As bool
|
||||
EffectiveNameServers = @()
|
||||
DhcpAssignedNameServers = @()
|
||||
NameServerBadFormat = $false
|
||||
}
|
||||
|
||||
if (($ns = Get-OptionalProperty -InputObject $iprop -Name $items.DhcpNameServer)) {
|
||||
$famInfo.EffectiveNameServers = $famInfo.DhcpAssignedNameServers = $ns.Split(' ')
|
||||
}
|
||||
|
||||
if (($ns = Get-OptionalProperty -InputObject $iprop -Name $items.StaticNameServer)) {
|
||||
$famInfo.EffectiveNameServers = $famInfo.StaticNameServers = $ns -split '[,;\ ]'
|
||||
$famInfo.UsingDhcp = $false
|
||||
$famInfo.NameServerBadFormat = $ns -match '[;\ ]'
|
||||
}
|
||||
|
||||
$famInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# minimal impl of Set-DnsClientServerAddress for 2008/2008R2
|
||||
Function Set-DnsClientServerAddressLegacy {
|
||||
Param(
|
||||
[int]$InterfaceIndex,
|
||||
[Array]$ServerAddresses=@(),
|
||||
[switch]$ResetServerAddresses
|
||||
)
|
||||
$cim_params = @{
|
||||
ClassName = "Win32_NetworkAdapterConfiguration"
|
||||
Filter = "InterfaceIndex = $InterfaceIndex"
|
||||
KeyOnly = $true
|
||||
}
|
||||
$adapter_config = Get-CimInstance @cim_params
|
||||
|
||||
If($ResetServerAddresses) {
|
||||
$arguments = @{}
|
||||
}
|
||||
Else {
|
||||
$arguments = @{ DNSServerSearchOrder = [string[]]$ServerAddresses }
|
||||
}
|
||||
$res = Invoke-CimMethod -InputObject $adapter_config -MethodName SetDNSServerSearchOrder -Arguments $arguments
|
||||
|
||||
If($res.ReturnValue -ne 0) {
|
||||
throw "Set-DnsClientServerAddressLegacy: Error calling SetDNSServerSearchOrder, code $($res.ReturnValue))"
|
||||
}
|
||||
}
|
||||
|
||||
If(-not $(Get-Command Set-DnsClientServerAddress -ErrorAction SilentlyContinue)) {
|
||||
New-Alias Set-DnsClientServerAddress Set-DnsClientServerAddressLegacy
|
||||
}
|
||||
|
||||
Function Test-DnsClientMatch {
|
||||
Param(
|
||||
[PSCustomObject]$AdapterInfo,
|
||||
[System.Net.IPAddress[]] $dns_servers
|
||||
)
|
||||
Write-DebugLog ("Getting DNS config for adapter {0}" -f $AdapterInfo.Name)
|
||||
|
||||
foreach ($proto in $AdapterInfo.RegInfo) {
|
||||
$desired_dns = if ($dns_servers) {
|
||||
$dns_servers | Where-Object -FilterScript {$_.AddressFamily -eq $proto.AddressFamily}
|
||||
}
|
||||
|
||||
$current_dns = [System.Net.IPAddress[]]($proto.EffectiveNameServers)
|
||||
Write-DebugLog ("Current DNS settings for '{1}' Address Family: {0}" -f ([string[]]$current_dns -join ", "),$AddressFamilies[$proto.AddressFamily])
|
||||
|
||||
if ($proto.NameServerBadFormat) {
|
||||
Write-DebugLog "Malicious DNS server format detected. Will set DNS desired state."
|
||||
return $false
|
||||
# See: https://www.welivesecurity.com/2016/06/02/crouching-tiger-hidden-dns/
|
||||
}
|
||||
|
||||
if ($proto.UsingDhcp -and -not $desired_dns) {
|
||||
Write-DebugLog "DHCP DNS Servers are in use and no DNS servers were requested (DHCP is desired)."
|
||||
} else {
|
||||
if ($desired_dns -and -not $current_dns) {
|
||||
Write-DebugLog "There are currently no DNS servers in use, but they should be present."
|
||||
return $false
|
||||
}
|
||||
|
||||
if ($current_dns -and -not $desired_dns) {
|
||||
Write-DebugLog "There are currently DNS servers in use, but they should be absent."
|
||||
return $false
|
||||
}
|
||||
|
||||
if ($null -ne $current_dns -and
|
||||
$null -ne $desired_dns -and
|
||||
(Compare-Object -ReferenceObject $current_dns -DifferenceObject $desired_dns -SyncWindow 0)) {
|
||||
Write-DebugLog "Static DNS servers are not in the desired state (incorrect or in the wrong order)."
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
Write-DebugLog ("Current DNS settings match ({0})." -f ([string[]]$desired_dns -join ", "))
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
|
||||
Function Assert-IPAddress {
|
||||
Param([string] $address)
|
||||
|
||||
$addrout = $null
|
||||
|
||||
return [System.Net.IPAddress]::TryParse($address, [ref] $addrout)
|
||||
}
|
||||
|
||||
Function Set-DnsClientAddresses
|
||||
{
|
||||
Param(
|
||||
[PSCustomObject]$AdapterInfo,
|
||||
[System.Net.IPAddress[]] $dns_servers
|
||||
)
|
||||
|
||||
Write-DebugLog ("Setting DNS addresses for adapter {0} to ({1})" -f $AdapterInfo.Name, ([string[]]$dns_servers -join ", "))
|
||||
|
||||
If ($dns_servers) {
|
||||
Set-DnsClientServerAddress -InterfaceIndex $AdapterInfo.InterfaceIndex -ServerAddresses $dns_servers
|
||||
} Else {
|
||||
Set-DnsClientServerAddress -InterfaceIndex $AdapterInfo.InterfaceIndex -ResetServerAddress
|
||||
}
|
||||
}
|
||||
|
||||
if($dns_servers -is [string]) {
|
||||
if($dns_servers.Length -gt 0) {
|
||||
$dns_servers = @($dns_servers)
|
||||
} else {
|
||||
$dns_servers = @()
|
||||
}
|
||||
}
|
||||
# Using object equals here, to check for exact match (without implicit type conversion)
|
||||
if([System.Object]::Equals($adapter_names, "*")) {
|
||||
$adapters = Get-NetAdapterInfo
|
||||
} else {
|
||||
$adapters = $adapter_names | Get-NetAdapterInfo
|
||||
}
|
||||
|
||||
Try {
|
||||
|
||||
Write-DebugLog ("Validating IP addresses ({0})" -f ($dns_servers -join ", "))
|
||||
$invalid_addresses = @($dns_servers | Where-Object { -not (Assert-IPAddress $_) })
|
||||
if($invalid_addresses.Count -gt 0) {
|
||||
throw "Invalid IP address(es): ({0})" -f ($invalid_addresses -join ", ")
|
||||
}
|
||||
|
||||
foreach($adapter_info in $adapters) {
|
||||
Write-DebugLog ("Validating adapter name {0}" -f $adapter_info.Name)
|
||||
|
||||
if(-not (Test-DnsClientMatch $adapter_info $dns_servers)) {
|
||||
$result.changed = $true
|
||||
if(-not $check_mode) {
|
||||
Set-DnsClientAddresses $adapter_info $dns_servers
|
||||
} else {
|
||||
Write-DebugLog "Check mode, skipping"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Exit-Json $result
|
||||
|
||||
}
|
||||
Catch {
|
||||
$excep = $_
|
||||
|
||||
Write-DebugLog "Exception: $($excep | out-string)"
|
||||
|
||||
Throw
|
||||
}
|
||||
@ -1,72 +0,0 @@
|
||||
#!/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)
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_dns_client
|
||||
version_added: "2.3"
|
||||
short_description: Configures DNS lookup on Windows hosts
|
||||
description:
|
||||
- The C(win_dns_client) module configures the DNS client on Windows network adapters.
|
||||
options:
|
||||
adapter_names:
|
||||
description:
|
||||
- Adapter name or list of adapter names for which to manage DNS settings ('*' is supported as a wildcard value).
|
||||
- The adapter name used is the connection caption in the Network Control Panel or the InterfaceAlias of C(Get-DnsClientServerAddress).
|
||||
type: list
|
||||
required: yes
|
||||
dns_servers:
|
||||
description:
|
||||
- Single or ordered list of DNS servers (IPv4 and IPv6 addresses) to configure for lookup.
|
||||
- An empty list will configure the adapter to use the DHCP-assigned values on connections where DHCP is enabled,
|
||||
or disable DNS lookup on statically-configured connections.
|
||||
- IPv6 DNS servers can only be set on Windows Server 2012 or newer, older hosts can only set IPv4 addresses.
|
||||
- Before 2.10 use ipv4_addresses instead.
|
||||
type: list
|
||||
required: yes
|
||||
aliases: [ "ipv4_addresses", "ip_addresses", "addresses" ]
|
||||
notes:
|
||||
- Before 2.10, when setting an empty list of DNS server addresses on an adapter with DHCP enabled, a change was always registered.
|
||||
- In 2.10, DNS servers will always be reset if the format of nameservers in the registry is not comma delimited.
|
||||
See U(https://www.welivesecurity.com/2016/06/02/crouching-tiger-hidden-dns/)
|
||||
author:
|
||||
- Matt Davis (@nitzmahone)
|
||||
- Brian Scholer (@briantist)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Set a single address on the adapter named Ethernet
|
||||
win_dns_client:
|
||||
adapter_names: Ethernet
|
||||
dns_servers: 192.168.34.5
|
||||
|
||||
- name: Set multiple lookup addresses on all visible adapters (usually physical adapters that are in the Up state), with debug logging to a file
|
||||
win_dns_client:
|
||||
adapter_names: '*'
|
||||
dns_servers:
|
||||
- 192.168.34.5
|
||||
- 192.168.34.6
|
||||
log_path: C:\dns_log.txt
|
||||
|
||||
- name: Set IPv6 DNS servers on the adapter named Ethernet
|
||||
win_dns_client:
|
||||
adapter_names: Ethernet
|
||||
dns_servers:
|
||||
- '2001:db8::2'
|
||||
- '2001:db8::3'
|
||||
|
||||
- name: Configure all adapters whose names begin with Ethernet to use DHCP-assigned DNS values
|
||||
win_dns_client:
|
||||
adapter_names: 'Ethernet*'
|
||||
dns_servers: []
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
'''
|
||||
@ -1,160 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
|
||||
Set-StrictMode -Version 2
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# FUTURE: Consider action wrapper to manage reboots and credential changes
|
||||
|
||||
Function Ensure-Prereqs {
|
||||
$gwf = Get-WindowsFeature AD-Domain-Services
|
||||
if ($gwf.InstallState -ne "Installed") {
|
||||
$result.changed = $true
|
||||
|
||||
# NOTE: AD-Domain-Services includes: RSAT-AD-AdminCenter, RSAT-AD-Powershell and RSAT-ADDS-Tools
|
||||
$awf = Add-WindowsFeature AD-Domain-Services -WhatIf:$check_mode
|
||||
$result.reboot_required = $awf.RestartNeeded
|
||||
# FUTURE: Check if reboot necessary
|
||||
|
||||
return $true
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
$params = Parse-Args $args -supports_check_mode $true
|
||||
$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -default $false
|
||||
$dns_domain_name = Get-AnsibleParam -obj $params -name "dns_domain_name" -failifempty $true
|
||||
$domain_netbios_name = Get-AnsibleParam -obj $params -name "domain_netbios_name"
|
||||
$safe_mode_admin_password = Get-AnsibleParam -obj $params -name "safe_mode_password" -failifempty $true
|
||||
$database_path = Get-AnsibleParam -obj $params -name "database_path" -type "path"
|
||||
$sysvol_path = Get-AnsibleParam -obj $params -name "sysvol_path" -type "path"
|
||||
$log_path = Get-AnsibleParam -obj $params -name "log_path" -type "path"
|
||||
$create_dns_delegation = Get-AnsibleParam -obj $params -name "create_dns_delegation" -type "bool"
|
||||
$domain_mode = Get-AnsibleParam -obj $params -name "domain_mode" -type "str"
|
||||
$forest_mode = Get-AnsibleParam -obj $params -name "forest_mode" -type "str"
|
||||
$install_dns = Get-AnsibleParam -obj $params -name "install_dns" -type "bool" -default $true
|
||||
|
||||
# FUTURE: Support down to Server 2012?
|
||||
if ([System.Environment]::OSVersion.Version -lt [Version]"6.3.9600.0") {
|
||||
Fail-Json -message "win_domain requires Windows Server 2012R2 or higher"
|
||||
}
|
||||
|
||||
# Check that domain_netbios_name is less than 15 characters
|
||||
if ($domain_netbios_name -and $domain_netbios_name.length -gt 15) {
|
||||
Fail-Json -message "The parameter 'domain_netbios_name' should not exceed 15 characters in length"
|
||||
}
|
||||
|
||||
$result = @{
|
||||
changed=$false;
|
||||
reboot_required=$false;
|
||||
}
|
||||
|
||||
# FUTURE: Any sane way to do the detection under check-mode *without* installing the feature?
|
||||
$installed = Ensure-Prereqs
|
||||
|
||||
# when in check mode and the prereq was "installed" we need to exit early as
|
||||
# the AD cmdlets weren't really installed
|
||||
if ($check_mode -and $installed) {
|
||||
Exit-Json -obj $result
|
||||
}
|
||||
|
||||
# Check that we got a valid domain_mode
|
||||
$valid_domain_modes = [Enum]::GetNames((Get-Command -Name Install-ADDSForest).Parameters.DomainMode.ParameterType)
|
||||
if (($null -ne $domain_mode) -and -not ($domain_mode -in $valid_domain_modes)) {
|
||||
Fail-Json -obj $result -message "The parameter 'domain_mode' does not accept '$domain_mode', please use one of: $valid_domain_modes"
|
||||
}
|
||||
|
||||
# Check that we got a valid forest_mode
|
||||
$valid_forest_modes = [Enum]::GetNames((Get-Command -Name Install-ADDSForest).Parameters.ForestMode.ParameterType)
|
||||
if (($null -ne $forest_mode) -and -not ($forest_mode -in $valid_forest_modes)) {
|
||||
Fail-Json -obj $result -message "The parameter 'forest_mode' does not accept '$forest_mode', please use one of: $valid_forest_modes"
|
||||
}
|
||||
|
||||
$forest = $null
|
||||
try {
|
||||
# Cannot use Get-ADForest as that requires credential delegation, the below does not
|
||||
$forest_context = New-Object -TypeName System.DirectoryServices.ActiveDirectory.DirectoryContext -ArgumentList Forest, $dns_domain_name
|
||||
$forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($forest_context)
|
||||
} catch [System.DirectoryServices.ActiveDirectory.ActiveDirectoryObjectNotFoundException] {
|
||||
} catch [System.DirectoryServices.ActiveDirectory.ActiveDirectoryOperationException] { }
|
||||
|
||||
if (-not $forest) {
|
||||
$result.changed = $true
|
||||
|
||||
$sm_cred = ConvertTo-SecureString $safe_mode_admin_password -AsPlainText -Force
|
||||
|
||||
$install_params = @{
|
||||
DomainName=$dns_domain_name;
|
||||
SafeModeAdministratorPassword=$sm_cred;
|
||||
Confirm=$false;
|
||||
SkipPreChecks=$true;
|
||||
InstallDns=$install_dns;
|
||||
NoRebootOnCompletion=$true;
|
||||
WhatIf=$check_mode;
|
||||
}
|
||||
|
||||
if ($database_path) {
|
||||
$install_params.DatabasePath = $database_path
|
||||
}
|
||||
|
||||
if ($sysvol_path) {
|
||||
$install_params.SysvolPath = $sysvol_path
|
||||
}
|
||||
|
||||
if ($log_path) {
|
||||
$install_params.LogPath = $log_path
|
||||
}
|
||||
|
||||
if ($domain_netbios_name) {
|
||||
$install_params.DomainNetBiosName = $domain_netbios_name
|
||||
}
|
||||
|
||||
if ($null -ne $create_dns_delegation) {
|
||||
$install_params.CreateDnsDelegation = $create_dns_delegation
|
||||
}
|
||||
|
||||
if ($domain_mode) {
|
||||
$install_params.DomainMode = $domain_mode
|
||||
}
|
||||
|
||||
if ($forest_mode) {
|
||||
$install_params.ForestMode = $forest_mode
|
||||
}
|
||||
|
||||
$iaf = $null
|
||||
try {
|
||||
$iaf = Install-ADDSForest @install_params
|
||||
} catch [Microsoft.DirectoryServices.Deployment.DCPromoExecutionException] {
|
||||
# ExitCode 15 == 'Role change is in progress or this computer needs to be restarted.'
|
||||
# DCPromo exit codes details can be found at https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/deploy/troubleshooting-domain-controller-deployment
|
||||
if ($_.Exception.ExitCode -in @(15, 19)) {
|
||||
$result.reboot_required = $true
|
||||
} else {
|
||||
Fail-Json -obj $result -message "Failed to install ADDSForest, DCPromo exited with $($_.Exception.ExitCode): $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
if ($check_mode) {
|
||||
# the return value after -WhatIf does not have RebootRequired populated
|
||||
# manually set to True as the domain would have been installed
|
||||
$result.reboot_required = $true
|
||||
} elseif ($null -ne $iaf) {
|
||||
$result.reboot_required = $iaf.RebootRequired
|
||||
|
||||
# The Netlogon service is set to auto start but is not started. This is
|
||||
# required for Ansible to connect back to the host and reboot in a
|
||||
# later task. Even if this fails Ansible can still connect but only
|
||||
# with ansible_winrm_transport=basic so we just display a warning if
|
||||
# this fails.
|
||||
try {
|
||||
Start-Service -Name Netlogon
|
||||
} catch {
|
||||
Add-Warning -obj $result -message "Failed to start the Netlogon service after promoting the host, Ansible may be unable to connect until the host is manually rebooting: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Exit-Json $result
|
||||
@ -1,121 +0,0 @@
|
||||
#!/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)
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
module: win_domain
|
||||
short_description: Ensures the existence of a Windows domain
|
||||
version_added: 2.3
|
||||
description:
|
||||
- Ensure that the domain named by C(dns_domain_name) exists and is reachable.
|
||||
- If the domain is not reachable, the domain is created in a new forest on the target Windows Server 2012R2+ host.
|
||||
- This module may require subsequent use of the M(win_reboot) action if changes are made.
|
||||
options:
|
||||
dns_domain_name:
|
||||
description:
|
||||
- The DNS name of the domain which should exist and be reachable or reside on the target Windows host.
|
||||
type: str
|
||||
required: yes
|
||||
domain_netbios_name:
|
||||
description:
|
||||
- The NetBIOS name for the root domain in the new forest.
|
||||
- For NetBIOS names to be valid for use with this parameter they must be single label names of 15 characters or less, if not it will fail.
|
||||
- If this parameter is not set, then the default is automatically computed from the value of the I(domain_name) parameter.
|
||||
type: str
|
||||
version_added: '2.6'
|
||||
safe_mode_password:
|
||||
description:
|
||||
- Safe mode password for the domain controller.
|
||||
type: str
|
||||
required: yes
|
||||
database_path:
|
||||
description:
|
||||
- The path to a directory on a fixed disk of the Windows host where the
|
||||
domain database will be created.
|
||||
- If not set then the default path is C(%SYSTEMROOT%\NTDS).
|
||||
type: path
|
||||
version_added: '2.5'
|
||||
log_path:
|
||||
description:
|
||||
- Specifies the fully qualified, non-UNC path to a directory on a fixed disk of the local computer where the log file for this operation is written.
|
||||
- If not set then the default path is C(%SYSTEMROOT%\NTDS).
|
||||
type: path
|
||||
version_added: '2.10'
|
||||
sysvol_path:
|
||||
description:
|
||||
- The path to a directory on a fixed disk of the Windows host where the
|
||||
Sysvol file will be created.
|
||||
- If not set then the default path is C(%SYSTEMROOT%\SYSVOL).
|
||||
type: path
|
||||
version_added: '2.5'
|
||||
create_dns_delegation:
|
||||
description:
|
||||
- Whether to create a DNS delegation that references the new DNS server that you install along with the domain controller.
|
||||
- Valid for Active Directory-integrated DNS only.
|
||||
- The default is computed automatically based on the environment.
|
||||
type: bool
|
||||
version_added: '2.8'
|
||||
domain_mode:
|
||||
description:
|
||||
- Specifies the domain functional level of the first domain in the creation of a new forest.
|
||||
- The domain functional level cannot be lower than the forest functional level, but it can be higher.
|
||||
- The default is automatically computed and set.
|
||||
type: str
|
||||
choices: [ Win2003, Win2008, Win2008R2, Win2012, Win2012R2, WinThreshold ]
|
||||
version_added: '2.8'
|
||||
forest_mode:
|
||||
description:
|
||||
- Specifies the forest functional level for the new forest.
|
||||
- The default forest functional level in Windows Server is typically the same as the version you are running.
|
||||
# - Beware that the default forest functional level in Windows Server 2008 R2 when you create a new forest is C(Win2003).
|
||||
type: str
|
||||
choices: [ Win2003, Win2008, Win2008R2, Win2012, Win2012R2, WinThreshold ]
|
||||
version_added: '2.8'
|
||||
install_dns:
|
||||
description:
|
||||
- Whether to install the DNS service when creating the domain controller.
|
||||
type: bool
|
||||
default: yes
|
||||
version_added: '2.10'
|
||||
seealso:
|
||||
- module: win_domain_controller
|
||||
- module: win_domain_computer
|
||||
- module: win_domain_group
|
||||
- module: win_domain_membership
|
||||
- module: win_domain_user
|
||||
author:
|
||||
- Matt Davis (@nitzmahone)
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
reboot_required:
|
||||
description: True if changes were made that require a reboot.
|
||||
returned: always
|
||||
type: bool
|
||||
sample: true
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Create new domain in a new forest on the target host
|
||||
win_domain:
|
||||
dns_domain_name: ansible.vagrant
|
||||
safe_mode_password: password123!
|
||||
|
||||
- name: Create new Windows domain in a new forest with specific parameters
|
||||
win_domain:
|
||||
create_dns_delegation: no
|
||||
database_path: C:\Windows\NTDS
|
||||
dns_domain_name: ansible.vagrant
|
||||
domain_mode: Win2012R2
|
||||
domain_netbios_name: ANSIBLE
|
||||
forest_mode: Win2012R2
|
||||
safe_mode_password: password123!
|
||||
sysvol_path: C:\Windows\SYSVOL
|
||||
register: domain_install
|
||||
'''
|
||||
@ -1,299 +0,0 @@
|
||||
#!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
|
||||
|
||||
Set-StrictMode -Version 2
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ConfirmPreference = "None"
|
||||
|
||||
$log_path = $null
|
||||
|
||||
Function Write-DebugLog {
|
||||
Param(
|
||||
[string]$msg
|
||||
)
|
||||
|
||||
$DebugPreference = "Continue"
|
||||
$ErrorActionPreference = "Continue"
|
||||
$date_str = Get-Date -Format u
|
||||
$msg = "$date_str $msg"
|
||||
|
||||
Write-Debug $msg
|
||||
if($log_path) {
|
||||
Add-Content $log_path $msg
|
||||
}
|
||||
}
|
||||
|
||||
$required_features = @("AD-Domain-Services","RSAT-ADDS")
|
||||
|
||||
Function Get-MissingFeatures {
|
||||
Write-DebugLog "Checking for missing Windows features..."
|
||||
|
||||
$features = @(Get-WindowsFeature $required_features)
|
||||
|
||||
If($features.Count -ne $required_features.Count) {
|
||||
Throw "One or more Windows features required for a domain controller are unavailable"
|
||||
}
|
||||
|
||||
$missing_features = @($features | Where-Object InstallState -ne Installed)
|
||||
|
||||
return ,$missing_features # no, the comma's not a typo- allows us to return an empty array
|
||||
}
|
||||
|
||||
Function Ensure-FeatureInstallation {
|
||||
# ensure RSAT-ADDS and AD-Domain-Services features are installed
|
||||
|
||||
Write-DebugLog "Ensuring required Windows features are installed..."
|
||||
$feature_result = Install-WindowsFeature $required_features
|
||||
$result.reboot_required = $feature_result.RestartNeeded
|
||||
|
||||
If(-not $feature_result.Success) {
|
||||
Exit-Json -message ("Error installing AD-Domain-Services and RSAT-ADDS features: {0}" -f ($feature_result | Out-String))
|
||||
}
|
||||
}
|
||||
|
||||
# return the domain we're a DC for, or null if not a DC
|
||||
Function Get-DomainControllerDomain {
|
||||
Write-DebugLog "Checking for domain controller role and domain name"
|
||||
|
||||
$sys_cim = Get-CIMInstance Win32_ComputerSystem
|
||||
|
||||
$is_dc = $sys_cim.DomainRole -in (4,5) # backup/primary DC
|
||||
# this will be our workgroup or joined-domain if we're not a DC
|
||||
$domain = $sys_cim.Domain
|
||||
|
||||
Switch($is_dc) {
|
||||
$true { return $domain }
|
||||
Default { return $null }
|
||||
}
|
||||
}
|
||||
|
||||
Function Create-Credential {
|
||||
Param(
|
||||
[string] $cred_user,
|
||||
[string] $cred_password
|
||||
)
|
||||
|
||||
$cred = New-Object System.Management.Automation.PSCredential($cred_user, $($cred_password | ConvertTo-SecureString -AsPlainText -Force))
|
||||
|
||||
Return $cred
|
||||
}
|
||||
|
||||
Function Get-OperationMasterRoles {
|
||||
$assigned_roles = @((Get-ADDomainController -Server localhost).OperationMasterRoles)
|
||||
|
||||
Return ,$assigned_roles # no, the comma's not a typo- allows us to return an empty array
|
||||
}
|
||||
|
||||
$result = @{
|
||||
changed = $false
|
||||
reboot_required = $false
|
||||
}
|
||||
|
||||
$params = Parse-Args -arguments $args -supports_check_mode $true
|
||||
|
||||
$dns_domain_name = Get-AnsibleParam -obj $params -name "dns_domain_name"
|
||||
$safe_mode_password= Get-AnsibleParam -obj $params -name "safe_mode_password"
|
||||
$domain_admin_user = Get-AnsibleParam -obj $params -name "domain_admin_user" -failifempty $result
|
||||
$domain_admin_password= Get-AnsibleParam -obj $params -name "domain_admin_password" -failifempty $result
|
||||
$local_admin_password= Get-AnsibleParam -obj $params -name "local_admin_password"
|
||||
$database_path = Get-AnsibleParam -obj $params -name "database_path" -type "path"
|
||||
$sysvol_path = Get-AnsibleParam -obj $params -name "sysvol_path" -type "path"
|
||||
$domain_log_path = Get-AnsibleParam -obj $params -name "domain_log_path" -type "path" # TODO: Use log_path and alias domain_log_path once the log_path for debug logging option has been removed.
|
||||
$read_only = Get-AnsibleParam -obj $params -name "read_only" -type "bool" -default $false
|
||||
$site_name = Get-AnsibleParam -obj $params -name "site_name" -type "str" -failifempty $read_only
|
||||
$install_dns = Get-AnsibleParam -obj $params -name "install_dns" -type "bool"
|
||||
|
||||
$state = Get-AnsibleParam -obj $params -name "state" -validateset ("domain_controller", "member_server") -failifempty $result
|
||||
|
||||
$log_path = Get-AnsibleParam -obj $params -name "log_path"
|
||||
if ($log_path) {
|
||||
$msg = "Param 'log_path' is deprecated. See the module docs for more information"
|
||||
Add-DeprecationWarning -obj $result -message $msg -version "2.14"
|
||||
}
|
||||
$_ansible_check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -default $false
|
||||
|
||||
$global:log_path = $log_path
|
||||
|
||||
Try {
|
||||
# ensure target OS support; < 2012 doesn't have cmdlet support for DC promotion
|
||||
If(-not (Get-Command Install-WindowsFeature -ErrorAction SilentlyContinue)) {
|
||||
Fail-Json -message "win_domain_controller requires at least Windows Server 2012"
|
||||
}
|
||||
|
||||
# validate args
|
||||
If($state -eq "domain_controller") {
|
||||
If(-not $dns_domain_name) {
|
||||
Fail-Json -message "dns_domain_name is required when desired state is 'domain_controller'"
|
||||
}
|
||||
|
||||
If(-not $safe_mode_password) {
|
||||
Fail-Json -message "safe_mode_password is required when desired state is 'domain_controller'"
|
||||
}
|
||||
|
||||
# ensure that domain admin user is in UPN or down-level domain format (prevent hang from https://support.microsoft.com/en-us/kb/2737935)
|
||||
If(-not $domain_admin_user.Contains("\") -and -not $domain_admin_user.Contains("@")) {
|
||||
Fail-Json -message "domain_admin_user must be in domain\user or user@domain.com format"
|
||||
}
|
||||
}
|
||||
Else { # member_server
|
||||
If(-not $local_admin_password) {
|
||||
Fail-Json -message "local_admin_password is required when desired state is 'member_server'"
|
||||
}
|
||||
}
|
||||
|
||||
# short-circuit "member server" check, since we don't need feature checks for this...
|
||||
|
||||
$current_dc_domain = Get-DomainControllerDomain
|
||||
|
||||
If($state -eq "member_server" -and -not $current_dc_domain) {
|
||||
Exit-Json $result
|
||||
}
|
||||
|
||||
# all other operations will require the AD-DS and RSAT-ADDS features...
|
||||
|
||||
$missing_features = Get-MissingFeatures
|
||||
|
||||
If($missing_features.Count -gt 0) {
|
||||
Write-DebugLog ("Missing Windows features ({0}), need to install" -f ($missing_features -join ", "))
|
||||
$result.changed = $true # we need to install features
|
||||
If($_ansible_check_mode) {
|
||||
# bail out here- we can't proceed without knowing the features are installed
|
||||
Write-DebugLog "check-mode, exiting early"
|
||||
Exit-Json $result
|
||||
}
|
||||
|
||||
Ensure-FeatureInstallation | Out-Null
|
||||
}
|
||||
|
||||
$domain_admin_cred = Create-Credential -cred_user $domain_admin_user -cred_password $domain_admin_password
|
||||
|
||||
switch($state) {
|
||||
domain_controller {
|
||||
If(-not $safe_mode_password) {
|
||||
Fail-Json -message "safe_mode_password is required for state=domain_controller"
|
||||
}
|
||||
|
||||
If($current_dc_domain) {
|
||||
# FUTURE: implement managed Remove/Add to change domains?
|
||||
|
||||
If($current_dc_domain -ne $dns_domain_name) {
|
||||
Fail-Json "$(hostname) is a domain controller for domain $current_dc_domain; changing DC domains is not implemented"
|
||||
}
|
||||
}
|
||||
|
||||
# need to promote to DC
|
||||
If(-not $current_dc_domain) {
|
||||
Write-DebugLog "Not currently a domain controller; needs promotion"
|
||||
$result.changed = $true
|
||||
If($_ansible_check_mode) {
|
||||
Write-DebugLog "check-mode, exiting early"
|
||||
Fail-Json -message $result
|
||||
}
|
||||
|
||||
$result.reboot_required = $true
|
||||
|
||||
$safe_mode_secure = $safe_mode_password | ConvertTo-SecureString -AsPlainText -Force
|
||||
Write-DebugLog "Installing domain controller..."
|
||||
$install_params = @{
|
||||
DomainName = $dns_domain_name
|
||||
Credential = $domain_admin_cred
|
||||
SafeModeAdministratorPassword = $safe_mode_secure
|
||||
}
|
||||
if ($database_path) {
|
||||
$install_params.DatabasePath = $database_path
|
||||
}
|
||||
if ($domain_log_path) {
|
||||
$install_params.LogPath = $domain_log_path
|
||||
}
|
||||
if ($sysvol_path) {
|
||||
$install_params.SysvolPath = $sysvol_path
|
||||
}
|
||||
if ($read_only) {
|
||||
# while this is a switch value, if we set on $false site_name is required
|
||||
# https://github.com/ansible/ansible/issues/35858
|
||||
$install_params.ReadOnlyReplica = $true
|
||||
}
|
||||
if ($site_name) {
|
||||
$install_params.SiteName = $site_name
|
||||
}
|
||||
if ($null -ne $install_dns) {
|
||||
$install_params.InstallDns = $install_dns
|
||||
}
|
||||
try
|
||||
{
|
||||
$null = Install-ADDSDomainController -NoRebootOnCompletion -Force @install_params
|
||||
} catch [Microsoft.DirectoryServices.Deployment.DCPromoExecutionException] {
|
||||
# ExitCode 15 == 'Role change is in progress or this computer needs to be restarted.'
|
||||
# DCPromo exit codes details can be found at https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/deploy/troubleshooting-domain-controller-deployment
|
||||
if ($_.Exception.ExitCode -eq 15) {
|
||||
$result.reboot_required = $true
|
||||
} else {
|
||||
Fail-Json -obj $result -message "Failed to install ADDSDomainController with DCPromo: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
# If $_.FullyQualifiedErrorId -eq 'Test.VerifyUserCredentialPermissions.DCPromo.General.25,Microsoft.DirectoryServices.Deployment.PowerShell.Commands.InstallADDSDomainControllerCommand'
|
||||
# the module failed to resolve the given dns domain name
|
||||
|
||||
Write-DebugLog "Installation complete, trying to start the Netlogon service"
|
||||
# The Netlogon service is set to auto start but is not started. This is
|
||||
# required for Ansible to connect back to the host and reboot in a
|
||||
# later task. Even if this fails Ansible can still connect but only
|
||||
# with ansible_winrm_transport=basic so we just display a warning if
|
||||
# this fails.
|
||||
try {
|
||||
Start-Service -Name Netlogon
|
||||
} catch {
|
||||
Write-DebugLog "Failed to start the Netlogon service: $($_.Exception.Message)"
|
||||
Add-Warning -obj $result -message "Failed to start the Netlogon service after promoting the host, Ansible may be unable to connect until the host is manually rebooting: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
Write-DebugLog "Domain Controller setup completed, needs reboot..."
|
||||
}
|
||||
}
|
||||
member_server {
|
||||
If(-not $local_admin_password) {
|
||||
Fail-Json -message "local_admin_password is required for state=domain_controller"
|
||||
}
|
||||
# at this point we already know we're a DC and shouldn't be...
|
||||
Write-DebugLog "Need to uninstall domain controller..."
|
||||
$result.changed = $true
|
||||
|
||||
Write-DebugLog "Checking for operation master roles assigned to this DC..."
|
||||
|
||||
$assigned_roles = Get-OperationMasterRoles
|
||||
|
||||
# FUTURE: figure out a sane way to hand off roles automatically (designated recipient server, randomly look one up?)
|
||||
If($assigned_roles.Count -gt 0) {
|
||||
Fail-Json -message ("This domain controller has operation master role(s) ({0}) assigned; they must be moved to other DCs before demotion (see Move-ADDirectoryServerOperationMasterRole)" -f ($assigned_roles -join ", "))
|
||||
}
|
||||
|
||||
If($_ansible_check_mode) {
|
||||
Write-DebugLog "check-mode, exiting early"
|
||||
Exit-Json $result
|
||||
}
|
||||
|
||||
$result.reboot_required = $true
|
||||
|
||||
$local_admin_secure = $local_admin_password | ConvertTo-SecureString -AsPlainText -Force
|
||||
|
||||
Write-DebugLog "Uninstalling domain controller..."
|
||||
Uninstall-ADDSDomainController -NoRebootOnCompletion -LocalAdministratorPassword $local_admin_secure -Credential $domain_admin_cred
|
||||
Write-DebugLog "Uninstallation complete, needs reboot..."
|
||||
}
|
||||
default { throw ("invalid state {0}" -f $state) }
|
||||
}
|
||||
|
||||
Exit-Json $result
|
||||
}
|
||||
Catch {
|
||||
$excep = $_
|
||||
|
||||
Write-DebugLog "Exception: $($excep | out-string)"
|
||||
|
||||
Throw
|
||||
}
|
||||
@ -1,156 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2017, Red Hat, Inc.
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
module: win_domain_controller
|
||||
short_description: Manage domain controller/member server state for a Windows host
|
||||
version_added: '2.3'
|
||||
description:
|
||||
- Ensure that a Windows Server 2012+ host is configured as a domain controller or demoted to member server.
|
||||
- This module may require subsequent use of the M(win_reboot) action if changes are made.
|
||||
options:
|
||||
dns_domain_name:
|
||||
description:
|
||||
- When C(state) is C(domain_controller), the DNS name of the domain for which the targeted Windows host should be a DC.
|
||||
type: str
|
||||
domain_admin_user:
|
||||
description:
|
||||
- Username of a domain admin for the target domain (necessary to promote or demote a domain controller).
|
||||
type: str
|
||||
required: true
|
||||
domain_admin_password:
|
||||
description:
|
||||
- Password for the specified C(domain_admin_user).
|
||||
type: str
|
||||
required: true
|
||||
safe_mode_password:
|
||||
description:
|
||||
- Safe mode password for the domain controller (required when C(state) is C(domain_controller)).
|
||||
type: str
|
||||
local_admin_password:
|
||||
description:
|
||||
- Password to be assigned to the local C(Administrator) user (required when C(state) is C(member_server)).
|
||||
type: str
|
||||
read_only:
|
||||
description:
|
||||
- Whether to install the domain controller as a read only replica for an existing domain.
|
||||
type: bool
|
||||
default: no
|
||||
version_added: '2.5'
|
||||
site_name:
|
||||
description:
|
||||
- Specifies the name of an existing site where you can place the new domain controller.
|
||||
- This option is required when I(read_only) is C(yes).
|
||||
type: str
|
||||
version_added: '2.5'
|
||||
state:
|
||||
description:
|
||||
- Whether the target host should be a domain controller or a member server.
|
||||
type: str
|
||||
choices: [ domain_controller, member_server ]
|
||||
database_path:
|
||||
description:
|
||||
- The path to a directory on a fixed disk of the Windows host where the
|
||||
domain database will be created..
|
||||
- If not set then the default path is C(%SYSTEMROOT%\NTDS).
|
||||
type: path
|
||||
version_added: '2.5'
|
||||
domain_log_path:
|
||||
description:
|
||||
- Specified the fully qualified, non-UNC path to a directory on a fixed disk of the local computer that will
|
||||
contain the domain log files.
|
||||
type: path
|
||||
version_added: '2.10'
|
||||
sysvol_path:
|
||||
description:
|
||||
- The path to a directory on a fixed disk of the Windows host where the
|
||||
Sysvol folder will be created.
|
||||
- If not set then the default path is C(%SYSTEMROOT%\SYSVOL).
|
||||
type: path
|
||||
version_added: '2.5'
|
||||
install_dns:
|
||||
description:
|
||||
- Whether to install the DNS service when creating the domain controller.
|
||||
- If not specified then the C(-InstallDns) option is not supplied to C(Install-ADDSDomainController) command,
|
||||
see U(https://docs.microsoft.com/en-us/powershell/module/addsdeployment/install-addsdomaincontroller).
|
||||
type: bool
|
||||
version_added: '2.10'
|
||||
log_path:
|
||||
description:
|
||||
- The path to log any debug information when running the module.
|
||||
- This option is deprecated and should not be used, it will be removed in Ansible 2.14.
|
||||
- This does not relate to the C(-LogPath) paramter of the install controller cmdlet.
|
||||
type: str
|
||||
seealso:
|
||||
- module: win_domain
|
||||
- module: win_domain_computer
|
||||
- module: win_domain_group
|
||||
- module: win_domain_membership
|
||||
- module: win_domain_user
|
||||
author:
|
||||
- Matt Davis (@nitzmahone)
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
reboot_required:
|
||||
description: True if changes were made that require a reboot.
|
||||
returned: always
|
||||
type: bool
|
||||
sample: true
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Ensure a server is a domain controller
|
||||
win_domain_controller:
|
||||
dns_domain_name: ansible.vagrant
|
||||
domain_admin_user: testguy@ansible.vagrant
|
||||
domain_admin_password: password123!
|
||||
safe_mode_password: password123!
|
||||
state: domain_controller
|
||||
|
||||
# ensure a server is not a domain controller
|
||||
# note that without an action wrapper, in the case where a DC is demoted,
|
||||
# the task will fail with a 401 Unauthorized, because the domain credential
|
||||
# becomes invalid to fetch the final output over WinRM. This requires win_async
|
||||
# with credential switching (or other clever credential-switching
|
||||
# mechanism to get the output and trigger the required reboot)
|
||||
- win_domain_controller:
|
||||
domain_admin_user: testguy@ansible.vagrant
|
||||
domain_admin_password: password123!
|
||||
local_admin_password: password123!
|
||||
state: member_server
|
||||
|
||||
- name: Promote server as a read only domain controller
|
||||
win_domain_controller:
|
||||
dns_domain_name: ansible.vagrant
|
||||
domain_admin_user: testguy@ansible.vagrant
|
||||
domain_admin_password: password123!
|
||||
safe_mode_password: password123!
|
||||
state: domain_controller
|
||||
read_only: yes
|
||||
site_name: London
|
||||
|
||||
- name: Promote server with custom paths
|
||||
win_domain_controller:
|
||||
dns_domain_name: ansible.vagrant
|
||||
domain_admin_user: testguy@ansible.vagrant
|
||||
domain_admin_password: password123!
|
||||
safe_mode_password: password123!
|
||||
state: domain_controller
|
||||
sysvol_path: D:\SYSVOL
|
||||
database_path: D:\NTDS
|
||||
domain_log_path: D:\NTDS
|
||||
register: dc_promotion
|
||||
|
||||
- name: Reboot after promotion
|
||||
win_reboot:
|
||||
when: dc_promotion.reboot_required
|
||||
'''
|
||||
@ -1,315 +0,0 @@
|
||||
#!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
|
||||
|
||||
Set-StrictMode -Version 2
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$log_path = $null
|
||||
|
||||
Function Write-DebugLog {
|
||||
Param(
|
||||
[string]$msg
|
||||
)
|
||||
|
||||
$DebugPreference = "Continue"
|
||||
$date_str = Get-Date -Format u
|
||||
$msg = "$date_str $msg"
|
||||
|
||||
Write-Debug $msg
|
||||
if($log_path) {
|
||||
Add-Content $log_path $msg
|
||||
}
|
||||
}
|
||||
|
||||
Function Get-DomainMembershipMatch {
|
||||
Param(
|
||||
[string] $dns_domain_name
|
||||
)
|
||||
|
||||
# FUTURE: add support for NetBIOS domain name?
|
||||
|
||||
# this requires the DC to be accessible; "DC unavailable" is indistinguishable from "not joined to the domain"...
|
||||
Try {
|
||||
Write-DebugLog "calling GetComputerDomain()"
|
||||
$current_dns_domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain().Name
|
||||
|
||||
$domain_match = $current_dns_domain -eq $dns_domain_name
|
||||
|
||||
Write-DebugLog ("current domain {0} matches {1}: {2}" -f $current_dns_domain, $dns_domain_name, $domain_match)
|
||||
|
||||
return $domain_match
|
||||
}
|
||||
catch [System.Security.Authentication.AuthenticationException] {
|
||||
Write-DebugLog "Failed to get computer domain. Attempting a different method."
|
||||
Add-Type -AssemblyName System.DirectoryServices.AccountManagement
|
||||
$user_principal = [System.DirectoryServices.AccountManagement.UserPrincipal]::Current
|
||||
If ($user_principal.ContextType -eq "Machine") {
|
||||
$current_dns_domain = (Get-CimInstance -ClassName Win32_ComputerSystem -Property Domain).Domain
|
||||
|
||||
$domain_match = $current_dns_domain -eq $dns_domain_name
|
||||
|
||||
Write-DebugLog ("current domain {0} matches {1}: {2}" -f $current_dns_domain, $dns_domain_name, $domain_match)
|
||||
|
||||
return $domain_match
|
||||
}
|
||||
Else {
|
||||
Fail-Json -obj $result -message "Failed to authenticate with domain controller and cannot retrieve the existing domain name: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
Catch [System.DirectoryServices.ActiveDirectory.ActiveDirectoryObjectNotFoundException] {
|
||||
Write-DebugLog "not currently joined to a reachable domain"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
Function Create-Credential {
|
||||
Param(
|
||||
[string] $cred_user,
|
||||
[string] $cred_pass
|
||||
)
|
||||
|
||||
$cred = New-Object System.Management.Automation.PSCredential($cred_user, $($cred_pass | ConvertTo-SecureString -AsPlainText -Force))
|
||||
|
||||
return $cred
|
||||
}
|
||||
|
||||
Function Get-HostnameMatch {
|
||||
Param(
|
||||
[string] $hostname
|
||||
)
|
||||
|
||||
# Add-Computer will validate the "shape" of the hostname- we just care if it matches...
|
||||
|
||||
$hostname_match = $env:COMPUTERNAME -eq $hostname
|
||||
Write-DebugLog ("current hostname {0} matches {1}: {2}" -f $env:COMPUTERNAME, $hostname, $hostname_match)
|
||||
|
||||
return $hostname_match
|
||||
}
|
||||
|
||||
Function Is-DomainJoined {
|
||||
return (Get-CIMInstance Win32_ComputerSystem).PartOfDomain
|
||||
}
|
||||
|
||||
Function Join-Domain {
|
||||
Param(
|
||||
[string] $dns_domain_name,
|
||||
[string] $new_hostname,
|
||||
[string] $domain_admin_user,
|
||||
[string] $domain_admin_password,
|
||||
[string] $domain_ou_path
|
||||
)
|
||||
|
||||
Write-DebugLog ("Creating credential for user {0}" -f $domain_admin_user)
|
||||
$domain_cred = Create-Credential $domain_admin_user $domain_admin_password
|
||||
|
||||
$add_args = @{
|
||||
ComputerName="."
|
||||
Credential=$domain_cred
|
||||
DomainName=$dns_domain_name
|
||||
Force=$null
|
||||
}
|
||||
|
||||
Write-DebugLog "adding hostname set arg to Add-Computer args"
|
||||
If($new_hostname) {
|
||||
$add_args["NewName"] = $new_hostname
|
||||
}
|
||||
|
||||
|
||||
if($domain_ou_path){
|
||||
Write-DebugLog "adding OU destination arg to Add-Computer args"
|
||||
$add_args["OUPath"] = $domain_ou_path
|
||||
}
|
||||
$argstr = $add_args | Out-String
|
||||
Write-DebugLog "calling Add-Computer with args: $argstr"
|
||||
try {
|
||||
$add_result = Add-Computer @add_args
|
||||
} catch {
|
||||
Fail-Json -obj $result -message "failed to join domain: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
Write-DebugLog ("Add-Computer result was \n{0}" -f $add_result | Out-String)
|
||||
}
|
||||
|
||||
Function Get-Workgroup {
|
||||
return (Get-CIMInstance Win32_ComputerSystem).Workgroup
|
||||
}
|
||||
|
||||
Function Set-Workgroup {
|
||||
Param(
|
||||
[string] $workgroup_name
|
||||
)
|
||||
|
||||
Write-DebugLog ("Calling JoinDomainOrWorkgroup with workgroup {0}" -f $workgroup_name)
|
||||
try {
|
||||
$swg_result = Get-CimInstance Win32_ComputerSystem | Invoke-CimMethod -MethodName JoinDomainOrWorkgroup -Arguments @{Name="$workgroup_name"}
|
||||
} catch {
|
||||
Fail-Json -obj $result -message "failed to call Win32_ComputerSystem.JoinDomainOrWorkgroup($workgroup_name): $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
if ($swg_result.ReturnValue -ne 0) {
|
||||
Fail-Json -obj $result -message "failed to set workgroup through WMI, return value: $($swg_result.ReturnValue)"
|
||||
}
|
||||
}
|
||||
|
||||
Function Join-Workgroup {
|
||||
Param(
|
||||
[string] $workgroup_name,
|
||||
[string] $domain_admin_user,
|
||||
[string] $domain_admin_password
|
||||
)
|
||||
|
||||
If(Is-DomainJoined) { # if we're on a domain, unjoin it (which forces us to join a workgroup)
|
||||
$domain_cred = Create-Credential $domain_admin_user $domain_admin_password
|
||||
|
||||
# 2012+ call the Workgroup arg WorkgroupName, but seem to accept
|
||||
try {
|
||||
Remove-Computer -Workgroup $workgroup_name -Credential $domain_cred -Force
|
||||
} catch {
|
||||
Fail-Json -obj $result -message "failed to remove computer from domain: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
# we're already on a workgroup- change it.
|
||||
Else {
|
||||
Set-Workgroup $workgroup_name
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$result = @{
|
||||
changed = $false
|
||||
reboot_required = $false
|
||||
}
|
||||
|
||||
$params = Parse-Args -arguments $args -supports_check_mode $true
|
||||
|
||||
$state = Get-AnsibleParam $params "state" -validateset @("domain","workgroup") -failifempty $result
|
||||
|
||||
$dns_domain_name = Get-AnsibleParam $params "dns_domain_name"
|
||||
$hostname = Get-AnsibleParam $params "hostname"
|
||||
$workgroup_name = Get-AnsibleParam $params "workgroup_name"
|
||||
$domain_admin_user = Get-AnsibleParam $params "domain_admin_user" -failifempty $result
|
||||
$domain_admin_password = Get-AnsibleParam $params "domain_admin_password" -failifempty $result
|
||||
$domain_ou_path = Get-AnsibleParam $params "domain_ou_path"
|
||||
|
||||
$log_path = Get-AnsibleParam $params "log_path"
|
||||
$_ansible_check_mode = Get-AnsibleParam $params "_ansible_check_mode" -default $false
|
||||
|
||||
If ($state -eq "domain") {
|
||||
If(-not $dns_domain_name) {
|
||||
Fail-Json @{} "dns_domain_name is required when state is 'domain'"
|
||||
}
|
||||
}
|
||||
Else { # workgroup
|
||||
If(-not $workgroup_name) {
|
||||
Fail-Json @{} "workgroup_name is required when state is 'workgroup'"
|
||||
}
|
||||
}
|
||||
|
||||
$global:log_path = $log_path
|
||||
|
||||
Try {
|
||||
|
||||
$hostname_match = If($hostname) { Get-HostnameMatch $hostname } Else { $true }
|
||||
|
||||
$result.changed = $result.changed -or (-not $hostname_match)
|
||||
|
||||
Switch($state) {
|
||||
domain {
|
||||
$domain_match = Get-DomainMembershipMatch $dns_domain_name
|
||||
|
||||
$result.changed = $result.changed -or (-not $domain_match)
|
||||
|
||||
If($result.changed -and -not $_ansible_check_mode) {
|
||||
If(-not $domain_match) {
|
||||
If(Is-DomainJoined) {
|
||||
Write-DebugLog "domain doesn't match, and we're already joined to another domain"
|
||||
throw "switching domains is not implemented"
|
||||
}
|
||||
|
||||
$join_args = @{
|
||||
dns_domain_name = $dns_domain_name
|
||||
domain_admin_user = $domain_admin_user
|
||||
domain_admin_password = $domain_admin_password
|
||||
}
|
||||
|
||||
Write-DebugLog "not a domain member, joining..."
|
||||
|
||||
If(-not $hostname_match) {
|
||||
Write-DebugLog "adding hostname change to domain-join args"
|
||||
$join_args.new_hostname = $hostname
|
||||
}
|
||||
If($null -ne $domain_ou_path){ # If OU Path is not empty
|
||||
Write-DebugLog "adding domain_ou_path to domain-join args"
|
||||
$join_args.domain_ou_path = $domain_ou_path
|
||||
}
|
||||
|
||||
Join-Domain @join_args
|
||||
|
||||
# this change requires a reboot
|
||||
$result.reboot_required = $true
|
||||
}
|
||||
ElseIf(-not $hostname_match) { # domain matches but hostname doesn't, just do a rename
|
||||
Write-DebugLog ("domain matches, setting hostname to {0}" -f $hostname)
|
||||
|
||||
$rename_args = @{NewName=$hostname}
|
||||
|
||||
If (Is-DomainJoined) {
|
||||
$domain_cred = Create-Credential $domain_admin_user $domain_admin_password
|
||||
$rename_args.DomainCredential = $domain_cred
|
||||
}
|
||||
|
||||
Rename-Computer @rename_args
|
||||
|
||||
# this change requires a reboot
|
||||
$result.reboot_required = $true
|
||||
} Else {
|
||||
# no change is needed
|
||||
}
|
||||
|
||||
}
|
||||
Else {
|
||||
Write-DebugLog "check mode, exiting early..."
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
workgroup {
|
||||
$workgroup_match = $(Get-Workgroup) -eq $workgroup_name
|
||||
|
||||
$result.changed = $result.changed -or (-not $workgroup_match)
|
||||
|
||||
If(-not $_ansible_check_mode) {
|
||||
If(-not $workgroup_match) {
|
||||
Write-DebugLog ("setting workgroup to {0}" -f $workgroup_name)
|
||||
Join-Workgroup -workgroup_name $workgroup_name -domain_admin_user $domain_admin_user -domain_admin_password $domain_admin_password
|
||||
|
||||
# this change requires a reboot
|
||||
$result.reboot_required = $true
|
||||
}
|
||||
If(-not $hostname_match) {
|
||||
Write-DebugLog ("setting hostname to {0}" -f $hostname)
|
||||
Rename-Computer -NewName $hostname
|
||||
|
||||
# this change requires a reboot
|
||||
$result.reboot_required = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
default { throw "invalid state $state" }
|
||||
}
|
||||
|
||||
Exit-Json $result
|
||||
}
|
||||
Catch {
|
||||
$excep = $_
|
||||
|
||||
Write-DebugLog "Exception: $($excep | out-string)"
|
||||
|
||||
Throw
|
||||
}
|
||||
@ -1,106 +0,0 @@
|
||||
#!/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)
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
module: win_domain_membership
|
||||
short_description: Manage domain/workgroup membership for a Windows host
|
||||
version_added: '2.3'
|
||||
description:
|
||||
- Manages domain membership or workgroup membership for a Windows host. Also supports hostname changes.
|
||||
- This module may require subsequent use of the M(win_reboot) action if changes are made.
|
||||
options:
|
||||
dns_domain_name:
|
||||
description:
|
||||
- When C(state) is C(domain), the DNS name of the domain to which the targeted Windows host should be joined.
|
||||
type: str
|
||||
domain_admin_user:
|
||||
description:
|
||||
- Username of a domain admin for the target domain (required to join or leave the domain).
|
||||
type: str
|
||||
required: yes
|
||||
domain_admin_password:
|
||||
description:
|
||||
- Password for the specified C(domain_admin_user).
|
||||
type: str
|
||||
hostname:
|
||||
description:
|
||||
- The desired hostname for the Windows host.
|
||||
type: str
|
||||
domain_ou_path:
|
||||
description:
|
||||
- The desired OU path for adding the computer object.
|
||||
- This is only used when adding the target host to a domain, if it is already a member then it is ignored.
|
||||
type: str
|
||||
version_added: "2.4"
|
||||
state:
|
||||
description:
|
||||
- Whether the target host should be a member of a domain or workgroup.
|
||||
type: str
|
||||
choices: [ domain, workgroup ]
|
||||
workgroup_name:
|
||||
description:
|
||||
- When C(state) is C(workgroup), the name of the workgroup that the Windows host should be in.
|
||||
type: str
|
||||
seealso:
|
||||
- module: win_domain
|
||||
- module: win_domain_controller
|
||||
- module: win_domain_computer
|
||||
- module: win_domain_group
|
||||
- module: win_domain_user
|
||||
- module: win_group
|
||||
- module: win_group_membership
|
||||
- module: win_user
|
||||
author:
|
||||
- Matt Davis (@nitzmahone)
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
reboot_required:
|
||||
description: True if changes were made that require a reboot.
|
||||
returned: always
|
||||
type: bool
|
||||
sample: true
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
|
||||
# host should be a member of domain ansible.vagrant; module will ensure the hostname is mydomainclient
|
||||
# and will use the passed credentials to join domain if necessary.
|
||||
# Ansible connection should use local credentials if possible.
|
||||
# If a reboot is required, the second task will trigger one and wait until the host is available.
|
||||
- hosts: winclient
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- win_domain_membership:
|
||||
dns_domain_name: ansible.vagrant
|
||||
hostname: mydomainclient
|
||||
domain_admin_user: testguy@ansible.vagrant
|
||||
domain_admin_password: password123!
|
||||
domain_ou_path: "OU=Windows,OU=Servers,DC=ansible,DC=vagrant"
|
||||
state: domain
|
||||
register: domain_state
|
||||
|
||||
- win_reboot:
|
||||
when: domain_state.reboot_required
|
||||
|
||||
|
||||
|
||||
# Host should be in workgroup mywg- module will use the passed credentials to clean-unjoin domain if possible.
|
||||
# Ansible connection should use local credentials if possible.
|
||||
# The domain admin credentials can be sourced from a vault-encrypted variable
|
||||
- hosts: winclient
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- win_domain_membership:
|
||||
workgroup_name: mywg
|
||||
domain_admin_user: '{{ win_domain_admin_user }}'
|
||||
domain_admin_password: '{{ win_domain_admin_password }}'
|
||||
state: workgroup
|
||||
'''
|
||||
@ -1,398 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2015, Trond Hindenes <trond@hindenes.com>, and others
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#AnsibleRequires -CSharpUtil Ansible.Basic
|
||||
#Requires -Version 5
|
||||
|
||||
Function ConvertTo-ArgSpecType {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Converts the DSC parameter type to the arg spec type required for Ansible.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][String]$CimType
|
||||
)
|
||||
|
||||
$arg_type = switch($CimType) {
|
||||
Boolean { "bool" }
|
||||
Char16 { [Func[[Object], [Char]]]{ [System.Char]::Parse($args[0].ToString()) } }
|
||||
DateTime { [Func[[Object], [DateTime]]]{ [System.DateTime]($args[0].ToString()) } }
|
||||
Instance { "dict" }
|
||||
Real32 { "float" }
|
||||
Real64 { [Func[[Object], [Double]]]{ [System.Double]::Parse($args[0].ToString()) } }
|
||||
Reference { "dict" }
|
||||
SInt16 { [Func[[Object], [Int16]]]{ [System.Int16]::Parse($args[0].ToString()) } }
|
||||
SInt32 { "int" }
|
||||
SInt64 { [Func[[Object], [Int64]]]{ [System.Int64]::Parse($args[0].ToString()) } }
|
||||
SInt8 { [Func[[Object], [SByte]]]{ [System.SByte]::Parse($args[0].ToString()) } }
|
||||
String { "str" }
|
||||
UInt16 { [Func[[Object], [UInt16]]]{ [System.UInt16]::Parse($args[0].ToString()) } }
|
||||
UInt32 { [Func[[Object], [UInt32]]]{ [System.UInt32]::Parse($args[0].ToString()) } }
|
||||
UInt64 { [Func[[Object], [UInt64]]]{ [System.UInt64]::Parse($args[0].ToString()) } }
|
||||
UInt8 { [Func[[Object], [Byte]]]{ [System.Byte]::Parse($args[0].ToString()) } }
|
||||
Unknown { "raw" }
|
||||
default { "raw" }
|
||||
}
|
||||
return $arg_type
|
||||
}
|
||||
|
||||
Function Get-DscCimClassProperties {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Get's a list of CimProperties of a CIM Class. It filters out any magic or
|
||||
read only properties that we don't need to know about.
|
||||
#>
|
||||
param([Parameter(Mandatory=$true)][String]$ClassName)
|
||||
|
||||
$resource = Get-CimClass -ClassName $ClassName -Namespace root\Microsoft\Windows\DesiredStateConfiguration
|
||||
|
||||
# Filter out any magic properties that are used internally on an OMI_BaseResource
|
||||
# https://github.com/PowerShell/PowerShell/blob/master/src/System.Management.Automation/DscSupport/CimDSCParser.cs#L1203
|
||||
$magic_properties = @("ResourceId", "SourceInfo", "ModuleName", "ModuleVersion", "ConfigurationName")
|
||||
$properties = $resource.CimClassProperties | Where-Object {
|
||||
|
||||
($resource.CimSuperClassName -ne "OMI_BaseResource" -or $_.Name -notin $magic_properties) -and
|
||||
-not $_.Flags.HasFlag([Microsoft.Management.Infrastructure.CimFlags]::ReadOnly)
|
||||
}
|
||||
|
||||
return ,$properties
|
||||
}
|
||||
|
||||
Function Add-PropertyOption {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Adds the spec for the property type to the existing module specification.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][Hashtable]$Spec,
|
||||
[Parameter(Mandatory=$true)]
|
||||
[Microsoft.Management.Infrastructure.CimPropertyDeclaration]$Property
|
||||
)
|
||||
|
||||
$option = @{
|
||||
required = $false
|
||||
}
|
||||
$property_name = $Property.Name
|
||||
$property_type = $Property.CimType.ToString()
|
||||
|
||||
if ($Property.Flags.HasFlag([Microsoft.Management.Infrastructure.CimFlags]::Key) -or
|
||||
$Property.Flags.HasFlag([Microsoft.Management.Infrastructure.CimFlags]::Required)) {
|
||||
$option.required = $true
|
||||
}
|
||||
|
||||
if ($null -ne $Property.Qualifiers['Values']) {
|
||||
$option.choices = [System.Collections.Generic.List`1[Object]]$Property.Qualifiers['Values'].Value
|
||||
}
|
||||
|
||||
if ($property_name -eq "Name") {
|
||||
# For backwards compatibility we support specifying the Name DSC property as item_name
|
||||
$option.aliases = @("item_name")
|
||||
} elseif ($property_name -ceq "key") {
|
||||
# There seems to be a bug in the CIM property parsing when the property name is 'Key'. The CIM instance will
|
||||
# think the name is 'key' when the MOF actually defines it as 'Key'. We set the proper casing so the module arg
|
||||
# validator won't fire a case sensitive warning
|
||||
$property_name = "Key"
|
||||
}
|
||||
|
||||
if ($Property.ReferenceClassName -eq "MSFT_Credential") {
|
||||
# Special handling for the MSFT_Credential type (PSCredential), we handle this with having 2 options that
|
||||
# have the suffix _username and _password.
|
||||
$option_spec_pass = @{
|
||||
type = "str"
|
||||
required = $option.required
|
||||
no_log = $true
|
||||
}
|
||||
$Spec.options."$($property_name)_password" = $option_spec_pass
|
||||
$Spec.required_together.Add(@("$($property_name)_username", "$($property_name)_password")) > $null
|
||||
|
||||
$property_name = "$($property_name)_username"
|
||||
$option.type = "str"
|
||||
} elseif ($Property.ReferenceClassName -eq "MSFT_KeyValuePair") {
|
||||
$option.type = "dict"
|
||||
} elseif ($property_type.EndsWith("Array")) {
|
||||
$option.type = "list"
|
||||
$option.elements = ConvertTo-ArgSpecType -CimType $property_type.Substring(0, $property_type.Length - 5)
|
||||
} else {
|
||||
$option.type = ConvertTo-ArgSpecType -CimType $property_type
|
||||
}
|
||||
|
||||
if (($option.type -eq "dict" -or ($option.type -eq "list" -and $option.elements -eq "dict")) -and
|
||||
$Property.ReferenceClassName -ne "MSFT_KeyValuePair") {
|
||||
# Get the sub spec if the type is a Instance (CimInstance/dict)
|
||||
$sub_option_spec = Get-OptionSpec -ClassName $Property.ReferenceClassName
|
||||
$option += $sub_option_spec
|
||||
}
|
||||
|
||||
$Spec.options.$property_name = $option
|
||||
}
|
||||
|
||||
Function Get-OptionSpec {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Generates the specifiec used in AnsibleModule for a CIM MOF resource name.
|
||||
|
||||
.NOTES
|
||||
This won't be able to retrieve the default values for an option as that is not defined in the MOF for a resource.
|
||||
Default values are still preserved in the DSC engine if we don't pass in the property at all, we just can't report
|
||||
on what they are automatically.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][String]$ClassName
|
||||
)
|
||||
|
||||
$spec = @{
|
||||
options = @{}
|
||||
required_together = [System.Collections.ArrayList]@()
|
||||
}
|
||||
$properties = Get-DscCimClassProperties -ClassName $ClassName
|
||||
foreach ($property in $properties) {
|
||||
Add-PropertyOption -Spec $spec -Property $property
|
||||
}
|
||||
|
||||
return $spec
|
||||
}
|
||||
|
||||
Function ConvertTo-CimInstance {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Converts a dict to a CimInstance of the specified Class. Also provides a
|
||||
better error message if this fails that contains the option name that failed.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][String]$Name,
|
||||
[Parameter(Mandatory=$true)][String]$ClassName,
|
||||
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Value,
|
||||
[Parameter(Mandatory=$true)][Ansible.Basic.AnsibleModule]$Module,
|
||||
[Switch]$Recurse
|
||||
)
|
||||
|
||||
$properties = @{}
|
||||
foreach ($value_info in $Value.GetEnumerator()) {
|
||||
# Need to remove all null values from existing dict so the conversion works
|
||||
if ($null -eq $value_info.Value) {
|
||||
continue
|
||||
}
|
||||
$properties.($value_info.Key) = $value_info.Value
|
||||
}
|
||||
|
||||
if ($Recurse) {
|
||||
# We want to validate and convert and values to what's required by DSC
|
||||
$properties = ConvertTo-DscProperty -ClassName $ClassName -Params $properties -Module $Module
|
||||
}
|
||||
|
||||
try {
|
||||
return (New-CimInstance -ClassName $ClassName -Property $properties -ClientOnly)
|
||||
} catch {
|
||||
# New-CimInstance raises a poor error message, make sure we mention what option it is for
|
||||
$Module.FailJson("Failed to cast dict value for option '$Name' to a CimInstance: $($_.Exception.Message)", $_)
|
||||
}
|
||||
}
|
||||
|
||||
Function ConvertTo-DscProperty {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Converts the input module parameters that have been validated and casted
|
||||
into the types expected by the DSC engine. This is mostly done to deal with
|
||||
types like PSCredential and Dictionaries.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][String]$ClassName,
|
||||
[Parameter(Mandatory=$true)][System.Collections.IDictionary]$Params,
|
||||
[Parameter(Mandatory=$true)][Ansible.Basic.AnsibleModule]$Module
|
||||
)
|
||||
$properties = Get-DscCimClassProperties -ClassName $ClassName
|
||||
|
||||
$dsc_properties = @{}
|
||||
foreach ($property in $properties) {
|
||||
$property_name = $property.Name
|
||||
$property_type = $property.CimType.ToString()
|
||||
|
||||
if ($property.ReferenceClassName -eq "MSFT_Credential") {
|
||||
$username = $Params."$($property_name)_username"
|
||||
$password = $Params."$($property_name)_password"
|
||||
|
||||
# No user set == No option set in playbook, skip this property
|
||||
if ($null -eq $username) {
|
||||
continue
|
||||
}
|
||||
$sec_password = ConvertTo-SecureString -String $password -AsPlainText -Force
|
||||
$value = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $username, $sec_password
|
||||
} else {
|
||||
$value = $Params.$property_name
|
||||
|
||||
# The actual value wasn't set, skip adding this property
|
||||
if ($null -eq $value) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ($property.ReferenceClassName -eq "MSFT_KeyValuePair") {
|
||||
$key_value_pairs = [System.Collections.Generic.List`1[CimInstance]]@()
|
||||
foreach ($value_info in $value.GetEnumerator()) {
|
||||
$kvp = @{Key = $value_info.Key; Value = $value_info.Value.ToString()}
|
||||
$cim_instance = ConvertTo-CimInstance -Name $property_name -ClassName MSFT_KeyValuePair `
|
||||
-Value $kvp -Module $Module
|
||||
$key_value_pairs.Add($cim_instance) > $null
|
||||
}
|
||||
$value = $key_value_pairs.ToArray()
|
||||
} elseif ($null -ne $property.ReferenceClassName) {
|
||||
# Convert the dict to a CimInstance (or list of CimInstances)
|
||||
$convert_args = @{
|
||||
ClassName = $property.ReferenceClassName
|
||||
Module = $Module
|
||||
Name = $property_name
|
||||
Recurse = $true
|
||||
}
|
||||
if ($property_type.EndsWith("Array")) {
|
||||
$value = [System.Collections.Generic.List`1[CimInstance]]@()
|
||||
foreach ($raw in $Params.$property_name.GetEnumerator()) {
|
||||
$cim_instance = ConvertTo-CimInstance -Value $raw @convert_args
|
||||
$value.Add($cim_instance) > $null
|
||||
}
|
||||
$value = $value.ToArray() # Need to make sure we are dealing with an Array not a List
|
||||
} else {
|
||||
$value = ConvertTo-CimInstance -Value $value @convert_args
|
||||
}
|
||||
}
|
||||
}
|
||||
$dsc_properties.$property_name = $value
|
||||
}
|
||||
|
||||
return $dsc_properties
|
||||
}
|
||||
|
||||
Function Invoke-DscMethod {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Invokes the DSC Resource Method specified in another PS pipeline. This is
|
||||
done so we can retrieve the Verbose stream and return it back to the user
|
||||
for futher debugging.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][Ansible.Basic.AnsibleModule]$Module,
|
||||
[Parameter(Mandatory=$true)][String]$Method,
|
||||
[Parameter(Mandatory=$true)][Hashtable]$Arguments
|
||||
)
|
||||
|
||||
# Invoke the DSC resource in a separate runspace so we can capture the Verbose output
|
||||
$ps = [PowerShell]::Create()
|
||||
$ps.AddCommand("Invoke-DscResource").AddParameter("Method", $Method) > $null
|
||||
$ps.AddParameters($Arguments) > $null
|
||||
|
||||
$result = $ps.Invoke()
|
||||
|
||||
# Pass the warnings through to the AnsibleModule return result
|
||||
foreach ($warning in $ps.Streams.Warning) {
|
||||
$Module.Warn($warning.Message)
|
||||
}
|
||||
|
||||
# If running at a high enough verbosity, add the verbose output to the AnsibleModule return result
|
||||
if ($Module.Verbosity -ge 3) {
|
||||
$verbose_logs = [System.Collections.Generic.List`1[String]]@()
|
||||
foreach ($verbosity in $ps.Streams.Verbose) {
|
||||
$verbose_logs.Add($verbosity.Message) > $null
|
||||
}
|
||||
$Module.Result."verbose_$($Method.ToLower())" = $verbose_logs
|
||||
}
|
||||
|
||||
if ($ps.HadErrors) {
|
||||
# Cannot pass in the ErrorRecord as it's a RemotingErrorRecord and doesn't contain the ScriptStackTrace
|
||||
# or other info that would be useful
|
||||
$Module.FailJson("Failed to invoke DSC $Method method: $($ps.Streams.Error[0].Exception.Message)")
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
# win_dsc is unique in that is builds the arg spec based on DSC Resource input. To get this info
|
||||
# we need to read the resource_name and module_version value which is done outside of Ansible.Basic
|
||||
if ($args.Length -gt 0) {
|
||||
$params = Get-Content -Path $args[0] | ConvertFrom-Json
|
||||
} else {
|
||||
$params = $complex_args
|
||||
}
|
||||
if (-not $params.ContainsKey("resource_name")) {
|
||||
$res = @{
|
||||
msg = "missing required argument: resource_name"
|
||||
failed = $true
|
||||
}
|
||||
Write-Output -InputObject (ConvertTo-Json -Compress -InputObject $res)
|
||||
exit 1
|
||||
}
|
||||
$resource_name = $params.resource_name
|
||||
|
||||
if ($params.ContainsKey("module_version")) {
|
||||
$module_version = $params.module_version
|
||||
} else {
|
||||
$module_version = "latest"
|
||||
}
|
||||
|
||||
$module_versions = (Get-DscResource -Name $resource_name -ErrorAction SilentlyContinue | Sort-Object -Property Version)
|
||||
$resource = $null
|
||||
if ($module_version -eq "latest" -and $null -ne $module_versions) {
|
||||
$resource = $module_versions[-1]
|
||||
} elseif ($module_version -ne "latest") {
|
||||
$resource = $module_versions | Where-Object { $_.Version -eq $module_version }
|
||||
}
|
||||
|
||||
if (-not $resource) {
|
||||
if ($module_version -eq "latest") {
|
||||
$msg = "Resource '$resource_name' not found."
|
||||
} else {
|
||||
$msg = "Resource '$resource_name' with version '$module_version' not found."
|
||||
$msg += " Versions installed: '$($module_versions.Version -join "', '")'."
|
||||
}
|
||||
|
||||
Write-Output -InputObject (ConvertTo-Json -Compress -InputObject @{ failed = $true; msg = $msg })
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Build the base args for the DSC Invocation based on the resource selected
|
||||
$dsc_args = @{
|
||||
Name = $resource.Name
|
||||
}
|
||||
|
||||
# Binary resources are not working very well with that approach - need to guesstimate module name/version
|
||||
$module_version = $null
|
||||
if ($resource.Module) {
|
||||
$dsc_args.ModuleName = @{
|
||||
ModuleName = $resource.Module.Name
|
||||
ModuleVersion = $resource.Module.Version
|
||||
}
|
||||
$module_version = $resource.Module.Version.ToString()
|
||||
} else {
|
||||
$dsc_args.ModuleName = "PSDesiredStateConfiguration"
|
||||
}
|
||||
|
||||
# To ensure the class registered with CIM is the one based on our version, we want to run the Get method so the DSC
|
||||
# engine updates the metadata propery. We don't care about any errors here
|
||||
try {
|
||||
Invoke-DscResource -Method Get -Property @{Fake="Fake"} @dsc_args > $null
|
||||
} catch {}
|
||||
|
||||
# Dynamically build the option spec based on the resource_name specified and create the module object
|
||||
$spec = Get-OptionSpec -ClassName $resource.ResourceType
|
||||
$spec.supports_check_mode = $true
|
||||
$spec.options.module_version = @{ type = "str"; default = "latest" }
|
||||
$spec.options.resource_name = @{ type = "str"; required = $true }
|
||||
|
||||
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||||
$module.Result.reboot_required = $false
|
||||
$module.Result.module_version = $module_version
|
||||
|
||||
# Build the DSC invocation arguments and invoke the resource
|
||||
$dsc_args.Property = ConvertTo-DscProperty -ClassName $resource.ResourceType -Module $module -Params $Module.Params
|
||||
$dsc_args.Verbose = $true
|
||||
|
||||
$test_result = Invoke-DscMethod -Module $module -Method Test -Arguments $dsc_args
|
||||
if ($test_result.InDesiredState -ne $true) {
|
||||
if (-not $module.CheckMode) {
|
||||
$result = Invoke-DscMethod -Module $module -Method Set -Arguments $dsc_args
|
||||
$module.Result.reboot_required = $result.RebootRequired
|
||||
}
|
||||
$module.Result.changed = $true
|
||||
}
|
||||
|
||||
$module.ExitJson()
|
||||
@ -1,183 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2015, Trond Hindenes <trond@hindenes.com>, and others
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_dsc
|
||||
version_added: "2.4"
|
||||
short_description: Invokes a PowerShell DSC configuration
|
||||
description:
|
||||
- Configures a resource using PowerShell DSC.
|
||||
- Requires PowerShell version 5.0 or newer.
|
||||
- Most of the options for this module are dynamic and will vary depending on
|
||||
the DSC Resource specified in I(resource_name).
|
||||
- See :doc:`/user_guide/windows_dsc` for more information on how to use this module.
|
||||
options:
|
||||
resource_name:
|
||||
description:
|
||||
- The name of the DSC Resource to use.
|
||||
- Must be accessible to PowerShell using any of the default paths.
|
||||
type: str
|
||||
required: yes
|
||||
module_version:
|
||||
description:
|
||||
- Can be used to configure the exact version of the DSC resource to be
|
||||
invoked.
|
||||
- Useful if the target node has multiple versions installed of the module
|
||||
containing the DSC resource.
|
||||
- If not specified, the module will follow standard PowerShell convention
|
||||
and use the highest version available.
|
||||
type: str
|
||||
default: latest
|
||||
free_form:
|
||||
description:
|
||||
- The M(win_dsc) module takes in multiple free form options based on the
|
||||
DSC resource being invoked by I(resource_name).
|
||||
- There is no option actually named C(free_form) so see the examples.
|
||||
- This module will try and convert the option to the correct type required
|
||||
by the DSC resource and throw a warning if it fails.
|
||||
- If the type of the DSC resource option is a C(CimInstance) or
|
||||
C(CimInstance[]), this means the value should be a dictionary or list
|
||||
of dictionaries based on the values required by that option.
|
||||
- If the type of the DSC resource option is a C(PSCredential) then there
|
||||
needs to be 2 options set in the Ansible task definition suffixed with
|
||||
C(_username) and C(_password).
|
||||
- If the type of the DSC resource option is an array, then a list should be
|
||||
provided but a comma separated string also work. Use a list where
|
||||
possible as no escaping is required and it works with more complex types
|
||||
list C(CimInstance[]).
|
||||
- If the type of the DSC resource option is a C(DateTime), you should use
|
||||
a string in the form of an ISO 8901 string to ensure the exact date is
|
||||
used.
|
||||
- Since Ansible 2.8, Ansible will now validate the input fields against the
|
||||
DSC resource definition automatically. Older versions will silently
|
||||
ignore invalid fields.
|
||||
type: str
|
||||
required: true
|
||||
notes:
|
||||
- By default there are a few builtin resources that come with PowerShell 5.0,
|
||||
see U(https://docs.microsoft.com/en-us/powershell/scripting/dsc/resources/resources) for
|
||||
more information on these resources.
|
||||
- Custom DSC resources can be installed with M(win_psmodule) using the I(name)
|
||||
option.
|
||||
- The DSC engine run's each task as the SYSTEM account, any resources that need
|
||||
to be accessed with a different account need to have C(PsDscRunAsCredential)
|
||||
set.
|
||||
- To see the valid options for a DSC resource, run the module with C(-vvv) to
|
||||
show the possible module invocation. Default values are not shown in this
|
||||
output but are applied within the DSC engine.
|
||||
author:
|
||||
- Trond Hindenes (@trondhindenes)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Extract zip file
|
||||
win_dsc:
|
||||
resource_name: Archive
|
||||
Ensure: Present
|
||||
Path: C:\Temp\zipfile.zip
|
||||
Destination: C:\Temp\Temp2
|
||||
|
||||
- name: Install a Windows feature with the WindowsFeature resource
|
||||
win_dsc:
|
||||
resource_name: WindowsFeature
|
||||
Name: telnet-client
|
||||
|
||||
- name: Edit HKCU reg key under specific user
|
||||
win_dsc:
|
||||
resource_name: Registry
|
||||
Ensure: Present
|
||||
Key: HKEY_CURRENT_USER\ExampleKey
|
||||
ValueName: TestValue
|
||||
ValueData: TestData
|
||||
PsDscRunAsCredential_username: '{{ansible_user}}'
|
||||
PsDscRunAsCredential_password: '{{ansible_password}}'
|
||||
no_log: true
|
||||
|
||||
- name: Create file with multiple attributes
|
||||
win_dsc:
|
||||
resource_name: File
|
||||
DestinationPath: C:\ansible\dsc
|
||||
Attributes: # can also be a comma separated string, e.g. 'Hidden, System'
|
||||
- Hidden
|
||||
- System
|
||||
Ensure: Present
|
||||
Type: Directory
|
||||
|
||||
- name: Call DSC resource with DateTime option
|
||||
win_dsc:
|
||||
resource_name: DateTimeResource
|
||||
DateTimeOption: '2019-02-22T13:57:31.2311892+00:00'
|
||||
|
||||
# more complex example using custom DSC resource and dict values
|
||||
- name: Setup the xWebAdministration module
|
||||
win_psmodule:
|
||||
name: xWebAdministration
|
||||
state: present
|
||||
|
||||
- name: Create IIS Website with Binding and Authentication options
|
||||
win_dsc:
|
||||
resource_name: xWebsite
|
||||
Ensure: Present
|
||||
Name: DSC Website
|
||||
State: Started
|
||||
PhysicalPath: C:\inetpub\wwwroot
|
||||
BindingInfo: # Example of a CimInstance[] DSC parameter (list of dicts)
|
||||
- Protocol: https
|
||||
Port: 1234
|
||||
CertificateStoreName: MY
|
||||
CertificateThumbprint: C676A89018C4D5902353545343634F35E6B3A659
|
||||
HostName: DSCTest
|
||||
IPAddress: '*'
|
||||
SSLFlags: '1'
|
||||
- Protocol: http
|
||||
Port: 4321
|
||||
IPAddress: '*'
|
||||
AuthenticationInfo: # Example of a CimInstance DSC parameter (dict)
|
||||
Anonymous: no
|
||||
Basic: true
|
||||
Digest: false
|
||||
Windows: yes
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
module_version:
|
||||
description: The version of the dsc resource/module used.
|
||||
returned: always
|
||||
type: str
|
||||
sample: "1.0.1"
|
||||
reboot_required:
|
||||
description: Flag returned from the DSC engine indicating whether or not
|
||||
the machine requires a reboot for the invoked changes to take effect.
|
||||
returned: always
|
||||
type: bool
|
||||
sample: true
|
||||
verbose_test:
|
||||
description: The verbose output as a list from executing the DSC test
|
||||
method.
|
||||
returned: Ansible verbosity is -vvv or greater
|
||||
type: list
|
||||
sample: [
|
||||
"Perform operation 'Invoke CimMethod' with the following parameters, ",
|
||||
"[SERVER]: LCM: [Start Test ] [[File]DirectResourceAccess]",
|
||||
"Operation 'Invoke CimMethod' complete."
|
||||
]
|
||||
verbose_set:
|
||||
description: The verbose output as a list from executing the DSC Set
|
||||
method.
|
||||
returned: Ansible verbosity is -vvv or greater and a change occurred
|
||||
type: list
|
||||
sample: [
|
||||
"Perform operation 'Invoke CimMethod' with the following parameters, ",
|
||||
"[SERVER]: LCM: [Start Set ] [[File]DirectResourceAccess]",
|
||||
"Operation 'Invoke CimMethod' complete."
|
||||
]
|
||||
'''
|
||||
@ -1,61 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2015, Jon Hawkesworth (@jhawkesworth) <figs@unity.demon.co.uk>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#AnsibleRequires -CSharpUtil Ansible.Basic
|
||||
|
||||
$spec = @{
|
||||
options = @{
|
||||
name = @{ type = "str"; required = $true }
|
||||
level = @{ type = "str"; choices = "machine", "process", "user"; required = $true }
|
||||
state = @{ type = "str"; choices = "absent", "present"; default = "present" }
|
||||
value = @{ type = "str" }
|
||||
}
|
||||
required_if = @(,@("state", "present", @("value")))
|
||||
supports_check_mode = $true
|
||||
}
|
||||
|
||||
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||||
|
||||
$name = $module.Params.name
|
||||
$level = $module.Params.level
|
||||
$state = $module.Params.state
|
||||
$value = $module.Params.value
|
||||
|
||||
$before_value = [Environment]::GetEnvironmentVariable($name, $level)
|
||||
$module.Result.before_value = $before_value
|
||||
$module.Result.value = $value
|
||||
|
||||
# When removing environment, set value to $null if set
|
||||
if ($state -eq "absent" -and $value) {
|
||||
$module.Warn("When removing environment variable '$name' it should not have a value '$value' set")
|
||||
$value = $null
|
||||
} elseif ($state -eq "present" -and (-not $value)) {
|
||||
$module.FailJson("When state=present, value must be defined and not an empty string, if you wish to remove the envvar, set state=absent")
|
||||
}
|
||||
|
||||
$module.Diff.before = @{ $level = @{} }
|
||||
if ($before_value) {
|
||||
$module.Diff.before.$level.$name = $before_value
|
||||
}
|
||||
$module.Diff.after = @{ $level = @{} }
|
||||
if ($value) {
|
||||
$module.Diff.after.$level.$name = $value
|
||||
}
|
||||
|
||||
if ($state -eq "present" -and $before_value -ne $value) {
|
||||
if (-not $module.CheckMode) {
|
||||
[Environment]::SetEnvironmentVariable($name, $value, $level)
|
||||
}
|
||||
$module.Result.changed = $true
|
||||
|
||||
} elseif ($state -eq "absent" -and $null -ne $before_value) {
|
||||
if (-not $module.CheckMode) {
|
||||
[Environment]::SetEnvironmentVariable($name, $null, $level)
|
||||
}
|
||||
$module.Result.changed = $true
|
||||
}
|
||||
|
||||
$module.ExitJson()
|
||||
|
||||
@ -1,89 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2015, Jon Hawkesworth (@jhawkesworth) <figs@unity.demon.co.uk>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_environment
|
||||
version_added: '2.0'
|
||||
short_description: Modify environment variables on windows hosts
|
||||
description:
|
||||
- Uses .net Environment to set or remove environment variables and can set at User, Machine or Process level.
|
||||
- User level environment variables will be set, but not available until the user has logged off and on again.
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Set to C(present) to ensure environment variable is set.
|
||||
- Set to C(absent) to ensure it is removed.
|
||||
type: str
|
||||
choices: [ absent, present ]
|
||||
default: present
|
||||
name:
|
||||
description:
|
||||
- The name of the environment variable.
|
||||
type: str
|
||||
required: yes
|
||||
value:
|
||||
description:
|
||||
- The value to store in the environment variable.
|
||||
- Must be set when C(state=present) and cannot be an empty string.
|
||||
- Can be omitted for C(state=absent).
|
||||
type: str
|
||||
level:
|
||||
description:
|
||||
- The level at which to set the environment variable.
|
||||
- Use C(machine) to set for all users.
|
||||
- Use C(user) to set for the current user that ansible is connected as.
|
||||
- Use C(process) to set for the current process. Probably not that useful.
|
||||
type: str
|
||||
required: yes
|
||||
choices: [ machine, process, user ]
|
||||
notes:
|
||||
- This module is best-suited for setting the entire value of an
|
||||
environment variable. For safe element-based management of
|
||||
path-like environment vars, use the M(win_path) module.
|
||||
- This module does not broadcast change events.
|
||||
This means that the minority of windows applications which can have
|
||||
their environment changed without restarting will not be notified and
|
||||
therefore will need restarting to pick up new environment settings.
|
||||
User level environment variables will require the user to log out
|
||||
and in again before they become available.
|
||||
seealso:
|
||||
- module: win_path
|
||||
author:
|
||||
- Jon Hawkesworth (@jhawkesworth)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Set an environment variable for all users
|
||||
win_environment:
|
||||
state: present
|
||||
name: TestVariable
|
||||
value: Test value
|
||||
level: machine
|
||||
|
||||
- name: Remove an environment variable for the current user
|
||||
win_environment:
|
||||
state: absent
|
||||
name: TestVariable
|
||||
level: user
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
before_value:
|
||||
description: the value of the environment key before a change, this is null if it didn't exist
|
||||
returned: always
|
||||
type: str
|
||||
sample: C:\Windows\System32
|
||||
value:
|
||||
description: the value the environment key has been set to, this is null if removed
|
||||
returned: always
|
||||
type: str
|
||||
sample: C:\Program Files\jdk1.8
|
||||
'''
|
||||
@ -1,111 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2014, Paul Durivage <paul.durivage@rackspace.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
|
||||
Import-Module -Name ServerManager
|
||||
|
||||
$result = @{
|
||||
changed = $false
|
||||
}
|
||||
|
||||
$params = Parse-Args $args -supports_check_mode $true
|
||||
$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
|
||||
|
||||
$name = Get-AnsibleParam -obj $params -name "name" -type "list" -failifempty $true
|
||||
$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present","absent"
|
||||
|
||||
$include_sub_features = Get-AnsibleParam -obj $params -name "include_sub_features" -type "bool" -default $false
|
||||
$include_management_tools = Get-AnsibleParam -obj $params -name "include_management_tools" -type "bool" -default $false
|
||||
$source = Get-AnsibleParam -obj $params -name "source" -type "str"
|
||||
|
||||
$install_cmdlet = $false
|
||||
if (Get-Command -Name Install-WindowsFeature -ErrorAction SilentlyContinue) {
|
||||
Set-Alias -Name Install-AnsibleWindowsFeature -Value Install-WindowsFeature
|
||||
Set-Alias -Name Uninstall-AnsibleWindowsFeature -Value Uninstall-WindowsFeature
|
||||
$install_cmdlet = $true
|
||||
} elseif (Get-Command -Name Add-WindowsFeature -ErrorAction SilentlyContinue) {
|
||||
Set-Alias -Name Install-AnsibleWindowsFeature -Value Add-WindowsFeature
|
||||
Set-Alias -Name Uninstall-AnsibleWindowsFeature -Value Remove-WindowsFeature
|
||||
} else {
|
||||
Fail-Json -obj $result -message "This version of Windows does not support the cmdlets Install-WindowsFeature or Add-WindowsFeature"
|
||||
}
|
||||
|
||||
if ($state -eq "present") {
|
||||
$install_args = @{
|
||||
Name = $name
|
||||
IncludeAllSubFeature = $include_sub_features
|
||||
Restart = $false
|
||||
WhatIf = $check_mode
|
||||
ErrorAction = "Stop"
|
||||
}
|
||||
|
||||
if ($install_cmdlet) {
|
||||
$install_args.IncludeManagementTools = $include_management_tools
|
||||
$install_args.Confirm = $false
|
||||
if ($source) {
|
||||
if (-not (Test-Path -Path $source)) {
|
||||
Fail-Json -obj $result -message "Failed to find source path $source for feature install"
|
||||
}
|
||||
$install_args.Source = $source
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$action_results = Install-AnsibleWindowsFeature @install_args
|
||||
} catch {
|
||||
Fail-Json -obj $result -message "Failed to install Windows Feature: $($_.Exception.Message)"
|
||||
}
|
||||
} else {
|
||||
$uninstall_args = @{
|
||||
Name = $name
|
||||
Restart = $false
|
||||
WhatIf = $check_mode
|
||||
ErrorAction = "Stop"
|
||||
}
|
||||
if ($install_cmdlet) {
|
||||
$uninstall_args.IncludeManagementTools = $include_management_tools
|
||||
}
|
||||
|
||||
try {
|
||||
$action_results = Uninstall-AnsibleWindowsFeature @uninstall_args
|
||||
} catch {
|
||||
Fail-Json -obj $result -message "Failed to uninstall Windows Feature: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
# Loop through results and create a hash containing details about
|
||||
# each role/feature that is installed/removed
|
||||
# $action_results.FeatureResult is not empty if anything was changed
|
||||
$feature_results = @()
|
||||
foreach ($action_result in $action_results.FeatureResult) {
|
||||
$message = @()
|
||||
foreach ($msg in $action_result.Message) {
|
||||
$message += @{
|
||||
message_type = $msg.MessageType.ToString()
|
||||
error_code = $msg.ErrorCode
|
||||
text = $msg.Text
|
||||
}
|
||||
}
|
||||
|
||||
$feature_results += @{
|
||||
id = $action_result.Id
|
||||
display_name = $action_result.DisplayName
|
||||
message = $message
|
||||
reboot_required = ConvertTo-Bool -obj $action_result.RestartNeeded
|
||||
skip_reason = $action_result.SkipReason.ToString()
|
||||
success = ConvertTo-Bool -obj $action_result.Success
|
||||
restart_needed = ConvertTo-Bool -obj $action_result.RestartNeeded
|
||||
}
|
||||
$result.changed = $true
|
||||
}
|
||||
$result.feature_result = $feature_results
|
||||
$result.success = ConvertTo-Bool -obj $action_results.Success
|
||||
$result.exitcode = $action_results.ExitCode.ToString()
|
||||
$result.reboot_required = ConvertTo-Bool -obj $action_results.RestartNeeded
|
||||
# controls whether Ansible will fail or not
|
||||
$result.failed = (-not $action_results.Success)
|
||||
|
||||
Exit-Json -obj $result
|
||||
@ -1,149 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2014, Paul Durivage <paul.durivage@rackspace.com>
|
||||
# Copyright: (c) 2014, Trond Hindenes <trond@hindenes.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
# this is a windows documentation stub. actual code lives in the .ps1
|
||||
# file of the same name
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_feature
|
||||
version_added: "1.7"
|
||||
short_description: Installs and uninstalls Windows Features on Windows Server
|
||||
description:
|
||||
- Installs or uninstalls Windows Roles or Features on Windows Server.
|
||||
- This module uses the Add/Remove-WindowsFeature Cmdlets on Windows 2008 R2
|
||||
and Install/Uninstall-WindowsFeature Cmdlets on Windows 2012, which are not available on client os machines.
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Names of roles or features to install as a single feature or a comma-separated list of features.
|
||||
- To list all available features use the PowerShell command C(Get-WindowsFeature).
|
||||
type: list
|
||||
required: yes
|
||||
state:
|
||||
description:
|
||||
- State of the features or roles on the system.
|
||||
type: str
|
||||
choices: [ absent, present ]
|
||||
default: present
|
||||
include_sub_features:
|
||||
description:
|
||||
- Adds all subfeatures of the specified feature.
|
||||
type: bool
|
||||
default: no
|
||||
include_management_tools:
|
||||
description:
|
||||
- Adds the corresponding management tools to the specified feature.
|
||||
- Not supported in Windows 2008 R2 and will be ignored.
|
||||
type: bool
|
||||
default: no
|
||||
source:
|
||||
description:
|
||||
- Specify a source to install the feature from.
|
||||
- Not supported in Windows 2008 R2 and will be ignored.
|
||||
- Can either be C({driveletter}:\sources\sxs) or C(\\{IP}\share\sources\sxs).
|
||||
type: str
|
||||
version_added: "2.1"
|
||||
seealso:
|
||||
- module: win_chocolatey
|
||||
- module: win_package
|
||||
author:
|
||||
- Paul Durivage (@angstwad)
|
||||
- Trond Hindenes (@trondhindenes)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Install IIS (Web-Server only)
|
||||
win_feature:
|
||||
name: Web-Server
|
||||
state: present
|
||||
|
||||
- name: Install IIS (Web-Server and Web-Common-Http)
|
||||
win_feature:
|
||||
name:
|
||||
- Web-Server
|
||||
- Web-Common-Http
|
||||
state: present
|
||||
|
||||
- name: Install NET-Framework-Core from file
|
||||
win_feature:
|
||||
name: NET-Framework-Core
|
||||
source: C:\Temp\iso\sources\sxs
|
||||
state: present
|
||||
|
||||
- name: Install IIS Web-Server with sub features and management tools
|
||||
win_feature:
|
||||
name: Web-Server
|
||||
state: present
|
||||
include_sub_features: yes
|
||||
include_management_tools: yes
|
||||
register: win_feature
|
||||
|
||||
- name: Reboot if installing Web-Server feature requires it
|
||||
win_reboot:
|
||||
when: win_feature.reboot_required
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
exitcode:
|
||||
description: The stringified exit code from the feature installation/removal command.
|
||||
returned: always
|
||||
type: str
|
||||
sample: Success
|
||||
feature_result:
|
||||
description: List of features that were installed or removed.
|
||||
returned: success
|
||||
type: complex
|
||||
sample:
|
||||
contains:
|
||||
display_name:
|
||||
description: Feature display name.
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Telnet Client"
|
||||
id:
|
||||
description: A list of KB article IDs that apply to the update.
|
||||
returned: always
|
||||
type: int
|
||||
sample: 44
|
||||
message:
|
||||
description: Any messages returned from the feature subsystem that occurred during installation or removal of this feature.
|
||||
returned: always
|
||||
type: list
|
||||
elements: str
|
||||
sample: []
|
||||
reboot_required:
|
||||
description: True when the target server requires a reboot as a result of installing or removing this feature.
|
||||
returned: always
|
||||
type: bool
|
||||
sample: true
|
||||
restart_needed:
|
||||
description: DEPRECATED in Ansible 2.4 (refer to C(reboot_required) instead). True when the target server requires a reboot as a
|
||||
result of installing or removing this feature.
|
||||
returned: always
|
||||
type: bool
|
||||
sample: true
|
||||
skip_reason:
|
||||
description: The reason a feature installation or removal was skipped.
|
||||
returned: always
|
||||
type: str
|
||||
sample: NotSkipped
|
||||
success:
|
||||
description: If the feature installation or removal was successful.
|
||||
returned: always
|
||||
type: bool
|
||||
sample: true
|
||||
reboot_required:
|
||||
description: True when the target server requires a reboot to complete updates (no further updates can be installed until after a reboot).
|
||||
returned: success
|
||||
type: bool
|
||||
sample: true
|
||||
'''
|
||||
@ -1,152 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$params = Parse-Args $args -supports_check_mode $true
|
||||
|
||||
$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -default $false
|
||||
$_remote_tmp = Get-AnsibleParam $params "_ansible_remote_tmp" -type "path" -default $env:TMP
|
||||
|
||||
$path = Get-AnsibleParam -obj $params -name "path" -type "path" -failifempty $true -aliases "dest","name"
|
||||
$state = Get-AnsibleParam -obj $params -name "state" -type "str" -validateset "absent","directory","file","touch"
|
||||
|
||||
# used in template/copy when dest is the path to a dir and source is a file
|
||||
$original_basename = Get-AnsibleParam -obj $params -name "_original_basename" -type "str"
|
||||
if ((Test-Path -LiteralPath $path -PathType Container) -and ($null -ne $original_basename)) {
|
||||
$path = Join-Path -Path $path -ChildPath $original_basename
|
||||
}
|
||||
|
||||
$result = @{
|
||||
changed = $false
|
||||
}
|
||||
|
||||
# Used to delete symlinks as powershell cannot delete broken symlinks
|
||||
$symlink_util = @"
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ansible.Command {
|
||||
public class SymLinkHelper {
|
||||
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
|
||||
public static extern bool DeleteFileW(string lpFileName);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
|
||||
public static extern bool RemoveDirectoryW(string lpPathName);
|
||||
|
||||
public static void DeleteDirectory(string path) {
|
||||
if (!RemoveDirectoryW(path))
|
||||
throw new Exception(String.Format("RemoveDirectoryW({0}) failed: {1}", path, new Win32Exception(Marshal.GetLastWin32Error()).Message));
|
||||
}
|
||||
|
||||
public static void DeleteFile(string path) {
|
||||
if (!DeleteFileW(path))
|
||||
throw new Exception(String.Format("DeleteFileW({0}) failed: {1}", path, new Win32Exception(Marshal.GetLastWin32Error()).Message));
|
||||
}
|
||||
}
|
||||
}
|
||||
"@
|
||||
$original_tmp = $env:TMP
|
||||
$env:TMP = $_remote_tmp
|
||||
Add-Type -TypeDefinition $symlink_util
|
||||
$env:TMP = $original_tmp
|
||||
|
||||
# Used to delete directories and files with logic on handling symbolic links
|
||||
function Remove-File($file, $checkmode) {
|
||||
try {
|
||||
if ($file.Attributes -band [System.IO.FileAttributes]::ReparsePoint) {
|
||||
# Bug with powershell, if you try and delete a symbolic link that is pointing
|
||||
# to an invalid path it will fail, using Win32 API to do this instead
|
||||
if ($file.PSIsContainer) {
|
||||
if (-not $checkmode) {
|
||||
[Ansible.Command.SymLinkHelper]::DeleteDirectory($file.FullName)
|
||||
}
|
||||
} else {
|
||||
if (-not $checkmode) {
|
||||
[Ansible.Command.SymlinkHelper]::DeleteFile($file.FullName)
|
||||
}
|
||||
}
|
||||
} elseif ($file.PSIsContainer) {
|
||||
Remove-Directory -directory $file -checkmode $checkmode
|
||||
} else {
|
||||
Remove-Item -LiteralPath $file.FullName -Force -WhatIf:$checkmode
|
||||
}
|
||||
} catch [Exception] {
|
||||
Fail-Json $result "Failed to delete $($file.FullName): $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
function Remove-Directory($directory, $checkmode) {
|
||||
foreach ($file in Get-ChildItem -LiteralPath $directory.FullName) {
|
||||
Remove-File -file $file -checkmode $checkmode
|
||||
}
|
||||
Remove-Item -LiteralPath $directory.FullName -Force -Recurse -WhatIf:$checkmode
|
||||
}
|
||||
|
||||
|
||||
if ($state -eq "touch") {
|
||||
if (Test-Path -LiteralPath $path) {
|
||||
if (-not $check_mode) {
|
||||
(Get-ChildItem -LiteralPath $path).LastWriteTime = Get-Date
|
||||
}
|
||||
$result.changed = $true
|
||||
} else {
|
||||
Write-Output $null | Out-File -LiteralPath $path -Encoding ASCII -WhatIf:$check_mode
|
||||
$result.changed = $true
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $path) {
|
||||
$fileinfo = Get-Item -LiteralPath $path -Force
|
||||
if ($state -eq "absent") {
|
||||
Remove-File -file $fileinfo -checkmode $check_mode
|
||||
$result.changed = $true
|
||||
} else {
|
||||
if ($state -eq "directory" -and -not $fileinfo.PsIsContainer) {
|
||||
Fail-Json $result "path $path is not a directory"
|
||||
}
|
||||
|
||||
if ($state -eq "file" -and $fileinfo.PsIsContainer) {
|
||||
Fail-Json $result "path $path is not a file"
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
# If state is not supplied, test the $path to see if it looks like
|
||||
# a file or a folder and set state to file or folder
|
||||
if ($null -eq $state) {
|
||||
$basename = Split-Path -Path $path -Leaf
|
||||
if ($basename.length -gt 0) {
|
||||
$state = "file"
|
||||
} else {
|
||||
$state = "directory"
|
||||
}
|
||||
}
|
||||
|
||||
if ($state -eq "directory") {
|
||||
try {
|
||||
New-Item -Path $path -ItemType Directory -WhatIf:$check_mode | Out-Null
|
||||
} catch {
|
||||
if ($_.CategoryInfo.Category -eq "ResourceExists") {
|
||||
$fileinfo = Get-Item -LiteralPath $_.CategoryInfo.TargetName
|
||||
if ($state -eq "directory" -and -not $fileinfo.PsIsContainer) {
|
||||
Fail-Json $result "path $path is not a directory"
|
||||
}
|
||||
} else {
|
||||
Fail-Json $result $_.Exception.Message
|
||||
}
|
||||
}
|
||||
$result.changed = $true
|
||||
} elseif ($state -eq "file") {
|
||||
Fail-Json $result "path $path will not be created"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Exit-Json $result
|
||||
@ -1,70 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2015, Jon Hawkesworth (@jhawkesworth) <figs@unity.demon.co.uk>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['stableinterface'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_file
|
||||
version_added: "1.9.2"
|
||||
short_description: Creates, touches or removes files or directories
|
||||
description:
|
||||
- Creates (empty) files, updates file modification stamps of existing files,
|
||||
and can create or remove directories.
|
||||
- Unlike M(file), does not modify ownership, permissions or manipulate links.
|
||||
- For non-Windows targets, use the M(file) module instead.
|
||||
options:
|
||||
path:
|
||||
description:
|
||||
- Path to the file being managed.
|
||||
required: yes
|
||||
type: path
|
||||
aliases: [ dest, name ]
|
||||
state:
|
||||
description:
|
||||
- If C(directory), all immediate subdirectories will be created if they
|
||||
do not exist.
|
||||
- If C(file), the file will NOT be created if it does not exist, see the M(copy)
|
||||
or M(template) module if you want that behavior.
|
||||
- If C(absent), directories will be recursively deleted, and files will be removed.
|
||||
- If C(touch), an empty file will be created if the C(path) does not
|
||||
exist, while an existing file or directory will receive updated file access and
|
||||
modification times (similar to the way C(touch) works from the command line).
|
||||
type: str
|
||||
choices: [ absent, directory, file, touch ]
|
||||
seealso:
|
||||
- module: file
|
||||
- module: win_acl
|
||||
- module: win_acl_inheritance
|
||||
- module: win_owner
|
||||
- module: win_stat
|
||||
author:
|
||||
- Jon Hawkesworth (@jhawkesworth)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Touch a file (creates if not present, updates modification time if present)
|
||||
win_file:
|
||||
path: C:\Temp\foo.conf
|
||||
state: touch
|
||||
|
||||
- name: Remove a file, if present
|
||||
win_file:
|
||||
path: C:\Temp\foo.conf
|
||||
state: absent
|
||||
|
||||
- name: Create directory structure
|
||||
win_file:
|
||||
path: C:\Temp\folder\subfolder
|
||||
state: directory
|
||||
|
||||
- name: Remove directory structure
|
||||
win_file:
|
||||
path: C:\Temp
|
||||
state: absent
|
||||
'''
|
||||
@ -1,416 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2016, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#AnsibleRequires -CSharpUtil Ansible.Basic
|
||||
#Requires -Module Ansible.ModuleUtils.LinkUtil
|
||||
|
||||
$spec = @{
|
||||
options = @{
|
||||
paths = @{ type = "list"; elements = "str"; required = $true }
|
||||
age = @{ type = "str" }
|
||||
age_stamp = @{ type = "str"; default = "mtime"; choices = "mtime", "ctime", "atime" }
|
||||
file_type = @{ type = "str"; default = "file"; choices = "file", "directory" }
|
||||
follow = @{ type = "bool"; default = $false }
|
||||
hidden = @{ type = "bool"; default = $false }
|
||||
patterns = @{ type = "list"; elements = "str"; aliases = "regex", "regexp" }
|
||||
recurse = @{ type = "bool"; default = $false }
|
||||
size = @{ type = "str" }
|
||||
use_regex = @{ type = "bool"; default = $false }
|
||||
get_checksum = @{ type = "bool"; default = $true }
|
||||
checksum_algorithm = @{ type = "str"; default = "sha1"; choices = "md5", "sha1", "sha256", "sha384", "sha512" }
|
||||
}
|
||||
supports_check_mode = $true
|
||||
}
|
||||
|
||||
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||||
|
||||
$paths = $module.Params.paths
|
||||
$age = $module.Params.age
|
||||
$age_stamp = $module.Params.age_stamp
|
||||
$file_type = $module.Params.file_type
|
||||
$follow = $module.Params.follow
|
||||
$hidden = $module.Params.hidden
|
||||
$patterns = $module.Params.patterns
|
||||
$recurse = $module.Params.recurse
|
||||
$size = $module.Params.size
|
||||
$use_regex = $module.Params.use_regex
|
||||
$get_checksum = $module.Params.get_checksum
|
||||
$checksum_algorithm = $module.Params.checksum_algorithm
|
||||
|
||||
$module.Result.examined = 0
|
||||
$module.Result.files = @()
|
||||
$module.Result.matched = 0
|
||||
|
||||
Load-LinkUtils
|
||||
|
||||
Function Assert-Age {
|
||||
Param (
|
||||
[System.IO.FileSystemInfo]$File,
|
||||
[System.Int64]$Age,
|
||||
[System.String]$AgeStamp
|
||||
)
|
||||
|
||||
$actual_age = switch ($AgeStamp) {
|
||||
mtime { $File.LastWriteTime.Ticks }
|
||||
ctime { $File.CreationTime.Ticks }
|
||||
atime { $File.LastAccessTime.Ticks }
|
||||
}
|
||||
|
||||
if ($Age -ge 0) {
|
||||
return $Age -ge $actual_age
|
||||
} else {
|
||||
return ($Age * -1) -le $actual_age
|
||||
}
|
||||
}
|
||||
|
||||
Function Assert-FileType {
|
||||
Param (
|
||||
[System.IO.FileSystemInfo]$File,
|
||||
[System.String]$FileType
|
||||
)
|
||||
|
||||
$is_dir = $File.Attributes.HasFlag([System.IO.FileAttributes]::Directory)
|
||||
return ($FileType -eq 'directory' -and $is_dir) -or ($FileType -eq 'file' -and -not $is_dir)
|
||||
}
|
||||
|
||||
Function Assert-FileHidden {
|
||||
Param (
|
||||
[System.IO.FileSystemInfo]$File,
|
||||
[Switch]$IsHidden
|
||||
)
|
||||
|
||||
$file_is_hidden = $File.Attributes.HasFlag([System.IO.FileAttributes]::Hidden)
|
||||
return $IsHidden.IsPresent -eq $file_is_hidden
|
||||
}
|
||||
|
||||
|
||||
Function Assert-FileNamePattern {
|
||||
Param (
|
||||
[System.IO.FileSystemInfo]$File,
|
||||
[System.String[]]$Patterns,
|
||||
[Switch]$UseRegex
|
||||
)
|
||||
|
||||
$valid_match = $false
|
||||
foreach ($pattern in $Patterns) {
|
||||
if ($UseRegex) {
|
||||
if ($File.Name -match $pattern) {
|
||||
$valid_match = $true
|
||||
break
|
||||
}
|
||||
} else {
|
||||
if ($File.Name -like $pattern) {
|
||||
$valid_match = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return $valid_match
|
||||
}
|
||||
|
||||
Function Assert-FileSize {
|
||||
Param (
|
||||
[System.IO.FileSystemInfo]$File,
|
||||
[System.Int64]$Size
|
||||
)
|
||||
|
||||
if ($Size -ge 0) {
|
||||
return $File.Length -ge $Size
|
||||
} else {
|
||||
return $File.Length -le ($Size * -1)
|
||||
}
|
||||
}
|
||||
|
||||
Function Get-FileChecksum {
|
||||
Param (
|
||||
[System.String]$Path,
|
||||
[System.String]$Algorithm
|
||||
)
|
||||
|
||||
$sp = switch ($algorithm) {
|
||||
'md5' { New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider }
|
||||
'sha1' { New-Object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider }
|
||||
'sha256' { New-Object -TypeName System.Security.Cryptography.SHA256CryptoServiceProvider }
|
||||
'sha384' { New-Object -TypeName System.Security.Cryptography.SHA384CryptoServiceProvider }
|
||||
'sha512' { New-Object -TypeName System.Security.Cryptography.SHA512CryptoServiceProvider }
|
||||
}
|
||||
|
||||
$fp = [System.IO.File]::Open($Path, [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
|
||||
try {
|
||||
$hash = [System.BitConverter]::ToString($sp.ComputeHash($fp)).Replace("-", "").ToLower()
|
||||
} finally {
|
||||
$fp.Dispose()
|
||||
}
|
||||
|
||||
return $hash
|
||||
}
|
||||
|
||||
Function Search-Path {
|
||||
[CmdletBinding()]
|
||||
Param (
|
||||
[Parameter(Mandatory=$true)]
|
||||
[System.String]
|
||||
$Path,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[AllowEmptyCollection()]
|
||||
[System.Collections.Generic.HashSet`1[System.String]]
|
||||
$CheckedPaths,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[Object]
|
||||
$Module,
|
||||
|
||||
[System.Int64]
|
||||
$Age,
|
||||
|
||||
[System.String]
|
||||
$AgeStamp,
|
||||
|
||||
[System.String]
|
||||
$FileType,
|
||||
|
||||
[Switch]
|
||||
$Follow,
|
||||
|
||||
[Switch]
|
||||
$GetChecksum,
|
||||
|
||||
[Switch]
|
||||
$IsHidden,
|
||||
|
||||
[System.String[]]
|
||||
$Patterns,
|
||||
|
||||
[Switch]
|
||||
$Recurse,
|
||||
|
||||
[System.Int64]
|
||||
$Size,
|
||||
|
||||
[Switch]
|
||||
$UseRegex
|
||||
)
|
||||
|
||||
$dir_obj = New-Object -TypeName System.IO.DirectoryInfo -ArgumentList $Path
|
||||
if ([Int32]$dir_obj.Attributes -eq -1) {
|
||||
$Module.Warn("Argument path '$Path' does not exist, skipping")
|
||||
return
|
||||
} elseif (-not $dir_obj.Attributes.HasFlag([System.IO.FileAttributes]::Directory)) {
|
||||
$Module.Warn("Argument path '$Path' is a file not a directory, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
$dir_files = @()
|
||||
try {
|
||||
$dir_files = $dir_obj.EnumerateFileSystemInfos("*", [System.IO.SearchOption]::TopDirectoryOnly)
|
||||
} catch [System.IO.DirectoryNotFoundException] { # Broken ReparsePoint/Symlink, cannot enumerate
|
||||
} catch [System.UnauthorizedAccessException] {} # No ListDirectory permissions, Get-ChildItem ignored this
|
||||
|
||||
foreach ($dir_child in $dir_files) {
|
||||
if ($dir_child.Attributes.HasFlag([System.IO.FileAttributes]::Directory) -and $Recurse) {
|
||||
if ($Follow -or -not $dir_child.Attributes.HasFlag([System.IO.FileAttributes]::ReparsePoint)) {
|
||||
$PSBoundParameters.Remove('Path') > $null
|
||||
Search-Path -Path $dir_child.FullName @PSBoundParameters
|
||||
}
|
||||
}
|
||||
|
||||
# Check to see if we've already encountered this path and skip if we have.
|
||||
if (-not $CheckedPaths.Add($dir_child.FullName.ToLowerInvariant())) {
|
||||
continue
|
||||
}
|
||||
|
||||
$Module.Result.examined++
|
||||
|
||||
if ($PSBoundParameters.ContainsKey('Age')) {
|
||||
$age_match = Assert-Age -File $dir_child -Age $Age -AgeStamp $AgeStamp
|
||||
} else {
|
||||
$age_match = $true
|
||||
}
|
||||
|
||||
$file_type_match = Assert-FileType -File $dir_child -FileType $FileType
|
||||
$hidden_match = Assert-FileHidden -File $dir_child -IsHidden:$IsHidden
|
||||
|
||||
if ($PSBoundParameters.ContainsKey('Patterns')) {
|
||||
$pattern_match = Assert-FileNamePattern -File $dir_child -Patterns $Patterns -UseRegex:$UseRegex.IsPresent
|
||||
} else {
|
||||
$pattern_match = $true
|
||||
}
|
||||
|
||||
if ($PSBoundParameters.ContainsKey('Size')) {
|
||||
$size_match = Assert-FileSize -File $dir_child -Size $Size
|
||||
} else {
|
||||
$size_match = $true
|
||||
}
|
||||
|
||||
if (-not ($age_match -and $file_type_match -and $hidden_match -and $pattern_match -and $size_match)) {
|
||||
continue
|
||||
}
|
||||
|
||||
# It passed all our filters so add it
|
||||
$module.Result.matched++
|
||||
|
||||
# TODO: Make this generic so it can be shared with win_find and win_stat.
|
||||
$epoch = New-Object -Type System.DateTime -ArgumentList 1970, 1, 1, 0, 0, 0, 0
|
||||
$file_info = @{
|
||||
attributes = $dir_child.Attributes.ToString()
|
||||
checksum = $null
|
||||
creationtime = (New-TimeSpan -Start $epoch -End $dir_child.CreationTime).TotalSeconds
|
||||
exists = $true
|
||||
extension = $null
|
||||
filename = $dir_child.Name
|
||||
isarchive = $dir_child.Attributes.HasFlag([System.IO.FileAttributes]::Archive)
|
||||
isdir = $dir_child.Attributes.HasFlag([System.IO.FileAttributes]::Directory)
|
||||
ishidden = $dir_child.Attributes.HasFlag([System.IO.FileAttributes]::Hidden)
|
||||
isreadonly = $dir_child.Attributes.HasFlag([System.IO.FileAttributes]::ReadOnly)
|
||||
isreg = $false
|
||||
isshared = $false
|
||||
lastaccesstime = (New-TimeSpan -Start $epoch -End $dir_child.LastAccessTime).TotalSeconds
|
||||
lastwritetime = (New-TimeSpan -Start $epoch -End $dir_child.LastWriteTime).TotalSeconds
|
||||
owner = $null
|
||||
path = $dir_child.FullName
|
||||
sharename = $null
|
||||
size = $null
|
||||
}
|
||||
|
||||
try {
|
||||
$file_info.owner = $dir_child.GetAccessControl().Owner
|
||||
} catch {} # May not have rights to get the Owner, historical behaviour is to ignore.
|
||||
|
||||
if ($dir_child.Attributes.HasFlag([System.IO.FileAttributes]::Directory)) {
|
||||
$share_info = Get-CimInstance -ClassName Win32_Share -Filter "Path='$($dir_child.FullName -replace '\\', '\\')'"
|
||||
if ($null -ne $share_info) {
|
||||
$file_info.isshared = $true
|
||||
$file_info.sharename = $share_info.Name
|
||||
}
|
||||
} else {
|
||||
$file_info.extension = $dir_child.Extension
|
||||
$file_info.isreg = $true
|
||||
$file_info.size = $dir_child.Length
|
||||
|
||||
if ($GetChecksum) {
|
||||
try {
|
||||
$file_info.checksum = Get-FileChecksum -Path $dir_child.FullName -Algorithm $checksum_algorithm
|
||||
} catch {} # Just keep the checksum as $null in the case of a failure.
|
||||
}
|
||||
}
|
||||
|
||||
# Append the link information if the path is a link
|
||||
$link_info = @{
|
||||
isjunction = $false
|
||||
islnk = $false
|
||||
nlink = 1
|
||||
lnk_source = $null
|
||||
lnk_target = $null
|
||||
hlnk_targets = @()
|
||||
}
|
||||
$link_stat = Get-Link -link_path $dir_child.FullName
|
||||
if ($null -ne $link_stat) {
|
||||
switch ($link_stat.Type) {
|
||||
"SymbolicLink" {
|
||||
$link_info.islnk = $true
|
||||
$link_info.isreg = $false
|
||||
$link_info.lnk_source = $link_stat.AbsolutePath
|
||||
$link_info.lnk_target = $link_stat.TargetPath
|
||||
break
|
||||
}
|
||||
"JunctionPoint" {
|
||||
$link_info.isjunction = $true
|
||||
$link_info.isreg = $false
|
||||
$link_info.lnk_source = $link_stat.AbsolutePath
|
||||
$link_info.lnk_target = $link_stat.TargetPath
|
||||
break
|
||||
}
|
||||
"HardLink" {
|
||||
$link_info.nlink = $link_stat.HardTargets.Count
|
||||
|
||||
# remove current path from the targets
|
||||
$hlnk_targets = $link_info.HardTargets | Where-Object { $_ -ne $dir_child.FullName }
|
||||
$link_info.hlnk_targets = @($hlnk_targets)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($kv in $link_info.GetEnumerator()) {
|
||||
$file_info.$($kv.Key) = $kv.Value
|
||||
}
|
||||
|
||||
# Output the file_info object
|
||||
$file_info
|
||||
}
|
||||
}
|
||||
|
||||
$search_params = @{
|
||||
CheckedPaths = [System.Collections.Generic.HashSet`1[System.String]]@()
|
||||
GetChecksum = $get_checksum
|
||||
Module = $module
|
||||
FileType = $file_type
|
||||
Follow = $follow
|
||||
IsHidden = $hidden
|
||||
Recurse = $recurse
|
||||
}
|
||||
|
||||
if ($null -ne $age) {
|
||||
$seconds_per_unit = @{'s'=1; 'm'=60; 'h'=3600; 'd'=86400; 'w'=604800}
|
||||
$seconds_pattern = '^(-?\d+)(s|m|h|d|w)?$'
|
||||
$match = $age -match $seconds_pattern
|
||||
if ($Match) {
|
||||
$specified_seconds = [Int64]$Matches[1]
|
||||
if ($null -eq $Matches[2]) {
|
||||
$chosen_unit = 's'
|
||||
} else {
|
||||
$chosen_unit = $Matches[2]
|
||||
}
|
||||
|
||||
$total_seconds = $specified_seconds * ($seconds_per_unit.$chosen_unit)
|
||||
|
||||
if ($total_seconds -ge 0) {
|
||||
$search_params.Age = (Get-Date).AddSeconds($total_seconds * -1).Ticks
|
||||
} else {
|
||||
# Make sure we add the positive value of seconds to current time then make it negative for later comparisons.
|
||||
$age = (Get-Date).AddSeconds($total_seconds).Ticks
|
||||
$search_params.Age = $age * -1
|
||||
}
|
||||
$search_params.AgeStamp = $age_stamp
|
||||
} else {
|
||||
$module.FailJson("Invalid age pattern specified")
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -ne $patterns) {
|
||||
$search_params.Patterns = $patterns
|
||||
$search_params.UseRegex = $use_regex
|
||||
}
|
||||
|
||||
if ($null -ne $size) {
|
||||
$bytes_per_unit = @{'b'=1; 'k'=1KB; 'm'=1MB; 'g'=1GB;'t'=1TB}
|
||||
$size_pattern = '^(-?\d+)(b|k|m|g|t)?$'
|
||||
$match = $size -match $size_pattern
|
||||
if ($Match) {
|
||||
$specified_size = [Int64]$Matches[1]
|
||||
if ($null -eq $Matches[2]) {
|
||||
$chosen_byte = 'b'
|
||||
} else {
|
||||
$chosen_byte = $Matches[2]
|
||||
}
|
||||
|
||||
$search_params.Size = $specified_size * ($bytes_per_unit.$chosen_byte)
|
||||
} else {
|
||||
$module.FailJson("Invalid size pattern specified")
|
||||
}
|
||||
}
|
||||
|
||||
$matched_files = foreach ($path in $paths) {
|
||||
# Ensure we pass in an absolute path. We use the ExecutionContext as this is based on the PSProvider path not the
|
||||
# process location which can be different.
|
||||
$abs_path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($path)
|
||||
Search-Path -Path $abs_path @search_params
|
||||
}
|
||||
|
||||
# Make sure we sort the files in alphabetical order.
|
||||
$module.Result.files = @() + ($matched_files | Sort-Object -Property {$_.path})
|
||||
|
||||
$module.ExitJson()
|
||||
|
||||
@ -1,345 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
# this is a windows documentation stub. actual code lives in the .ps1
|
||||
# file of the same name
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_find
|
||||
version_added: "2.3"
|
||||
short_description: Return a list of files based on specific criteria
|
||||
description:
|
||||
- Return a list of files based on specified criteria.
|
||||
- Multiple criteria are AND'd together.
|
||||
- For non-Windows targets, use the M(find) module instead.
|
||||
options:
|
||||
age:
|
||||
description:
|
||||
- Select files or folders whose age is equal to or greater than
|
||||
the specified time.
|
||||
- Use a negative age to find files equal to or less than
|
||||
the specified time.
|
||||
- You can choose seconds, minutes, hours, days or weeks
|
||||
by specifying the first letter of an of
|
||||
those words (e.g., "2s", "10d", 1w").
|
||||
type: str
|
||||
age_stamp:
|
||||
description:
|
||||
- Choose the file property against which we compare C(age).
|
||||
- The default attribute we compare with is the last modification time.
|
||||
type: str
|
||||
choices: [ atime, ctime, mtime ]
|
||||
default: mtime
|
||||
checksum_algorithm:
|
||||
description:
|
||||
- Algorithm to determine the checksum of a file.
|
||||
- Will throw an error if the host is unable to use specified algorithm.
|
||||
type: str
|
||||
choices: [ md5, sha1, sha256, sha384, sha512 ]
|
||||
default: sha1
|
||||
file_type:
|
||||
description: Type of file to search for.
|
||||
type: str
|
||||
choices: [ directory, file ]
|
||||
default: file
|
||||
follow:
|
||||
description:
|
||||
- Set this to C(yes) to follow symlinks in the path.
|
||||
- This needs to be used in conjunction with C(recurse).
|
||||
type: bool
|
||||
default: no
|
||||
get_checksum:
|
||||
description:
|
||||
- Whether to return a checksum of the file in the return info (default sha1),
|
||||
use C(checksum_algorithm) to change from the default.
|
||||
type: bool
|
||||
default: yes
|
||||
hidden:
|
||||
description: Set this to include hidden files or folders.
|
||||
type: bool
|
||||
default: no
|
||||
paths:
|
||||
description:
|
||||
- List of paths of directories to search for files or folders in.
|
||||
- This can be supplied as a single path or a list of paths.
|
||||
type: list
|
||||
required: yes
|
||||
patterns:
|
||||
description:
|
||||
- One or more (powershell or regex) patterns to compare filenames with.
|
||||
- The type of pattern matching is controlled by C(use_regex) option.
|
||||
- The patterns restrict the list of files or folders to be returned based on the filenames.
|
||||
- For a file to be matched it only has to match with one pattern in a list provided.
|
||||
type: list
|
||||
aliases: [ "regex", "regexp" ]
|
||||
recurse:
|
||||
description:
|
||||
- Will recursively descend into the directory looking for files or folders.
|
||||
type: bool
|
||||
default: no
|
||||
size:
|
||||
description:
|
||||
- Select files or folders whose size is equal to or greater than the specified size.
|
||||
- Use a negative value to find files equal to or less than the specified size.
|
||||
- You can specify the size with a suffix of the byte type i.e. kilo = k, mega = m...
|
||||
- Size is not evaluated for symbolic links.
|
||||
type: str
|
||||
use_regex:
|
||||
description:
|
||||
- Will set patterns to run as a regex check if set to C(yes).
|
||||
type: bool
|
||||
default: no
|
||||
author:
|
||||
- Jordan Borean (@jborean93)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Find files in path
|
||||
win_find:
|
||||
paths: D:\Temp
|
||||
|
||||
- name: Find hidden files in path
|
||||
win_find:
|
||||
paths: D:\Temp
|
||||
hidden: yes
|
||||
|
||||
- name: Find files in multiple paths
|
||||
win_find:
|
||||
paths:
|
||||
- C:\Temp
|
||||
- D:\Temp
|
||||
|
||||
- name: Find files in directory while searching recursively
|
||||
win_find:
|
||||
paths: D:\Temp
|
||||
recurse: yes
|
||||
|
||||
- name: Find files in directory while following symlinks
|
||||
win_find:
|
||||
paths: D:\Temp
|
||||
recurse: yes
|
||||
follow: yes
|
||||
|
||||
- name: Find files with .log and .out extension using powershell wildcards
|
||||
win_find:
|
||||
paths: D:\Temp
|
||||
patterns: [ '*.log', '*.out' ]
|
||||
|
||||
- name: Find files in path based on regex pattern
|
||||
win_find:
|
||||
paths: D:\Temp
|
||||
patterns: out_\d{8}-\d{6}.log
|
||||
|
||||
- name: Find files older than 1 day
|
||||
win_find:
|
||||
paths: D:\Temp
|
||||
age: 86400
|
||||
|
||||
- name: Find files older than 1 day based on create time
|
||||
win_find:
|
||||
paths: D:\Temp
|
||||
age: 86400
|
||||
age_stamp: ctime
|
||||
|
||||
- name: Find files older than 1 day with unit syntax
|
||||
win_find:
|
||||
paths: D:\Temp
|
||||
age: 1d
|
||||
|
||||
- name: Find files newer than 1 hour
|
||||
win_find:
|
||||
paths: D:\Temp
|
||||
age: -3600
|
||||
|
||||
- name: Find files newer than 1 hour with unit syntax
|
||||
win_find:
|
||||
paths: D:\Temp
|
||||
age: -1h
|
||||
|
||||
- name: Find files larger than 1MB
|
||||
win_find:
|
||||
paths: D:\Temp
|
||||
size: 1048576
|
||||
|
||||
- name: Find files larger than 1GB with unit syntax
|
||||
win_find:
|
||||
paths: D:\Temp
|
||||
size: 1g
|
||||
|
||||
- name: Find files smaller than 1MB
|
||||
win_find:
|
||||
paths: D:\Temp
|
||||
size: -1048576
|
||||
|
||||
- name: Find files smaller than 1GB with unit syntax
|
||||
win_find:
|
||||
paths: D:\Temp
|
||||
size: -1g
|
||||
|
||||
- name: Find folders/symlinks in multiple paths
|
||||
win_find:
|
||||
paths:
|
||||
- C:\Temp
|
||||
- D:\Temp
|
||||
file_type: directory
|
||||
|
||||
- name: Find files and return SHA256 checksum of files found
|
||||
win_find:
|
||||
paths: C:\Temp
|
||||
get_checksum: yes
|
||||
checksum_algorithm: sha256
|
||||
|
||||
- name: Find files and do not return the checksum
|
||||
win_find:
|
||||
paths: C:\Temp
|
||||
get_checksum: no
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
examined:
|
||||
description: The number of files/folders that was checked.
|
||||
returned: always
|
||||
type: int
|
||||
sample: 10
|
||||
matched:
|
||||
description: The number of files/folders that match the criteria.
|
||||
returned: always
|
||||
type: int
|
||||
sample: 2
|
||||
files:
|
||||
description: Information on the files/folders that match the criteria returned as a list of dictionary elements
|
||||
for each file matched. The entries are sorted by the path value alphabetically.
|
||||
returned: success
|
||||
type: complex
|
||||
contains:
|
||||
attributes:
|
||||
description: attributes of the file at path in raw form.
|
||||
returned: success, path exists
|
||||
type: str
|
||||
sample: "Archive, Hidden"
|
||||
checksum:
|
||||
description: The checksum of a file based on checksum_algorithm specified.
|
||||
returned: success, path exists, path is a file, get_checksum == True
|
||||
type: str
|
||||
sample: 09cb79e8fc7453c84a07f644e441fd81623b7f98
|
||||
creationtime:
|
||||
description: The create time of the file represented in seconds since epoch.
|
||||
returned: success, path exists
|
||||
type: float
|
||||
sample: 1477984205.15
|
||||
exists:
|
||||
description: Whether the file exists, will always be true for M(win_find).
|
||||
returned: success, path exists
|
||||
type: bool
|
||||
sample: true
|
||||
extension:
|
||||
description: The extension of the file at path.
|
||||
returned: success, path exists, path is a file
|
||||
type: str
|
||||
sample: ".ps1"
|
||||
filename:
|
||||
description: The name of the file.
|
||||
returned: success, path exists
|
||||
type: str
|
||||
sample: temp
|
||||
hlnk_targets:
|
||||
description: List of other files pointing to the same file (hard links), excludes the current file.
|
||||
returned: success, path exists
|
||||
type: list
|
||||
sample:
|
||||
- C:\temp\file.txt
|
||||
- C:\Windows\update.log
|
||||
isarchive:
|
||||
description: If the path is ready for archiving or not.
|
||||
returned: success, path exists
|
||||
type: bool
|
||||
sample: true
|
||||
isdir:
|
||||
description: If the path is a directory or not.
|
||||
returned: success, path exists
|
||||
type: bool
|
||||
sample: true
|
||||
ishidden:
|
||||
description: If the path is hidden or not.
|
||||
returned: success, path exists
|
||||
type: bool
|
||||
sample: true
|
||||
isjunction:
|
||||
description: If the path is a junction point.
|
||||
returned: success, path exists
|
||||
type: bool
|
||||
sample: true
|
||||
islnk:
|
||||
description: If the path is a symbolic link.
|
||||
returned: success, path exists
|
||||
type: bool
|
||||
sample: true
|
||||
isreadonly:
|
||||
description: If the path is read only or not.
|
||||
returned: success, path exists
|
||||
type: bool
|
||||
sample: true
|
||||
isreg:
|
||||
description: If the path is a regular file or not.
|
||||
returned: success, path exists
|
||||
type: bool
|
||||
sample: true
|
||||
isshared:
|
||||
description: If the path is shared or not.
|
||||
returned: success, path exists
|
||||
type: bool
|
||||
sample: true
|
||||
lastaccesstime:
|
||||
description: The last access time of the file represented in seconds since epoch.
|
||||
returned: success, path exists
|
||||
type: float
|
||||
sample: 1477984205.15
|
||||
lastwritetime:
|
||||
description: The last modification time of the file represented in seconds since epoch.
|
||||
returned: success, path exists
|
||||
type: float
|
||||
sample: 1477984205.15
|
||||
lnk_source:
|
||||
description: The target of the symlink normalized for the remote filesystem.
|
||||
returned: success, path exists, path is a symbolic link or junction point
|
||||
type: str
|
||||
sample: C:\temp
|
||||
lnk_target:
|
||||
description: The target of the symlink. Note that relative paths remain relative, will return null if not a link.
|
||||
returned: success, path exists, path is a symbolic link or junction point
|
||||
type: str
|
||||
sample: temp
|
||||
nlink:
|
||||
description: Number of links to the file (hard links)
|
||||
returned: success, path exists
|
||||
type: int
|
||||
sample: 1
|
||||
owner:
|
||||
description: The owner of the file.
|
||||
returned: success, path exists
|
||||
type: str
|
||||
sample: BUILTIN\Administrators
|
||||
path:
|
||||
description: The full absolute path to the file.
|
||||
returned: success, path exists
|
||||
type: str
|
||||
sample: BUILTIN\Administrators
|
||||
sharename:
|
||||
description: The name of share if folder is shared.
|
||||
returned: success, path exists, path is a directory and isshared == True
|
||||
type: str
|
||||
sample: file-share
|
||||
size:
|
||||
description: The size in bytes of the file.
|
||||
returned: success, path exists, path is a file
|
||||
type: int
|
||||
sample: 1024
|
||||
'''
|
||||
@ -1,277 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2015, Paul Durivage <paul.durivage@rackspace.com>
|
||||
# Copyright: (c) 2015, Tal Auslander <tal@cloudshare.com>
|
||||
# Copyright: (c) 2017, Dag Wieers <dag@wieers.com>
|
||||
# Copyright: (c) 2019, Viktor Utkin <viktor_utkin@epam.com>
|
||||
# Copyright: (c) 2019, Uladzimir Klybik <uladzimir_klybik@epam.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#AnsibleRequires -CSharpUtil Ansible.Basic
|
||||
#Requires -Module Ansible.ModuleUtils.FileUtil
|
||||
#Requires -Module Ansible.ModuleUtils.WebRequest
|
||||
|
||||
$spec = @{
|
||||
options = @{
|
||||
url = @{ type="str"; required=$true }
|
||||
dest = @{ type='path'; required=$true }
|
||||
force = @{ type='bool'; default=$true }
|
||||
checksum = @{ type='str' }
|
||||
checksum_algorithm = @{ type='str'; default='sha1'; choices = @("md5", "sha1", "sha256", "sha384", "sha512") }
|
||||
checksum_url = @{ type='str' }
|
||||
|
||||
# Defined for the alias backwards compatibility, remove once aliases are removed
|
||||
url_username = @{
|
||||
aliases = @("user", "username")
|
||||
deprecated_aliases = @(
|
||||
@{ name = "user"; version = "2.14" },
|
||||
@{ name = "username"; version = "2.14" }
|
||||
)
|
||||
}
|
||||
url_password = @{
|
||||
aliases = @("password")
|
||||
deprecated_aliases = @(
|
||||
@{ name = "password"; version = "2.14" }
|
||||
)
|
||||
}
|
||||
}
|
||||
mutually_exclusive = @(
|
||||
,@('checksum', 'checksum_url')
|
||||
)
|
||||
supports_check_mode = $true
|
||||
}
|
||||
$spec = Merge-WebRequestSpec -ModuleSpec $spec
|
||||
|
||||
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||||
|
||||
$url = $module.Params.url
|
||||
$dest = $module.Params.dest
|
||||
$force = $module.Params.force
|
||||
$checksum = $module.Params.checksum
|
||||
$checksum_algorithm = $module.Params.checksum_algorithm
|
||||
$checksum_url = $module.Params.checksum_url
|
||||
|
||||
$module.Result.elapsed = 0
|
||||
$module.Result.url = $url
|
||||
|
||||
Function Get-ChecksumFromUri {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][Ansible.Basic.AnsibleModule]$Module,
|
||||
[Parameter(Mandatory=$true)][Uri]$Uri,
|
||||
[Uri]$SourceUri
|
||||
)
|
||||
|
||||
$script = {
|
||||
param($Response, $Stream)
|
||||
|
||||
$read_stream = New-Object -TypeName System.IO.StreamReader -ArgumentList $Stream
|
||||
$web_checksum = $read_stream.ReadToEnd()
|
||||
$basename = (Split-Path -Path $SourceUri.LocalPath -Leaf)
|
||||
$basename = [regex]::Escape($basename)
|
||||
$web_checksum_str = $web_checksum -split '\r?\n' | Select-String -Pattern $("\s+\.?\/?\\?" + $basename + "\s*$")
|
||||
if (-not $web_checksum_str) {
|
||||
$Module.FailJson("Checksum record not found for file name '$basename' in file from url: '$Uri'")
|
||||
}
|
||||
|
||||
$web_checksum_str_splitted = $web_checksum_str[0].ToString().split(" ", 2)
|
||||
$hash_from_file = $web_checksum_str_splitted[0].Trim()
|
||||
# Remove any non-alphanumeric characters
|
||||
$hash_from_file = $hash_from_file -replace '\W+', ''
|
||||
|
||||
Write-Output -InputObject $hash_from_file
|
||||
}
|
||||
$web_request = Get-AnsibleWebRequest -Uri $Uri -Module $Module
|
||||
|
||||
try {
|
||||
Invoke-WithWebRequest -Module $Module -Request $web_request -Script $script
|
||||
} catch {
|
||||
$Module.FailJson("Error when getting the remote checksum from '$Uri'. $($_.Exception.Message)", $_)
|
||||
}
|
||||
}
|
||||
|
||||
Function Compare-ModifiedFile {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Compares the remote URI resource against the local Dest resource. Will
|
||||
return true if the LastWriteTime/LastModificationDate of the remote is
|
||||
newer than the local resource date.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][Ansible.Basic.AnsibleModule]$Module,
|
||||
[Parameter(Mandatory=$true)][Uri]$Uri,
|
||||
[Parameter(Mandatory=$true)][String]$Dest
|
||||
)
|
||||
|
||||
$dest_last_mod = (Get-AnsibleItem -Path $Dest).LastWriteTimeUtc
|
||||
|
||||
# If the URI is a file we don't need to go through the whole WebRequest
|
||||
if ($Uri.IsFile) {
|
||||
$src_last_mod = (Get-AnsibleItem -Path $Uri.AbsolutePath).LastWriteTimeUtc
|
||||
} else {
|
||||
$web_request = Get-AnsibleWebRequest -Uri $Uri -Module $Module
|
||||
$web_request.Method = switch ($web_request.GetType().Name) {
|
||||
FtpWebRequest { [System.Net.WebRequestMethods+Ftp]::GetDateTimestamp }
|
||||
HttpWebRequest { [System.Net.WebRequestMethods+Http]::Head }
|
||||
}
|
||||
$script = { param($Response, $Stream); $Response.LastModified }
|
||||
|
||||
try {
|
||||
$src_last_mod = Invoke-WithWebRequest -Module $Module -Request $web_request -Script $script
|
||||
} catch {
|
||||
$Module.FailJson("Error when requesting 'Last-Modified' date from '$Uri'. $($_.Exception.Message)", $_)
|
||||
}
|
||||
}
|
||||
|
||||
# Return $true if the Uri LastModification date is newer than the Dest LastModification date
|
||||
((Get-Date -Date $src_last_mod).ToUniversalTime() -gt $dest_last_mod)
|
||||
}
|
||||
|
||||
Function Get-Checksum {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][String]$Path,
|
||||
[String]$Algorithm = "sha1"
|
||||
)
|
||||
|
||||
switch ($Algorithm) {
|
||||
'md5' { $sp = New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider }
|
||||
'sha1' { $sp = New-Object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider }
|
||||
'sha256' { $sp = New-Object -TypeName System.Security.Cryptography.SHA256CryptoServiceProvider }
|
||||
'sha384' { $sp = New-Object -TypeName System.Security.Cryptography.SHA384CryptoServiceProvider }
|
||||
'sha512' { $sp = New-Object -TypeName System.Security.Cryptography.SHA512CryptoServiceProvider }
|
||||
}
|
||||
|
||||
$fs = [System.IO.File]::Open($Path, [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read,
|
||||
[System.IO.FileShare]::ReadWrite)
|
||||
try {
|
||||
$hash = [System.BitConverter]::ToString($sp.ComputeHash($fs)).Replace("-", "").ToLower()
|
||||
} finally {
|
||||
$fs.Dispose()
|
||||
}
|
||||
return $hash
|
||||
}
|
||||
|
||||
Function Invoke-DownloadFile {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][Ansible.Basic.AnsibleModule]$Module,
|
||||
[Parameter(Mandatory=$true)][Uri]$Uri,
|
||||
[Parameter(Mandatory=$true)][String]$Dest,
|
||||
[String]$Checksum,
|
||||
[String]$ChecksumAlgorithm
|
||||
)
|
||||
|
||||
# Check $dest parent folder exists before attempting download, which avoids unhelpful generic error message.
|
||||
$dest_parent = Split-Path -LiteralPath $Dest
|
||||
if (-not (Test-Path -LiteralPath $dest_parent -PathType Container)) {
|
||||
$module.FailJson("The path '$dest_parent' does not exist for destination '$Dest', or is not visible to the current user. Ensure download destination folder exists (perhaps using win_file state=directory) before win_get_url runs.")
|
||||
}
|
||||
|
||||
$download_script = {
|
||||
param($Response, $Stream)
|
||||
|
||||
# Download the file to a temporary directory so we can compare it
|
||||
$tmp_dest = Join-Path -Path $Module.Tmpdir -ChildPath ([System.IO.Path]::GetRandomFileName())
|
||||
$fs = [System.IO.File]::Create($tmp_dest)
|
||||
try {
|
||||
$Stream.CopyTo($fs)
|
||||
$fs.Flush()
|
||||
} finally {
|
||||
$fs.Dispose()
|
||||
}
|
||||
$tmp_checksum = Get-Checksum -Path $tmp_dest -Algorithm $ChecksumAlgorithm
|
||||
$Module.Result.checksum_src = $tmp_checksum
|
||||
|
||||
# If the checksum has been set, verify the checksum of the remote against the input checksum.
|
||||
if ($Checksum -and $Checksum -ne $tmp_checksum) {
|
||||
$Module.FailJson(("The checksum for {0} did not match '{1}', it was '{2}'" -f $Uri, $Checksum, $tmp_checksum))
|
||||
}
|
||||
|
||||
$download = $true
|
||||
if (Test-Path -LiteralPath $Dest) {
|
||||
# Validate the remote checksum against the existing downloaded file
|
||||
$dest_checksum = Get-Checksum -Path $Dest -Algorithm $ChecksumAlgorithm
|
||||
|
||||
# If we don't need to download anything, save the dest checksum so we don't waste time calculating it
|
||||
# again at the end of the script
|
||||
if ($dest_checksum -eq $tmp_checksum) {
|
||||
$download = $false
|
||||
$Module.Result.checksum_dest = $dest_checksum
|
||||
$Module.Result.size = (Get-AnsibleItem -Path $Dest).Length
|
||||
}
|
||||
}
|
||||
|
||||
if ($download) {
|
||||
Copy-Item -LiteralPath $tmp_dest -Destination $Dest -Force -WhatIf:$Module.CheckMode > $null
|
||||
$Module.Result.changed = $true
|
||||
}
|
||||
}
|
||||
$web_request = Get-AnsibleWebRequest -Uri $Uri -Module $Module
|
||||
|
||||
try {
|
||||
Invoke-WithWebRequest -Module $Module -Request $web_request -Script $download_script
|
||||
} catch {
|
||||
$Module.FailJson("Error downloading '$Uri' to '$Dest': $($_.Exception.Message)", $_)
|
||||
}
|
||||
}
|
||||
|
||||
# Use last part of url for dest file name if a directory is supplied for $dest
|
||||
if (Test-Path -LiteralPath $dest -PathType Container) {
|
||||
$uri = [System.Uri]$url
|
||||
$basename = Split-Path -Path $uri.LocalPath -Leaf
|
||||
if ($uri.LocalPath -and $uri.LocalPath -ne '/' -and $basename) {
|
||||
$url_basename = Split-Path -Path $uri.LocalPath -Leaf
|
||||
$dest = Join-Path -Path $dest -ChildPath $url_basename
|
||||
} else {
|
||||
$dest = Join-Path -Path $dest -ChildPath $uri.Host
|
||||
}
|
||||
|
||||
# Ensure we have a string instead of a PS object to avoid serialization issues
|
||||
$dest = $dest.ToString()
|
||||
} elseif (([System.IO.Path]::GetFileName($dest)) -eq '') {
|
||||
# We have a trailing path separator
|
||||
$module.FailJson("The destination path '$dest' does not exist, or is not visible to the current user. Ensure download destination folder exists (perhaps using win_file state=directory) before win_get_url runs.")
|
||||
}
|
||||
|
||||
$module.Result.dest = $dest
|
||||
|
||||
if ($checksum) {
|
||||
$checksum = $checksum.Trim().ToLower()
|
||||
}
|
||||
if ($checksum_algorithm) {
|
||||
$checksum_algorithm = $checksum_algorithm.Trim().ToLower()
|
||||
}
|
||||
if ($checksum_url) {
|
||||
$checksum_url = $checksum_url.Trim()
|
||||
}
|
||||
|
||||
# Check for case $checksum variable contain url. If yes, get file data from url and replace original value in $checksum
|
||||
if ($checksum_url) {
|
||||
$checksum_uri = [System.Uri]$checksum_url
|
||||
if ($checksum_uri.Scheme -notin @("file", "ftp", "http", "https")) {
|
||||
$module.FailJson("Unsupported 'checksum_url' value for '$dest': '$checksum_url'")
|
||||
}
|
||||
|
||||
$checksum = Get-ChecksumFromUri -Module $Module -Uri $checksum_uri -SourceUri $url
|
||||
}
|
||||
|
||||
if ($force -or -not (Test-Path -LiteralPath $dest)) {
|
||||
# force=yes or dest does not exist, download the file
|
||||
# Note: Invoke-DownloadFile will compare the checksums internally if dest exists
|
||||
Invoke-DownloadFile -Module $module -Uri $url -Dest $dest -Checksum $checksum `
|
||||
-ChecksumAlgorithm $checksum_algorithm
|
||||
} else {
|
||||
# force=no, we want to check the last modified dates and only download if they don't match
|
||||
$is_modified = Compare-ModifiedFile -Module $module -Uri $url -Dest $dest
|
||||
if ($is_modified) {
|
||||
Invoke-DownloadFile -Module $module -Uri $url -Dest $dest -Checksum $checksum `
|
||||
-ChecksumAlgorithm $checksum_algorithm
|
||||
}
|
||||
}
|
||||
|
||||
if ((-not $module.Result.ContainsKey("checksum_dest")) -and (Test-Path -LiteralPath $dest)) {
|
||||
# Calculate the dest file checksum if it hasn't already been done
|
||||
$module.Result.checksum_dest = Get-Checksum -Path $dest -Algorithm $checksum_algorithm
|
||||
$module.Result.size = (Get-AnsibleItem -Path $dest).Length
|
||||
}
|
||||
|
||||
$module.ExitJson()
|
||||
|
||||
@ -1,215 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2014, Paul Durivage <paul.durivage@rackspace.com>, and others
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
# This is a windows documentation stub. actual code lives in the .ps1
|
||||
# file of the same name
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['stableinterface'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_get_url
|
||||
version_added: "1.7"
|
||||
short_description: Downloads file from HTTP, HTTPS, or FTP to node
|
||||
description:
|
||||
- Downloads files from HTTP, HTTPS, or FTP to the remote server.
|
||||
- The remote server I(must) have direct access to the remote resource.
|
||||
- For non-Windows targets, use the M(get_url) module instead.
|
||||
options:
|
||||
url:
|
||||
description:
|
||||
- The full URL of a file to download.
|
||||
type: str
|
||||
required: yes
|
||||
dest:
|
||||
description:
|
||||
- The location to save the file at the URL.
|
||||
- Be sure to include a filename and extension as appropriate.
|
||||
type: path
|
||||
required: yes
|
||||
force:
|
||||
description:
|
||||
- If C(yes), will download the file every time and replace the file if the contents change. If C(no), will only
|
||||
download the file if it does not exist or the remote file has been
|
||||
modified more recently than the local file.
|
||||
- This works by sending an http HEAD request to retrieve last modified
|
||||
time of the requested resource, so for this to work, the remote web
|
||||
server must support HEAD requests.
|
||||
type: bool
|
||||
default: yes
|
||||
version_added: "2.0"
|
||||
checksum:
|
||||
description:
|
||||
- If a I(checksum) is passed to this parameter, the digest of the
|
||||
destination file will be calculated after it is downloaded to ensure
|
||||
its integrity and verify that the transfer completed successfully.
|
||||
- This option cannot be set with I(checksum_url).
|
||||
type: str
|
||||
version_added: "2.8"
|
||||
checksum_algorithm:
|
||||
description:
|
||||
- Specifies the hashing algorithm used when calculating the checksum of
|
||||
the remote and destination file.
|
||||
type: str
|
||||
choices:
|
||||
- md5
|
||||
- sha1
|
||||
- sha256
|
||||
- sha384
|
||||
- sha512
|
||||
default: sha1
|
||||
version_added: "2.8"
|
||||
checksum_url:
|
||||
description:
|
||||
- Specifies a URL that contains the checksum values for the resource at
|
||||
I(url).
|
||||
- Like C(checksum), this is used to verify the integrity of the remote
|
||||
transfer.
|
||||
- This option cannot be set with I(checksum).
|
||||
type: str
|
||||
version_added: "2.8"
|
||||
url_username:
|
||||
description:
|
||||
- The username to use for authentication.
|
||||
- The aliases I(user) and I(username) are deprecated and will be removed in
|
||||
Ansible 2.14.
|
||||
aliases:
|
||||
- user
|
||||
- username
|
||||
url_password:
|
||||
description:
|
||||
- The password for I(url_username).
|
||||
- The alias I(password) is deprecated and will be removed in Ansible 2.14.
|
||||
aliases:
|
||||
- password
|
||||
proxy_url:
|
||||
version_added: "2.0"
|
||||
proxy_username:
|
||||
version_added: "2.0"
|
||||
proxy_password:
|
||||
version_added: "2.0"
|
||||
headers:
|
||||
version_added: "2.4"
|
||||
use_proxy:
|
||||
version_added: "2.4"
|
||||
follow_redirects:
|
||||
version_added: "2.9"
|
||||
maximum_redirection:
|
||||
version_added: "2.9"
|
||||
client_cert:
|
||||
version_added: "2.9"
|
||||
client_cert_password:
|
||||
version_added: "2.9"
|
||||
method:
|
||||
description:
|
||||
- This option is not for use with C(win_get_url) and should be ignored.
|
||||
version_added: "2.9"
|
||||
notes:
|
||||
- If your URL includes an escaped slash character (%2F) this module will convert it to a real slash.
|
||||
This is a result of the behaviour of the System.Uri class as described in
|
||||
L(the documentation,https://docs.microsoft.com/en-us/dotnet/framework/configure-apps/file-schema/network/schemesettings-element-uri-settings#remarks).
|
||||
- Since Ansible 2.8, the module will skip reporting a change if the remote
|
||||
checksum is the same as the local local even when C(force=yes). This is to
|
||||
better align with M(get_url).
|
||||
extends_documentation_fragment:
|
||||
- url_windows
|
||||
seealso:
|
||||
- module: get_url
|
||||
- module: uri
|
||||
- module: win_uri
|
||||
author:
|
||||
- Paul Durivage (@angstwad)
|
||||
- Takeshi Kuramochi (@tksarah)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Download earthrise.jpg to specified path
|
||||
win_get_url:
|
||||
url: http://www.example.com/earthrise.jpg
|
||||
dest: C:\Users\RandomUser\earthrise.jpg
|
||||
|
||||
- name: Download earthrise.jpg to specified path only if modified
|
||||
win_get_url:
|
||||
url: http://www.example.com/earthrise.jpg
|
||||
dest: C:\Users\RandomUser\earthrise.jpg
|
||||
force: no
|
||||
|
||||
- name: Download earthrise.jpg to specified path through a proxy server.
|
||||
win_get_url:
|
||||
url: http://www.example.com/earthrise.jpg
|
||||
dest: C:\Users\RandomUser\earthrise.jpg
|
||||
proxy_url: http://10.0.0.1:8080
|
||||
proxy_username: username
|
||||
proxy_password: password
|
||||
|
||||
- name: Download file from FTP with authentication
|
||||
win_get_url:
|
||||
url: ftp://server/file.txt
|
||||
dest: '%TEMP%\ftp-file.txt'
|
||||
url_username: ftp-user
|
||||
url_password: ftp-password
|
||||
|
||||
- name: Download src with sha256 checksum url
|
||||
win_get_url:
|
||||
url: http://www.example.com/earthrise.jpg
|
||||
dest: C:\temp\earthrise.jpg
|
||||
checksum_url: http://www.example.com/sha256sum.txt
|
||||
checksum_algorithm: sha256
|
||||
force: True
|
||||
|
||||
- name: Download src with sha256 checksum url
|
||||
win_get_url:
|
||||
url: http://www.example.com/earthrise.jpg
|
||||
dest: C:\temp\earthrise.jpg
|
||||
checksum: a97e6837f60cec6da4491bab387296bbcd72bdba
|
||||
checksum_algorithm: sha1
|
||||
force: True
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
dest:
|
||||
description: destination file/path
|
||||
returned: always
|
||||
type: str
|
||||
sample: C:\Users\RandomUser\earthrise.jpg
|
||||
checksum_dest:
|
||||
description: <algorithm> checksum of the file after the download
|
||||
returned: success and dest has been downloaded
|
||||
type: str
|
||||
sample: 6e642bb8dd5c2e027bf21dd923337cbb4214f827
|
||||
checksum_src:
|
||||
description: <algorithm> checksum of the remote resource
|
||||
returned: force=yes or dest did not exist
|
||||
type: str
|
||||
sample: 6e642bb8dd5c2e027bf21dd923337cbb4214f827
|
||||
elapsed:
|
||||
description: The elapsed seconds between the start of poll and the end of the module.
|
||||
returned: always
|
||||
type: float
|
||||
sample: 2.1406487
|
||||
size:
|
||||
description: size of the dest file
|
||||
returned: success
|
||||
type: int
|
||||
sample: 1220
|
||||
url:
|
||||
description: requested url
|
||||
returned: always
|
||||
type: str
|
||||
sample: http://www.example.com/earthrise.jpg
|
||||
msg:
|
||||
description: Error message, or HTTP status message from web-server
|
||||
returned: always
|
||||
type: str
|
||||
sample: OK
|
||||
status_code:
|
||||
description: HTTP status code
|
||||
returned: always
|
||||
type: int
|
||||
sample: 200
|
||||
'''
|
||||
@ -1,54 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2014, Chris Hoffman <choffman@chathamfinancial.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
|
||||
|
||||
$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
|
||||
$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present","absent"
|
||||
$description = Get-AnsibleParam -obj $params -name "description" -type "str"
|
||||
|
||||
$result = @{
|
||||
changed = $false
|
||||
}
|
||||
|
||||
$adsi = [ADSI]"WinNT://$env:COMPUTERNAME"
|
||||
$group = $adsi.Children | Where-Object {$_.SchemaClassName -eq 'group' -and $_.Name -eq $name }
|
||||
|
||||
try {
|
||||
If ($state -eq "present") {
|
||||
If (-not $group) {
|
||||
If (-not $check_mode) {
|
||||
$group = $adsi.Create("Group", $name)
|
||||
$group.SetInfo()
|
||||
}
|
||||
|
||||
$result.changed = $true
|
||||
}
|
||||
|
||||
If ($null -ne $description) {
|
||||
IF (-not $group.description -or $group.description -ne $description) {
|
||||
$group.description = $description
|
||||
If (-not $check_mode) {
|
||||
$group.SetInfo()
|
||||
}
|
||||
$result.changed = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
ElseIf ($state -eq "absent" -and $group) {
|
||||
If (-not $check_mode) {
|
||||
$adsi.delete("Group", $group.Name.Value)
|
||||
}
|
||||
$result.changed = $true
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Fail-Json $result $_.Exception.Message
|
||||
}
|
||||
|
||||
Exit-Json $result
|
||||
@ -1,57 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2014, Chris Hoffman <choffman@chathamfinancial.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
# this is a windows documentation stub. actual code lives in the .ps1
|
||||
# file of the same name
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['stableinterface'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_group
|
||||
version_added: "1.7"
|
||||
short_description: Add and remove local groups
|
||||
description:
|
||||
- Add and remove local groups.
|
||||
- For non-Windows targets, please use the M(group) module instead.
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of the group.
|
||||
type: str
|
||||
required: yes
|
||||
description:
|
||||
description:
|
||||
- Description of the group.
|
||||
type: str
|
||||
state:
|
||||
description:
|
||||
- Create or remove the group.
|
||||
type: str
|
||||
choices: [ absent, present ]
|
||||
default: present
|
||||
seealso:
|
||||
- module: group
|
||||
- module: win_domain_group
|
||||
- module: win_group_membership
|
||||
author:
|
||||
- Chris Hoffman (@chrishoffman)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Create a new group
|
||||
win_group:
|
||||
name: deploy
|
||||
description: Deploy Group
|
||||
state: present
|
||||
|
||||
- name: Remove a group
|
||||
win_group:
|
||||
name: deploy
|
||||
state: absent
|
||||
'''
|
||||
@ -1,190 +0,0 @@
|
||||
#!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
|
||||
#Requires -Module Ansible.ModuleUtils.SID
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Test-GroupMember {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Return SID and consistent account name (DOMAIN\Username) format of desired member.
|
||||
Also, ensure member can be resolved/exists on the target system by checking its SID.
|
||||
.NOTES
|
||||
Returns a hashtable of the same type as returned from Get-GroupMember.
|
||||
Accepts username (users, groups) and domains in the formats accepted by Convert-ToSID.
|
||||
#>
|
||||
param(
|
||||
[String]$GroupMember
|
||||
)
|
||||
|
||||
$parsed_member = @{
|
||||
sid = $null
|
||||
account_name = $null
|
||||
}
|
||||
|
||||
$sid = Convert-ToSID -account_name $GroupMember
|
||||
$account_name = Convert-FromSID -sid $sid
|
||||
|
||||
$parsed_member.sid = $sid
|
||||
$parsed_member.account_name = $account_name
|
||||
|
||||
return $parsed_member
|
||||
}
|
||||
|
||||
function Get-GroupMember {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Retrieve group members for a given group, and return in a common format.
|
||||
.NOTES
|
||||
Returns an array of hashtables of the same type as returned from Test-GroupMember.
|
||||
#>
|
||||
param(
|
||||
[System.DirectoryServices.DirectoryEntry]$Group
|
||||
)
|
||||
|
||||
# instead of using ForEach pipeline we use a standard loop and cast the
|
||||
# object to the ADSI adapter type before using it to get the SID and path
|
||||
# this solves an random issue where multiple casts could fail once the raw
|
||||
# object is invoked at least once
|
||||
$raw_members = $Group.psbase.Invoke("Members")
|
||||
$current_members = [System.Collections.ArrayList]@()
|
||||
foreach ($raw_member in $raw_members) {
|
||||
$raw_member = [ADSI]$raw_member
|
||||
$sid_bytes = $raw_member.InvokeGet("objectSID")
|
||||
$ads_path = $raw_member.InvokeGet("ADsPath")
|
||||
$member_info = @{
|
||||
sid = New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList $sid_bytes, 0
|
||||
adspath = $ads_path
|
||||
}
|
||||
$current_members.Add($member_info) > $null
|
||||
}
|
||||
|
||||
$members = @()
|
||||
foreach ($current_member in $current_members) {
|
||||
$parsed_member = @{
|
||||
sid = $current_member.sid
|
||||
account_name = $null
|
||||
}
|
||||
|
||||
$rootless_adspath = $current_member.adspath.Replace("WinNT://", "")
|
||||
$split_adspath = $rootless_adspath.Split("/")
|
||||
|
||||
# Ignore lookup on a broken SID, and just return the SID as the account_name
|
||||
if ($split_adspath.Count -eq 1 -and $split_adspath[0] -like "S-1*") {
|
||||
$parsed_member.account_name = $split_adspath[0]
|
||||
} else {
|
||||
$account_name = Convert-FromSID -sid $current_member.sid
|
||||
$parsed_member.account_name = $account_name
|
||||
}
|
||||
|
||||
$members += $parsed_member
|
||||
}
|
||||
|
||||
return $members
|
||||
}
|
||||
|
||||
$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
|
||||
$members = Get-AnsibleParam -obj $params -name "members" -type "list" -failifempty $true
|
||||
$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present","absent","pure"
|
||||
|
||||
$result = @{
|
||||
changed = $false
|
||||
name = $name
|
||||
}
|
||||
if ($state -in @("present", "pure")) {
|
||||
$result.added = @()
|
||||
}
|
||||
if ($state -in @("absent", "pure")) {
|
||||
$result.removed = @()
|
||||
}
|
||||
|
||||
$adsi = [ADSI]"WinNT://$env:COMPUTERNAME"
|
||||
$group = $adsi.Children | Where-Object { $_.SchemaClassName -eq "group" -and $_.Name -eq $name }
|
||||
|
||||
if (!$group) {
|
||||
Fail-Json -obj $result -message "Could not find local group $name"
|
||||
}
|
||||
|
||||
$current_members = Get-GroupMember -Group $group
|
||||
$pure_members = @()
|
||||
|
||||
foreach ($member in $members) {
|
||||
$group_member = Test-GroupMember -GroupMember $member
|
||||
if ($state -eq "pure") {
|
||||
$pure_members += $group_member
|
||||
}
|
||||
|
||||
$user_in_group = $false
|
||||
foreach ($current_member in $current_members) {
|
||||
if ($current_member.sid -eq $group_member.sid) {
|
||||
$user_in_group = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
$member_sid = "WinNT://{0}" -f $group_member.sid
|
||||
|
||||
try {
|
||||
if ($state -in @("present", "pure") -and !$user_in_group) {
|
||||
if (!$check_mode) {
|
||||
$group.Add($member_sid)
|
||||
$result.added += $group_member.account_name
|
||||
}
|
||||
$result.changed = $true
|
||||
} elseif ($state -eq "absent" -and $user_in_group) {
|
||||
if (!$check_mode) {
|
||||
$group.Remove($member_sid)
|
||||
$result.removed += $group_member.account_name
|
||||
}
|
||||
$result.changed = $true
|
||||
}
|
||||
} catch {
|
||||
Fail-Json -obj $result -message $_.Exception.Message
|
||||
}
|
||||
}
|
||||
|
||||
if ($state -eq "pure") {
|
||||
# Perform removals for existing group members not defined in $members
|
||||
$current_members = Get-GroupMember -Group $group
|
||||
|
||||
foreach ($current_member in $current_members) {
|
||||
$user_to_remove = $true
|
||||
foreach ($pure_member in $pure_members) {
|
||||
if ($pure_member.sid -eq $current_member.sid) {
|
||||
$user_to_remove = $false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
$member_sid = "WinNT://{0}" -f $current_member.sid
|
||||
|
||||
try {
|
||||
if ($user_to_remove) {
|
||||
if (!$check_mode) {
|
||||
$group.Remove($member_sid)
|
||||
$result.removed += $current_member.account_name
|
||||
}
|
||||
$result.changed = $true
|
||||
}
|
||||
} catch {
|
||||
Fail-Json -obj $result -message $_.Exception.Message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$final_members = Get-GroupMember -Group $group
|
||||
|
||||
if ($final_members) {
|
||||
$result.members = [Array]$final_members.account_name
|
||||
} else {
|
||||
$result.members = @()
|
||||
}
|
||||
|
||||
Exit-Json -obj $result
|
||||
@ -1,101 +0,0 @@
|
||||
#!/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)
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_group_membership
|
||||
version_added: "2.4"
|
||||
short_description: Manage Windows local group membership
|
||||
description:
|
||||
- Allows the addition and removal of local, service and domain users,
|
||||
and domain groups from a local group.
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of the local group to manage membership on.
|
||||
type: str
|
||||
required: yes
|
||||
members:
|
||||
description:
|
||||
- A list of members to ensure are present/absent from the group.
|
||||
- Accepts local users as .\username, and SERVERNAME\username.
|
||||
- Accepts domain users and groups as DOMAIN\username and username@DOMAIN.
|
||||
- Accepts service users as NT AUTHORITY\username.
|
||||
- Accepts all local, domain and service user types as username,
|
||||
favoring domain lookups when in a domain.
|
||||
type: list
|
||||
required: yes
|
||||
state:
|
||||
description:
|
||||
- Desired state of the members in the group.
|
||||
- C(pure) was added in Ansible 2.8.
|
||||
- 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
|
||||
seealso:
|
||||
- module: win_domain_group
|
||||
- module: win_domain_membership
|
||||
- module: win_group
|
||||
author:
|
||||
- Andrew Saraceni (@andrewsaraceni)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Add a local and domain user to a local group
|
||||
win_group_membership:
|
||||
name: Remote Desktop Users
|
||||
members:
|
||||
- NewLocalAdmin
|
||||
- DOMAIN\TestUser
|
||||
state: present
|
||||
|
||||
- name: Remove a domain group and service user from a local group
|
||||
win_group_membership:
|
||||
name: Backup Operators
|
||||
members:
|
||||
- DOMAIN\TestGroup
|
||||
- NT AUTHORITY\SYSTEM
|
||||
state: absent
|
||||
|
||||
- name: Ensure only a domain user exists in a local group
|
||||
win_group_membership:
|
||||
name: Remote Desktop Users
|
||||
members:
|
||||
- DOMAIN\TestUser
|
||||
state: pure
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
name:
|
||||
description: The name of the target local group.
|
||||
returned: always
|
||||
type: str
|
||||
sample: Administrators
|
||||
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)
|
||||
type: list
|
||||
sample: ["SERVERNAME\\NewLocalAdmin", "DOMAIN\\TestUser"]
|
||||
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)
|
||||
type: list
|
||||
sample: ["DOMAIN\\TestGroup", "NT AUTHORITY\\SYSTEM"]
|
||||
members:
|
||||
description: A list of all local group members at completion; this is empty
|
||||
if the group contains no members.
|
||||
returned: success
|
||||
type: list
|
||||
sample: ["DOMAIN\\TestUser", "SERVERNAME\\NewLocalAdmin"]
|
||||
'''
|
||||
@ -1,32 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2018, Ripon Banik (@riponbanik)
|
||||
# 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
|
||||
$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
|
||||
$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
|
||||
|
||||
$current_computer_name = (Get-CimInstance -Class Win32_ComputerSystem).DNSHostname
|
||||
|
||||
$result = @{
|
||||
changed = $false
|
||||
old_name = $current_computer_name
|
||||
reboot_required = $false
|
||||
}
|
||||
|
||||
if ($name -ne $current_computer_name) {
|
||||
Try {
|
||||
Rename-Computer -NewName $name -Force -WhatIf:$check_mode
|
||||
} Catch {
|
||||
Fail-Json -obj $result -message "Failed to rename computer to '$name': $($_.Exception.Message)"
|
||||
}
|
||||
$result.changed = $true
|
||||
$result.reboot_required = $true
|
||||
}
|
||||
|
||||
Exit-Json -obj $result
|
||||
@ -1,55 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# this is a windows documentation stub. actual code lives in the .ps1
|
||||
# file of the same name
|
||||
|
||||
# Copyright: (c) 2018, Ripon Banik (@riponbanik)
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
module: win_hostname
|
||||
version_added: "2.6"
|
||||
short_description: Manages local Windows computer name
|
||||
description:
|
||||
- Manages local Windows computer name.
|
||||
- A reboot is required for the computer name to take effect.
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- The hostname to set for the computer.
|
||||
type: str
|
||||
required: true
|
||||
seealso:
|
||||
- module: win_dns_client
|
||||
author:
|
||||
- Ripon Banik (@riponbanik)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Change the hostname to sample-hostname
|
||||
win_hostname:
|
||||
name: sample-hostname
|
||||
register: res
|
||||
|
||||
- name: Reboot
|
||||
win_reboot:
|
||||
when: res.reboot_required
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
old_name:
|
||||
description: The original hostname that was set before it was changed.
|
||||
returned: always
|
||||
type: str
|
||||
sample: old_hostname
|
||||
reboot_required:
|
||||
description: Whether a reboot is required to complete the hostname change.
|
||||
returned: always
|
||||
type: bool
|
||||
sample: true
|
||||
'''
|
||||
@ -1,83 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2019, Carson Anderson <rcanderson23@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 = "list"; required = $true }
|
||||
state = @{ type = "str"; default = "present"; choices = @("absent", "present") }
|
||||
source = @{ type = "str" }
|
||||
include_parent = @{ type = "bool"; default = $false }
|
||||
}
|
||||
supports_check_mode = $true
|
||||
}
|
||||
|
||||
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||||
|
||||
$name = $module.Params.name
|
||||
$state = $module.Params.state
|
||||
$source = $module.Params.source
|
||||
$include_parent = $module.Params.include_parent
|
||||
|
||||
$module.Result.reboot_required = $false
|
||||
|
||||
if (-not (Get-Command -Name Enable-WindowsOptionalFeature -ErrorAction SilentlyContinue)) {
|
||||
$module.FailJson("This version of Windows does not support the Enable-WindowsOptionalFeature.")
|
||||
}
|
||||
|
||||
$changed_features = [System.Collections.Generic.List`1[String]]@()
|
||||
foreach ($feature_name in $name) {
|
||||
try {
|
||||
$feature_state_start = Get-WindowsOptionalFeature -Online -FeatureName $feature_name
|
||||
} catch [System.Runtime.InteropServices.COMException] {
|
||||
# Server 2012 raises a COMException and doesn't return $null even with -ErrorAction SilentlyContinue
|
||||
$feature_state_start = $null
|
||||
}
|
||||
if (-not $feature_state_start) {
|
||||
$module.FailJson("Failed to find feature '$feature_name'")
|
||||
}
|
||||
|
||||
if ($state -eq "present" -and $feature_state_start.State -notlike "Enabled*") {
|
||||
# Matches for "Enabled" and "EnabledPending"
|
||||
$changed_features.Add($feature_name)
|
||||
} elseif ($state -eq "absent" -and $feature_state_start.State -notlike "Disabled*") {
|
||||
# Matches for Disabled, DisabledPending, and DisabledWithPayloadRemoved
|
||||
$changed_features.Add($feature_name)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ($state -eq "present" -and $changed_features.Count -gt 0) {
|
||||
$install_args = @{
|
||||
FeatureName = $changed_features
|
||||
All = $include_parent
|
||||
}
|
||||
|
||||
if ($source) {
|
||||
if (-not (Test-Path -LiteralPath $source)) {
|
||||
$module.FailJson("Path could not be found '$source'")
|
||||
}
|
||||
$install_args.Source = $source
|
||||
}
|
||||
|
||||
if (-not $module.CheckMode) {
|
||||
$action_result = Enable-WindowsOptionalFeature -Online -NoRestart @install_args
|
||||
$module.Result.reboot_required = $action_result.RestartNeeded
|
||||
}
|
||||
$module.Result.changed = $true
|
||||
} elseif ($state -eq "absent" -and $changed_features.Count -gt 0) {
|
||||
$remove_args = @{
|
||||
FeatureName = $changed_features
|
||||
}
|
||||
|
||||
if (-not $module.CheckMode) {
|
||||
$action_result = Disable-WindowsOptionalFeature -Online -NoRestart @remove_args
|
||||
$module.Result.reboot_required = $action_result.RestartNeeded
|
||||
}
|
||||
$module.Result.changed = $true
|
||||
}
|
||||
$module.ExitJson()
|
||||
|
||||
@ -1,90 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2019, Carson Anderson <rcanderson23@gmail.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
# this is a windows documentation stub. actual code lives in the .ps1
|
||||
# file of the same name
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_optional_feature
|
||||
version_added: "2.8"
|
||||
short_description: Manage optional Windows features
|
||||
description:
|
||||
- Install or uninstall optional Windows features on non-Server Windows.
|
||||
- This module uses the C(Enable-WindowsOptionalFeature) and C(Disable-WindowsOptionalFeature) cmdlets.
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- The name(s) of the feature to install.
|
||||
- This relates to C(FeatureName) in the Powershell cmdlet.
|
||||
- To list all available features use the PowerShell command C(Get-WindowsOptionalFeature).
|
||||
type: list
|
||||
required: yes
|
||||
state:
|
||||
description:
|
||||
- Whether to ensure the feature is absent or present on the system.
|
||||
type: str
|
||||
choices: [ absent, present ]
|
||||
default: present
|
||||
include_parent:
|
||||
description:
|
||||
- Whether to enable the parent feature and the parent's dependencies.
|
||||
type: bool
|
||||
default: no
|
||||
source:
|
||||
description:
|
||||
- Specify a source to install the feature from.
|
||||
- Can either be C({driveletter}:\sources\sxs) or C(\\{IP}\share\sources\sxs).
|
||||
type: str
|
||||
seealso:
|
||||
- module: win_chocolatey
|
||||
- module: win_feature
|
||||
- module: win_package
|
||||
author:
|
||||
- Carson Anderson (@rcanderson23)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Install .Net 3.5
|
||||
win_optional_feature:
|
||||
name: NetFx3
|
||||
state: present
|
||||
|
||||
- name: Install .Net 3.5 from source
|
||||
win_optional_feature:
|
||||
name: NetFx3
|
||||
source: \\share01\win10\sources\sxs
|
||||
state: present
|
||||
|
||||
- name: Install Microsoft Subsystem for Linux
|
||||
win_optional_feature:
|
||||
name: Microsoft-Windows-Subsystem-Linux
|
||||
state: present
|
||||
register: wsl_status
|
||||
|
||||
- name: Reboot if installing Linux Subsytem as feature requires it
|
||||
win_reboot:
|
||||
when: wsl_status.reboot_required
|
||||
|
||||
- name: Install multiple features in one task
|
||||
win_optional_feature:
|
||||
name:
|
||||
- NetFx3
|
||||
- Microsoft-Windows-Subsystem-Linux
|
||||
state: present
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
reboot_required:
|
||||
description: True when the target server requires a reboot to complete updates
|
||||
returned: success
|
||||
type: bool
|
||||
sample: true
|
||||
'''
|
||||
@ -1,60 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2015, Hans-Joachim Kliemeck <git@kliemeck.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
#Requires -Module Ansible.ModuleUtils.SID
|
||||
|
||||
$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
|
||||
|
||||
$path = Get-AnsibleParam -obj $params -name "path" -type "path" -failifempty $true
|
||||
$user = Get-AnsibleParam -obj $params -name "user" -type "str" -failifempty $true
|
||||
$recurse = Get-AnsibleParam -obj $params -name "recurse" -type "bool" -default $false -resultobj $result
|
||||
|
||||
If (-Not (Test-Path -LiteralPath $path)) {
|
||||
Fail-Json $result "$path file or directory does not exist on the host"
|
||||
}
|
||||
|
||||
# Test that the user/group is resolvable on the local machine
|
||||
$sid = Convert-ToSID -account_name $user
|
||||
if (!$sid) {
|
||||
Fail-Json $result "$user is not a valid user or group on the host machine or domain"
|
||||
}
|
||||
|
||||
Try {
|
||||
$objUser = New-Object System.Security.Principal.SecurityIdentifier($sid)
|
||||
|
||||
$file = Get-Item -LiteralPath $path
|
||||
$acl = Get-Acl -LiteralPath $file.FullName
|
||||
|
||||
If ($acl.getOwner([System.Security.Principal.SecurityIdentifier]) -ne $objUser) {
|
||||
$acl.setOwner($objUser)
|
||||
Set-Acl -LiteralPath $file.FullName -AclObject $acl -WhatIf:$check_mode
|
||||
$result.changed = $true
|
||||
}
|
||||
|
||||
If ($recurse -and $file -is [System.IO.DirectoryInfo]) {
|
||||
# Get-ChildItem falls flat on pre PSv5 when dealing with complex path chars
|
||||
$files = $file.EnumerateFileSystemInfos("*", [System.IO.SearchOption]::AllDirectories)
|
||||
ForEach($file in $files){
|
||||
$acl = Get-Acl -LiteralPath $file.FullName
|
||||
|
||||
If ($acl.getOwner([System.Security.Principal.SecurityIdentifier]) -ne $objUser) {
|
||||
$acl.setOwner($objUser)
|
||||
Set-Acl -LiteralPath $file.FullName -AclObject $acl -WhatIf:$check_mode
|
||||
$result.changed = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Catch {
|
||||
Fail-Json $result "an error occurred when attempting to change owner on $path for $($user): $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
Exit-Json $result
|
||||
@ -1,58 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2015, Hans-Joachim Kliemeck <git@kliemeck.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_owner
|
||||
version_added: "2.1"
|
||||
short_description: Set owner
|
||||
description:
|
||||
- Set owner of files or directories.
|
||||
options:
|
||||
path:
|
||||
description:
|
||||
- Path to be used for changing owner.
|
||||
type: path
|
||||
required: yes
|
||||
user:
|
||||
description:
|
||||
- Name to be used for changing owner.
|
||||
type: str
|
||||
required: yes
|
||||
recurse:
|
||||
description:
|
||||
- Indicates if the owner should be changed recursively.
|
||||
type: bool
|
||||
default: no
|
||||
seealso:
|
||||
- module: win_acl
|
||||
- module: win_file
|
||||
- module: win_stat
|
||||
author:
|
||||
- Hans-Joachim Kliemeck (@h0nIg)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Change owner of path
|
||||
win_owner:
|
||||
path: C:\apache
|
||||
user: apache
|
||||
recurse: yes
|
||||
|
||||
- name: Set the owner of root directory
|
||||
win_owner:
|
||||
path: C:\apache
|
||||
user: SYSTEM
|
||||
recurse: no
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
|
||||
'''
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,386 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2014, Trond Hindenes <trond@hindenes.com>, and others
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
# this is a windows documentation stub. actual code lives in the .ps1
|
||||
# file of the same name
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_package
|
||||
version_added: "1.7"
|
||||
short_description: Installs/uninstalls an installable package
|
||||
description:
|
||||
- Installs or uninstalls software packages for Windows.
|
||||
- Supports C(.exe), C(.msi), C(.msp), C(.appx), C(.appxbundle), C(.msix),
|
||||
and C(.msixbundle).
|
||||
- These packages can be sourced from the local file system, network file share
|
||||
or a url.
|
||||
- See I(provider) for more info on each package type that is supported.
|
||||
options:
|
||||
arguments:
|
||||
description:
|
||||
- Any arguments the installer needs to either install or uninstall the
|
||||
package.
|
||||
- If the package is an MSI do not supply the C(/qn), C(/log) or
|
||||
C(/norestart) arguments.
|
||||
- This is only used for the C(msi), C(msp), and C(registry) providers.
|
||||
- As of Ansible 2.5, this parameter can be a list of arguments and the
|
||||
module will escape the arguments as necessary, it is recommended to use a
|
||||
string when dealing with MSI packages due to the unique escaping issues
|
||||
with msiexec.
|
||||
type: raw
|
||||
chdir:
|
||||
description:
|
||||
- Set the specified path as the current working directory before installing
|
||||
or uninstalling a package.
|
||||
- This is only used for the C(msi), C(msp), and C(registry) providers.
|
||||
type: path
|
||||
version_added: '2.8'
|
||||
creates_path:
|
||||
description:
|
||||
- Will check the existence of the path specified and use the result to
|
||||
determine whether the package is already installed.
|
||||
- You can use this in conjunction with C(product_id) and other C(creates_*).
|
||||
type: path
|
||||
version_added: '2.4'
|
||||
creates_service:
|
||||
description:
|
||||
- Will check the existing of the service specified and use the result to
|
||||
determine whether the package is already installed.
|
||||
- You can use this in conjunction with C(product_id) and other C(creates_*).
|
||||
type: str
|
||||
version_added: '2.4'
|
||||
creates_version:
|
||||
description:
|
||||
- Will check the file version property of the file at C(creates_path) and
|
||||
use the result to determine whether the package is already installed.
|
||||
- C(creates_path) MUST be set and is a file.
|
||||
- You can use this in conjunction with C(product_id) and other C(creates_*).
|
||||
type: str
|
||||
version_added: '2.4'
|
||||
expected_return_code:
|
||||
description:
|
||||
- One or more return codes from the package installation that indicates
|
||||
success.
|
||||
- Before Ansible 2.4 this was just 0 but since Ansible 2.4 this is both C(0) and
|
||||
C(3010).
|
||||
- A return code of C(3010) usually means that a reboot is required, the
|
||||
C(reboot_required) return value is set if the return code is C(3010).
|
||||
- This is only used for the C(msi), C(msp), and C(registry) providers.
|
||||
type: list
|
||||
elements: int
|
||||
default: [0, 3010]
|
||||
log_path:
|
||||
description:
|
||||
- Specifies the path to a log file that is persisted after a package is
|
||||
installed or uninstalled.
|
||||
- This is only used for the C(msi) or C(msp) provider.
|
||||
- When omitted, a temporary log file is used instead for those providers.
|
||||
- This is only valid for MSI files, use C(arguments) for the C(registry)
|
||||
provider.
|
||||
type: path
|
||||
version_added: '2.8'
|
||||
password:
|
||||
description:
|
||||
- The password for C(user_name), must be set when C(user_name) is.
|
||||
- This option is deprecated in favour of using become, see examples for
|
||||
more information.
|
||||
type: str
|
||||
aliases: [ user_password ]
|
||||
path:
|
||||
description:
|
||||
- Location of the package to be installed or uninstalled.
|
||||
- This package can either be on the local file system, network share or a
|
||||
url.
|
||||
- When C(state=present), C(product_id) is not set and the path is a URL,
|
||||
this file will always be downloaded to a temporary directory for
|
||||
idempotency checks, otherwise the file will only be downloaded if the
|
||||
package has not been installed based on the C(product_id) checks.
|
||||
- If C(state=present) then this value MUST be set.
|
||||
- If C(state=absent) then this value does not need to be set if
|
||||
C(product_id) is.
|
||||
type: str
|
||||
product_id:
|
||||
description:
|
||||
- The product id of the installed packaged.
|
||||
- This is used for checking whether the product is already installed and
|
||||
getting the uninstall information if C(state=absent).
|
||||
- For msi packages, this is the C(ProductCode) (GUID) of the package. This
|
||||
can be found under the same registry paths as the C(registry) provider.
|
||||
- For msp packages, this is the C(PatchCode) (GUID) of the package which
|
||||
can found under the C(Details -> Revision number) of the file's properties.
|
||||
- For msix packages, this is the C(Name) or C(PackageFullName) of the
|
||||
package found under the C(Get-AppxPackage) cmdlet.
|
||||
- For registry (exe) packages, this is the registry key name under the
|
||||
registry paths specified in I(provider).
|
||||
- This value is ignored if C(path) is set to a local accesible file path
|
||||
and the package is not an C(exe).
|
||||
- This SHOULD be set when the package is an C(exe), or the path is a url
|
||||
or a network share and credential delegation is not being used. The
|
||||
C(creates_*) options can be used instead but is not recommended.
|
||||
- The C(productid) alias will be removed in Ansible 2.14.
|
||||
type: str
|
||||
aliases: [ productid ]
|
||||
provider:
|
||||
description:
|
||||
- Set the package provider to use when searching for a package.
|
||||
- The C(auto) provider will select the proper provider if I(path)
|
||||
otherwise it scans all the other providers based on the I(product_id).
|
||||
- The C(msi) provider scans for MSI packages installed on a machine wide
|
||||
and current user context based on the C(ProductCode) of the MSI. Before
|
||||
Ansible 2.10 only the machine wide context was searched.
|
||||
- The C(msix) provider is used to install C(.appx), C(.msix),
|
||||
C(.appxbundle), or C(.msixbundle) packages. These packages are only
|
||||
installed or removed on the current use. The host must be set to allow
|
||||
sideloaded apps or in developer mode. See the examples for how to enable
|
||||
this. If a package is already installed but C(path) points to an updated
|
||||
package, this will be installed over the top of the existing one.
|
||||
- The C(msp) provider scans for all MSP patches installed on a machine wide
|
||||
and current user context based on the C(PatchCode) of the MSP. A C(msp)
|
||||
will be applied or removed on all C(msi) products that it applies to and
|
||||
is installed. If the patch is obsoleted or superseded then no action will
|
||||
be taken.
|
||||
- The C(registry) provider is used for traditional C(exe) installers and
|
||||
uses the following registry path to determine if a product was installed;
|
||||
C(HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall),
|
||||
C(HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall),
|
||||
C(HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall), and
|
||||
C(HKCU:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall).
|
||||
Before Ansible 2.10 only the C(HKLM) hive was searched.
|
||||
- Before Ansible 2.10 only the C(msi) and C(registry) providers were used.
|
||||
choices:
|
||||
- auto
|
||||
- msi
|
||||
- msix
|
||||
- msp
|
||||
- registry
|
||||
default: auto
|
||||
type: str
|
||||
version_added: '2.10'
|
||||
state:
|
||||
description:
|
||||
- Whether to install or uninstall the package.
|
||||
- The module uses I(product_id) to determine whether the package is
|
||||
installed or not.
|
||||
- For all providers but C(auto), the I(path) can be used for idempotency
|
||||
checks if it is locally accesible filesystem path.
|
||||
- The C(ensure) alias will be removed in Ansible 2.14.
|
||||
type: str
|
||||
choices: [ absent, present ]
|
||||
default: present
|
||||
aliases: [ ensure ]
|
||||
username:
|
||||
description:
|
||||
- Username of an account with access to the package if it is located on a
|
||||
file share.
|
||||
- This is only needed if the WinRM transport is over an auth method that
|
||||
does not support credential delegation like Basic or NTLM or become is
|
||||
not used.
|
||||
- This option is deprecated in favour of using become, see examples for
|
||||
more information.
|
||||
type: str
|
||||
aliases: [ user_name ]
|
||||
|
||||
# Overrides the options in url_windows
|
||||
client_cert:
|
||||
version_added: '2.10'
|
||||
client_cert_password:
|
||||
version_added: '2.10'
|
||||
follow_redirects:
|
||||
version_added: '2.10'
|
||||
force_basic_auth:
|
||||
version_added: '2.10'
|
||||
headers:
|
||||
version_added: '2.10'
|
||||
http_agent:
|
||||
version_added: '2.10'
|
||||
maximum_redirection:
|
||||
version_added: '2.10'
|
||||
method:
|
||||
version_added: '2.10'
|
||||
proxy_password:
|
||||
version_added: '2.10'
|
||||
proxy_url:
|
||||
version_added: '2.10'
|
||||
proxy_use_default_credential:
|
||||
version_added: '2.10'
|
||||
proxy_username:
|
||||
version_added: '2.10'
|
||||
timeout:
|
||||
description:
|
||||
- Specifies how long the web download request can be pending before it
|
||||
times out in seconds.
|
||||
- Set to C(0) to specify an infinite timeout.
|
||||
version_added: '2.10'
|
||||
url_password:
|
||||
version_added: '2.10'
|
||||
url_username:
|
||||
version_added: '2.10'
|
||||
use_default_credential:
|
||||
version_added: '2.10'
|
||||
use_proxy:
|
||||
version_added: '2.10'
|
||||
extends_documentation_fragment:
|
||||
- url_windows
|
||||
notes:
|
||||
- When C(state=absent) and the product is an exe, the path may be different
|
||||
from what was used to install the package originally. If path is not set then
|
||||
the path used will be what is set under C(QuietUninstallString) or
|
||||
C(UninstallString) in the registry for that I(product_id).
|
||||
- By default all msi installs and uninstalls will be run with the arguments
|
||||
C(/log, /qn, /norestart).
|
||||
- All the installation checks under C(product_id) and C(creates_*) add
|
||||
together, if one fails then the program is considered to be absent.
|
||||
seealso:
|
||||
- module: win_chocolatey
|
||||
- module: win_hotfix
|
||||
- module: win_updates
|
||||
author:
|
||||
- Trond Hindenes (@trondhindenes)
|
||||
- Jordan Borean (@jborean93)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Install the Visual C thingy
|
||||
win_package:
|
||||
path: http://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe
|
||||
product_id: '{CF2BEA3C-26EA-32F8-AA9B-331F7E34BA97}'
|
||||
arguments: /install /passive /norestart
|
||||
|
||||
- name: Install Visual C thingy with list of arguments instead of a string
|
||||
win_package:
|
||||
path: http://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe
|
||||
product_id: '{CF2BEA3C-26EA-32F8-AA9B-331F7E34BA97}'
|
||||
arguments:
|
||||
- /install
|
||||
- /passive
|
||||
- /norestart
|
||||
|
||||
- name: Install Remote Desktop Connection Manager from msi with a permanent log
|
||||
win_package:
|
||||
path: https://download.microsoft.com/download/A/F/0/AF0071F3-B198-4A35-AA90-C68D103BDCCF/rdcman.msi
|
||||
product_id: '{0240359E-6A4C-4884-9E94-B397A02D893C}'
|
||||
state: present
|
||||
log_path: D:\logs\vcredist_x64-exe-{{lookup('pipe', 'date +%Y%m%dT%H%M%S')}}.log
|
||||
|
||||
- name: Uninstall Remote Desktop Connection Manager
|
||||
win_package:
|
||||
product_id: '{0240359E-6A4C-4884-9E94-B397A02D893C}'
|
||||
state: absent
|
||||
|
||||
- name: Install Remote Desktop Connection Manager locally omitting the product_id
|
||||
win_package:
|
||||
path: C:\temp\rdcman.msi
|
||||
state: present
|
||||
|
||||
- name: Uninstall Remote Desktop Connection Manager from local MSI omitting the product_id
|
||||
win_package:
|
||||
path: C:\temp\rdcman.msi
|
||||
state: absent
|
||||
|
||||
# 7-Zip exe doesn't use a guid for the Product ID
|
||||
- name: Install 7zip from a network share with specific credentials
|
||||
win_package:
|
||||
path: \\domain\programs\7z.exe
|
||||
product_id: 7-Zip
|
||||
arguments: /S
|
||||
state: present
|
||||
become: yes
|
||||
become_method: runas
|
||||
become_flags: logon_type=new_credential logon_flags=netcredentials_only
|
||||
vars:
|
||||
ansible_become_user: DOMAIN\User
|
||||
ansible_become_password: Password
|
||||
|
||||
- name: Install 7zip and use a file version for the installation check
|
||||
win_package:
|
||||
path: C:\temp\7z.exe
|
||||
creates_path: C:\Program Files\7-Zip\7z.exe
|
||||
creates_version: 16.04
|
||||
state: present
|
||||
|
||||
- name: Uninstall 7zip from the exe
|
||||
win_package:
|
||||
path: C:\Program Files\7-Zip\Uninstall.exe
|
||||
product_id: 7-Zip
|
||||
arguments: /S
|
||||
state: absent
|
||||
|
||||
- name: Uninstall 7zip without specifying the path
|
||||
win_package:
|
||||
product_id: 7-Zip
|
||||
arguments: /S
|
||||
state: absent
|
||||
|
||||
- name: Install application and override expected return codes
|
||||
win_package:
|
||||
path: https://download.microsoft.com/download/1/6/7/167F0D79-9317-48AE-AEDB-17120579F8E2/NDP451-KB2858728-x86-x64-AllOS-ENU.exe
|
||||
product_id: '{7DEBE4EB-6B40-3766-BB35-5CBBC385DA37}'
|
||||
arguments: '/q /norestart'
|
||||
state: present
|
||||
expected_return_code: [0, 666, 3010]
|
||||
|
||||
- name: Install a .msp patch
|
||||
win_package:
|
||||
path: C:\Patches\Product.msp
|
||||
state: present
|
||||
|
||||
- name: Remove a .msp patch
|
||||
win_package:
|
||||
product_id: '{AC76BA86-A440-FFFF-A440-0C13154E5D00}'
|
||||
state: absent
|
||||
|
||||
- name: Enable installation of 3rd party MSIX packages
|
||||
win_regedit:
|
||||
path: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock
|
||||
name: AllowAllTrustedApps
|
||||
data: 1
|
||||
type: dword
|
||||
state: present
|
||||
|
||||
- name: Install an MSIX package for the current user
|
||||
win_package:
|
||||
path: C:\Installers\Calculator.msix # Can be .appx, .msixbundle, or .appxbundle
|
||||
state: present
|
||||
|
||||
- name: Uninstall an MSIX package using the product_id
|
||||
win_package:
|
||||
product_id: InputApp
|
||||
state: absent
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
log:
|
||||
description: The contents of the MSI or MSP log.
|
||||
returned: installation/uninstallation failure for MSI or MSP packages
|
||||
type: str
|
||||
sample: Installation completed successfully
|
||||
rc:
|
||||
description: The return code of the package process.
|
||||
returned: change occurred
|
||||
type: int
|
||||
sample: 0
|
||||
reboot_required:
|
||||
description: Whether a reboot is required to finalise package. This is set
|
||||
to true if the executable return code is 3010.
|
||||
returned: always
|
||||
type: bool
|
||||
sample: true
|
||||
stdout:
|
||||
description: The stdout stream of the package process.
|
||||
returned: failure during install or uninstall
|
||||
type: str
|
||||
sample: Installing program
|
||||
stderr:
|
||||
description: The stderr stream of the package process.
|
||||
returned: failure during install or uninstall
|
||||
type: str
|
||||
sample: Failed to install program
|
||||
'''
|
||||
@ -1,145 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
|
||||
Set-StrictMode -Version 2
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$system_path = "System\CurrentControlSet\Control\Session Manager\Environment"
|
||||
$user_path = "Environment"
|
||||
|
||||
# list/arraylist methods don't allow IEqualityComparer override for case/backslash/quote-insensitivity, roll our own search
|
||||
Function Get-IndexOfPathElement ($list, [string]$value) {
|
||||
$idx = 0
|
||||
$value = $value.Trim('"').Trim('\')
|
||||
ForEach($el in $list) {
|
||||
If ([string]$el.Trim('"').Trim('\') -ieq $value) {
|
||||
return $idx
|
||||
}
|
||||
|
||||
$idx++
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
# alters list in place, returns true if at least one element was added
|
||||
Function Add-Elements ($existing_elements, $elements_to_add) {
|
||||
$last_idx = -1
|
||||
$changed = $false
|
||||
|
||||
ForEach($el in $elements_to_add) {
|
||||
$idx = Get-IndexOfPathElement $existing_elements $el
|
||||
|
||||
# add missing elements at the end
|
||||
If ($idx -eq -1) {
|
||||
$last_idx = $existing_elements.Add($el)
|
||||
$changed = $true
|
||||
}
|
||||
ElseIf ($idx -lt $last_idx) {
|
||||
$existing_elements.RemoveAt($idx) | Out-Null
|
||||
$existing_elements.Add($el) | Out-Null
|
||||
$last_idx = $existing_elements.Count - 1
|
||||
$changed = $true
|
||||
}
|
||||
Else {
|
||||
$last_idx = $idx
|
||||
}
|
||||
}
|
||||
|
||||
return $changed
|
||||
}
|
||||
|
||||
# alters list in place, returns true if at least one element was removed
|
||||
Function Remove-Elements ($existing_elements, $elements_to_remove) {
|
||||
$count = $existing_elements.Count
|
||||
|
||||
ForEach($el in $elements_to_remove) {
|
||||
$idx = Get-IndexOfPathElement $existing_elements $el
|
||||
$result.removed_idx = $idx
|
||||
If ($idx -gt -1) {
|
||||
$existing_elements.RemoveAt($idx)
|
||||
}
|
||||
}
|
||||
|
||||
return $count -ne $existing_elements.Count
|
||||
}
|
||||
|
||||
# PS registry provider doesn't allow access to unexpanded REG_EXPAND_SZ; fall back to .NET
|
||||
Function Get-RawPathVar ($scope) {
|
||||
If ($scope -eq "user") {
|
||||
$env_key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($user_path)
|
||||
}
|
||||
ElseIf ($scope -eq "machine") {
|
||||
$env_key = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey($system_path)
|
||||
}
|
||||
|
||||
return $env_key.GetValue($var_name, "", [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
|
||||
}
|
||||
|
||||
Function Set-RawPathVar($path_value, $scope) {
|
||||
If ($scope -eq "user") {
|
||||
$var_path = "HKCU:\" + $user_path
|
||||
}
|
||||
ElseIf ($scope -eq "machine") {
|
||||
$var_path = "HKLM:\" + $system_path
|
||||
}
|
||||
|
||||
Set-ItemProperty $var_path -Name $var_name -Value $path_value -Type ExpandString | Out-Null
|
||||
|
||||
return $path_value
|
||||
}
|
||||
|
||||
$parsed_args = Parse-Args $args -supports_check_mode $true
|
||||
|
||||
$result = @{changed=$false}
|
||||
|
||||
$var_name = Get-AnsibleParam $parsed_args "name" -Default "PATH"
|
||||
$elements = Get-AnsibleParam $parsed_args "elements" -FailIfEmpty $result
|
||||
$state = Get-AnsibleParam $parsed_args "state" -Default "present" -ValidateSet "present","absent"
|
||||
$scope = Get-AnsibleParam $parsed_args "scope" -Default "machine" -ValidateSet "machine","user"
|
||||
|
||||
$check_mode = Get-AnsibleParam $parsed_args "_ansible_check_mode" -Default $false
|
||||
|
||||
If ($elements -is [string]) {
|
||||
$elements = @($elements)
|
||||
}
|
||||
|
||||
If ($elements -isnot [Array]) {
|
||||
Fail-Json $result "elements must be a string or list of path strings"
|
||||
}
|
||||
|
||||
$current_value = Get-RawPathVar $scope
|
||||
$result.path_value = $current_value
|
||||
|
||||
# TODO: test case-canonicalization on wacky unicode values (eg turkish i)
|
||||
# TODO: detect and warn/fail on unparseable path? (eg, unbalanced quotes, invalid path chars)
|
||||
# TODO: detect and warn/fail if system path and Powershell isn't on it?
|
||||
|
||||
$existing_elements = New-Object System.Collections.ArrayList
|
||||
|
||||
# split on semicolons, accounting for quoted values with embedded semicolons (which may or may not be wrapped in whitespace)
|
||||
$pathsplit_re = [regex] '((?<q>\s*"[^"]+"\s*)|(?<q>[^;]+))(;$|$|;)'
|
||||
|
||||
ForEach ($m in $pathsplit_re.Matches($current_value)) {
|
||||
$existing_elements.Add($m.Groups['q'].Value) | Out-Null
|
||||
}
|
||||
|
||||
If ($state -eq "absent") {
|
||||
$result.changed = Remove-Elements $existing_elements $elements
|
||||
}
|
||||
ElseIf ($state -eq "present") {
|
||||
$result.changed = Add-Elements $existing_elements $elements
|
||||
}
|
||||
|
||||
# calculate the new path value from the existing elements
|
||||
$path_value = [String]::Join(";", $existing_elements.ToArray())
|
||||
$result.path_value = $path_value
|
||||
|
||||
If ($result.changed -and -not $check_mode) {
|
||||
Set-RawPathVar $path_value $scope | Out-Null
|
||||
}
|
||||
|
||||
Exit-Json $result
|
||||
@ -1,79 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016, Red Hat | Ansible
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
# This is a windows documentation stub. Actual code lives in the .ps1
|
||||
# file of the same name
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_path
|
||||
version_added: "2.3"
|
||||
short_description: Manage Windows path environment variables
|
||||
description:
|
||||
- Allows element-based ordering, addition, and removal of Windows path environment variables.
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Target path environment variable name.
|
||||
type: str
|
||||
default: PATH
|
||||
elements:
|
||||
description:
|
||||
- A single path element, or a list of path elements (ie, directories) to add or remove.
|
||||
- When multiple elements are included in the list (and C(state) is C(present)), the elements are guaranteed to appear in the same relative order
|
||||
in the resultant path value.
|
||||
- Variable expansions (eg, C(%VARNAME%)) are allowed, and are stored unexpanded in the target path element.
|
||||
- Any existing path elements not mentioned in C(elements) are always preserved in their current order.
|
||||
- New path elements are appended to the path, and existing path elements may be moved closer to the end to satisfy the requested ordering.
|
||||
- Paths are compared in a case-insensitive fashion, and trailing backslashes are ignored for comparison purposes. However, note that trailing
|
||||
backslashes in YAML require quotes.
|
||||
type: list
|
||||
required: yes
|
||||
state:
|
||||
description:
|
||||
- Whether the path elements specified in C(elements) should be present or absent.
|
||||
type: str
|
||||
choices: [ absent, present ]
|
||||
scope:
|
||||
description:
|
||||
- The level at which the environment variable specified by C(name) should be managed (either for the current user or global machine scope).
|
||||
type: str
|
||||
choices: [ machine, user ]
|
||||
default: machine
|
||||
notes:
|
||||
- This module is for modifying individual elements of path-like
|
||||
environment variables. For general-purpose management of other
|
||||
environment vars, use the M(win_environment) module.
|
||||
- This module does not broadcast change events.
|
||||
This means that the minority of windows applications which can have
|
||||
their environment changed without restarting will not be notified and
|
||||
therefore will need restarting to pick up new environment settings.
|
||||
- User level environment variables will require an interactive user to
|
||||
log out and in again before they become available.
|
||||
seealso:
|
||||
- module: win_environment
|
||||
author:
|
||||
- Matt Davis (@nitzmahone)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Ensure that system32 and Powershell are present on the global system path, and in the specified order
|
||||
win_path:
|
||||
elements:
|
||||
- '%SystemRoot%\system32'
|
||||
- '%SystemRoot%\system32\WindowsPowerShell\v1.0'
|
||||
|
||||
- name: Ensure that C:\Program Files\MyJavaThing is not on the current user's CLASSPATH
|
||||
win_path:
|
||||
name: CLASSPATH
|
||||
elements: C:\Program Files\MyJavaThing
|
||||
scope: user
|
||||
state: absent
|
||||
'''
|
||||
@ -1,21 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#AnsibleRequires -CSharpUtil Ansible.Basic
|
||||
|
||||
$spec = @{
|
||||
options = @{
|
||||
data = @{ type = "str"; default = "pong" }
|
||||
}
|
||||
supports_check_mode = $true
|
||||
}
|
||||
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||||
$data = $module.Params.data
|
||||
|
||||
if ($data -eq "crash") {
|
||||
throw "boom"
|
||||
}
|
||||
|
||||
$module.Result.ping = $data
|
||||
$module.ExitJson()
|
||||
@ -1,55 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>, and others
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
# this is a windows documentation stub. actual code lives in the .ps1
|
||||
# file of the same name
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['stableinterface'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_ping
|
||||
version_added: "1.7"
|
||||
short_description: A windows version of the classic ping module
|
||||
description:
|
||||
- Checks management connectivity of a windows host.
|
||||
- This is NOT ICMP ping, this is just a trivial test module.
|
||||
- For non-Windows targets, use the M(ping) module instead.
|
||||
- For Network targets, use the M(net_ping) module instead.
|
||||
options:
|
||||
data:
|
||||
description:
|
||||
- Alternate data to return instead of 'pong'.
|
||||
- If this parameter is set to C(crash), the module will cause an exception.
|
||||
type: str
|
||||
default: pong
|
||||
seealso:
|
||||
- module: ping
|
||||
author:
|
||||
- Chris Church (@cchurch)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
# Test connectivity to a windows host
|
||||
# ansible winserver -m win_ping
|
||||
|
||||
- name: Example from an Ansible Playbook
|
||||
win_ping:
|
||||
|
||||
- name: Induce an exception to see what happens
|
||||
win_ping:
|
||||
data: crash
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
ping:
|
||||
description: Value provided with the data parameter.
|
||||
returned: success
|
||||
type: str
|
||||
sample: pong
|
||||
'''
|
||||
@ -1,131 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['stableinterface'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_reboot
|
||||
short_description: Reboot a windows machine
|
||||
description:
|
||||
- Reboot a Windows machine, wait for it to go down, come back up, and respond to commands.
|
||||
- For non-Windows targets, use the M(reboot) module instead.
|
||||
version_added: '2.1'
|
||||
options:
|
||||
pre_reboot_delay:
|
||||
description:
|
||||
- Seconds to wait before reboot. Passed as a parameter to the reboot command.
|
||||
type: int
|
||||
default: 2
|
||||
aliases: [ pre_reboot_delay_sec ]
|
||||
post_reboot_delay:
|
||||
description:
|
||||
- Seconds to wait after the reboot command was successful before attempting to validate the system rebooted successfully.
|
||||
- This is useful if you want wait for something to settle despite your connection already working.
|
||||
type: int
|
||||
default: 0
|
||||
version_added: '2.4'
|
||||
aliases: [ post_reboot_delay_sec ]
|
||||
shutdown_timeout:
|
||||
description:
|
||||
- Maximum seconds to wait for shutdown to occur.
|
||||
- Increase this timeout for very slow hardware, large update applications, etc.
|
||||
- This option has been removed since Ansible 2.5 as the win_reboot behavior has changed.
|
||||
type: int
|
||||
default: 600
|
||||
aliases: [ shutdown_timeout_sec ]
|
||||
reboot_timeout:
|
||||
description:
|
||||
- Maximum seconds to wait for machine to re-appear on the network and respond to a test command.
|
||||
- This timeout is evaluated separately for both reboot verification and test command success so maximum clock time is actually twice this value.
|
||||
type: int
|
||||
default: 600
|
||||
aliases: [ reboot_timeout_sec ]
|
||||
connect_timeout:
|
||||
description:
|
||||
- Maximum seconds to wait for a single successful TCP connection to the WinRM endpoint before trying again.
|
||||
type: int
|
||||
default: 5
|
||||
aliases: [ connect_timeout_sec ]
|
||||
test_command:
|
||||
description:
|
||||
- Command to expect success for to determine the machine is ready for management.
|
||||
type: str
|
||||
default: whoami
|
||||
msg:
|
||||
description:
|
||||
- Message to display to users.
|
||||
type: str
|
||||
default: Reboot initiated by Ansible
|
||||
boot_time_command:
|
||||
description:
|
||||
- Command to run that returns a unique string indicating the last time the system was booted.
|
||||
- Setting this to a command that has different output each time it is run will cause the task to fail.
|
||||
type: str
|
||||
default: '(Get-WmiObject -ClassName Win32_OperatingSystem).LastBootUpTime'
|
||||
version_added: '2.10'
|
||||
notes:
|
||||
- If a shutdown was already scheduled on the system, C(win_reboot) will abort the scheduled shutdown and enforce its own shutdown.
|
||||
- Beware that when C(win_reboot) returns, the Windows system may not have settled yet and some base services could be in limbo.
|
||||
This can result in unexpected behavior. Check the examples for ways to mitigate this.
|
||||
- The connection user must have the C(SeRemoteShutdownPrivilege) privilege enabled, see
|
||||
U(https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/force-shutdown-from-a-remote-system)
|
||||
for more information.
|
||||
seealso:
|
||||
- module: reboot
|
||||
author:
|
||||
- Matt Davis (@nitzmahone)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Reboot the machine with all defaults
|
||||
win_reboot:
|
||||
|
||||
- name: Reboot a slow machine that might have lots of updates to apply
|
||||
win_reboot:
|
||||
reboot_timeout: 3600
|
||||
|
||||
# Install a Windows feature and reboot if necessary
|
||||
- name: Install IIS Web-Server
|
||||
win_feature:
|
||||
name: Web-Server
|
||||
register: iis_install
|
||||
|
||||
- name: Reboot when Web-Server feature requires it
|
||||
win_reboot:
|
||||
when: iis_install.reboot_required
|
||||
|
||||
# One way to ensure the system is reliable, is to set WinRM to a delayed startup
|
||||
- name: Ensure WinRM starts when the system has settled and is ready to work reliably
|
||||
win_service:
|
||||
name: WinRM
|
||||
start_mode: delayed
|
||||
|
||||
|
||||
# Additionally, you can add a delay before running the next task
|
||||
- name: Reboot a machine that takes time to settle after being booted
|
||||
win_reboot:
|
||||
post_reboot_delay: 120
|
||||
|
||||
# Or you can make win_reboot validate exactly what you need to work before running the next task
|
||||
- name: Validate that the netlogon service has started, before running the next task
|
||||
win_reboot:
|
||||
test_command: 'exit (Get-Service -Name Netlogon).Status -ne "Running"'
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
rebooted:
|
||||
description: True if the machine was rebooted.
|
||||
returned: always
|
||||
type: bool
|
||||
sample: true
|
||||
elapsed:
|
||||
description: The number of seconds that elapsed waiting for the system to be rebooted.
|
||||
returned: always
|
||||
type: float
|
||||
sample: 23.2
|
||||
'''
|
||||
@ -1,126 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# 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
|
||||
|
||||
$path = Get-AnsibleParam -obj $params -name "path" -type "str" -failifempty $true -aliases "key"
|
||||
$name = Get-AnsibleParam -obj $params -name "name" -type "str" -aliases "entry","value"
|
||||
|
||||
$result = @{
|
||||
changed = $false
|
||||
}
|
||||
|
||||
Function Get-PropertyValue {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][Microsoft.Win32.RegistryKey]$Key,
|
||||
[String]$Name
|
||||
)
|
||||
|
||||
$value = $Key.GetValue($Name, $null, [Microsoft.Win32.RegistryValueOptions]::None)
|
||||
if ($null -eq $value) {
|
||||
# Property does not exist or the key's (Default) is not set
|
||||
return $null
|
||||
}
|
||||
|
||||
$raw_value = $Key.GetValue($Name, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
|
||||
|
||||
if ($Name -eq "") {
|
||||
# The key's (Default) will fail on GetValueKind
|
||||
$type = [Microsoft.Win32.RegistryValueKind]::String
|
||||
} else {
|
||||
$type = $Key.GetValueKind($Name)
|
||||
}
|
||||
|
||||
if ($type -in @([Microsoft.Win32.RegistryValueKind]::Binary, [Microsoft.Win32.RegistryValueKind]::None)) {
|
||||
$formatted_raw_value = [System.Collections.Generic.List`1[String]]@()
|
||||
foreach ($byte in $value) {
|
||||
$formatted_raw_value.Add("0x{0:x2}" -f $byte)
|
||||
}
|
||||
$raw_value = $formatted_raw_value
|
||||
} elseif ($type -eq [Microsoft.Win32.RegistryValueKind]::DWord) {
|
||||
# .NET returns the value as a signed integer, we need to make it unsigned
|
||||
$value = [UInt32]("0x{0:x}" -f $value)
|
||||
$raw_value = $value
|
||||
} elseif ($type -eq [Microsoft.Win32.RegistryValueKind]::QWord) {
|
||||
$value = [UInt64]("0x{0:x}" -f $value)
|
||||
$raw_value = $value
|
||||
}
|
||||
|
||||
$return_type = switch($type.ToString()) {
|
||||
"Binary" { "REG_BINARY" }
|
||||
"String" { "REG_SZ" }
|
||||
"DWord" { "REG_DWORD" }
|
||||
"QWord" { "REG_QWORD" }
|
||||
"MultiString" { "REG_MULTI_SZ" }
|
||||
"ExpandString" { "REG_EXPAND_SZ" }
|
||||
"None" { "REG_NONE" }
|
||||
default { "Unknown - $($type.ToString())" }
|
||||
}
|
||||
|
||||
return @{
|
||||
type = $return_type
|
||||
value = $value
|
||||
raw_value = $raw_value
|
||||
}
|
||||
}
|
||||
|
||||
# Will validate the key parameter to make sure it matches known format
|
||||
if ($path -notmatch "^HK(CC|CR|CU|LM|U):\\") {
|
||||
Fail-Json -obj $result -message "path: $path is not a valid registry path, see module documentation for examples."
|
||||
}
|
||||
|
||||
$registry_path = (Split-Path -Path $path -NoQualifier).Substring(1) # removes the hive: and leading \
|
||||
$registry_hive = switch(Split-Path -Path $path -Qualifier) {
|
||||
"HKCR:" { [Microsoft.Win32.Registry]::ClassesRoot }
|
||||
"HKCC:" { [Microsoft.Win32.Registry]::CurrentConfig }
|
||||
"HKCU:" { [Microsoft.Win32.Registry]::CurrentUser }
|
||||
"HKLM:" { [Microsoft.Win32.Registry]::LocalMachine }
|
||||
"HKU:" { [Microsoft.Win32.Registry]::Users }
|
||||
}
|
||||
|
||||
$key = $null
|
||||
try {
|
||||
$key = $registry_hive.OpenSubKey($registry_path, $false)
|
||||
|
||||
if ($null -ne $key) {
|
||||
if ($null -eq $name) {
|
||||
$property_info = @{}
|
||||
foreach ($property in $key.GetValueNames()) {
|
||||
$property_info.$property = Get-PropertyValue -Key $key -Name $property
|
||||
}
|
||||
|
||||
# Return the key's (Default) property if it has been defined
|
||||
$default_value = Get-PropertyValue -Key $key -Name ""
|
||||
if ($null -ne $default_value) {
|
||||
$property_info."" = $default_value
|
||||
}
|
||||
|
||||
$result.exists = $true
|
||||
$result.properties = $property_info
|
||||
$result.sub_keys = $key.GetSubKeyNames()
|
||||
} else {
|
||||
$property_value = Get-PropertyValue -Key $key -Name $name
|
||||
if ($null -ne $property_value) {
|
||||
$result.exists = $true
|
||||
$result += $property_value
|
||||
} else {
|
||||
$result.exists = $false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$result.exists = $false
|
||||
}
|
||||
} finally {
|
||||
if ($key) {
|
||||
$key.Dispose()
|
||||
}
|
||||
$registry_hive.Dispose()
|
||||
}
|
||||
|
||||
Exit-Json -obj $result
|
||||
|
||||
@ -1,121 +0,0 @@
|
||||
#!/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)
|
||||
|
||||
# this is a windows documentation stub. actual code lives in the .ps1
|
||||
# file of the same name
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_reg_stat
|
||||
version_added: "2.3"
|
||||
short_description: Get information about Windows registry keys
|
||||
description:
|
||||
- Like M(win_file), M(win_reg_stat) will return whether the key/property exists.
|
||||
- It also returns the sub keys and properties of the key specified.
|
||||
- If specifying a property name through I(property), it will return the information specific for that property.
|
||||
options:
|
||||
path:
|
||||
description: The full registry key path including the hive to search for.
|
||||
type: str
|
||||
required: yes
|
||||
aliases: [ key ]
|
||||
name:
|
||||
description:
|
||||
- The registry property name to get information for, the return json will not include the sub_keys and properties entries for the I(key) specified.
|
||||
- Set to an empty string to target the registry key's C((Default)) property value.
|
||||
type: str
|
||||
aliases: [ entry, value, property ]
|
||||
notes:
|
||||
- The C(properties) return value will contain an empty string key C("") that refers to the key's C(Default) value. If
|
||||
the value has not been set then this key is not returned.
|
||||
seealso:
|
||||
- module: win_regedit
|
||||
- module: win_regmerge
|
||||
author:
|
||||
- Jordan Borean (@jborean93)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Obtain information about a registry key using short form
|
||||
win_reg_stat:
|
||||
path: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion
|
||||
register: current_version
|
||||
|
||||
- name: Obtain information about a registry key property
|
||||
win_reg_stat:
|
||||
path: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion
|
||||
name: CommonFilesDir
|
||||
register: common_files_dir
|
||||
|
||||
- name: Obtain the registry key's (Default) property
|
||||
win_reg_stat:
|
||||
path: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion
|
||||
name: ''
|
||||
register: current_version_default
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
changed:
|
||||
description: Whether anything was changed.
|
||||
returned: always
|
||||
type: bool
|
||||
sample: true
|
||||
exists:
|
||||
description: States whether the registry key/property exists.
|
||||
returned: success and path/property exists
|
||||
type: bool
|
||||
sample: true
|
||||
properties:
|
||||
description: A dictionary containing all the properties and their values in the registry key.
|
||||
returned: success, path exists and property not specified
|
||||
type: dict
|
||||
sample: {
|
||||
"" : {
|
||||
"raw_value": "",
|
||||
"type": "REG_SZ",
|
||||
"value": ""
|
||||
},
|
||||
"binary_property" : {
|
||||
"raw_value": ["0x01", "0x16"],
|
||||
"type": "REG_BINARY",
|
||||
"value": [1, 22]
|
||||
},
|
||||
"multi_string_property" : {
|
||||
"raw_value": ["a", "b"],
|
||||
"type": "REG_MULTI_SZ",
|
||||
"value": ["a", "b"]
|
||||
}
|
||||
}
|
||||
sub_keys:
|
||||
description: A list of all the sub keys of the key specified.
|
||||
returned: success, path exists and property not specified
|
||||
type: list
|
||||
sample: [
|
||||
"AppHost",
|
||||
"Casting",
|
||||
"DateTime"
|
||||
]
|
||||
raw_value:
|
||||
description: Returns the raw value of the registry property, REG_EXPAND_SZ has no string expansion, REG_BINARY or REG_NONE is in hex 0x format.
|
||||
REG_NONE, this value is a hex string in the 0x format.
|
||||
returned: success, path/property exists and property specified
|
||||
type: str
|
||||
sample: '%ProgramDir%\\Common Files'
|
||||
type:
|
||||
description: The property type.
|
||||
returned: success, path/property exists and property specified
|
||||
type: str
|
||||
sample: "REG_EXPAND_SZ"
|
||||
value:
|
||||
description: The value of the property.
|
||||
returned: success, path/property exists and property specified
|
||||
type: str
|
||||
sample: 'C:\\Program Files\\Common Files'
|
||||
'''
|
||||
@ -1,495 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2015, Adam Keech <akeech@chathamfinancial.com>
|
||||
# Copyright: (c) 2015, Josh Ludwig <jludwig@chathamfinancial.com>
|
||||
# Copyright: (c) 2017, Jordan Borean <jborean93@gmail.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
#Requires -Module Ansible.ModuleUtils.PrivilegeUtil
|
||||
|
||||
$params = Parse-Args -arguments $args -supports_check_mode $true
|
||||
$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
|
||||
$diff_mode = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false
|
||||
$_remote_tmp = Get-AnsibleParam $params "_ansible_remote_tmp" -type "path" -default $env:TMP
|
||||
|
||||
$path = Get-AnsibleParam -obj $params -name "path" -type "str" -failifempty $true -aliases "key"
|
||||
$name = Get-AnsibleParam -obj $params -name "name" -type "str" -aliases "entry","value"
|
||||
$data = Get-AnsibleParam -obj $params -name "data"
|
||||
$type = Get-AnsibleParam -obj $params -name "type" -type "str" -default "string" -validateset "none","binary","dword","expandstring","multistring","string","qword" -aliases "datatype"
|
||||
$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present","absent"
|
||||
$delete_key = Get-AnsibleParam -obj $params -name "delete_key" -type "bool" -default $true
|
||||
$hive = Get-AnsibleParam -obj $params -name "hive" -type "path"
|
||||
|
||||
$result = @{
|
||||
changed = $false
|
||||
data_changed = $false
|
||||
data_type_changed = $false
|
||||
}
|
||||
|
||||
if ($diff_mode) {
|
||||
$result.diff = @{
|
||||
before = ""
|
||||
after = ""
|
||||
}
|
||||
}
|
||||
|
||||
$registry_util = @'
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ansible.WinRegedit
|
||||
{
|
||||
internal class NativeMethods
|
||||
{
|
||||
[DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
|
||||
public static extern int RegLoadKeyW(
|
||||
UInt32 hKey,
|
||||
string lpSubKey,
|
||||
string lpFile);
|
||||
|
||||
[DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
|
||||
public static extern int RegUnLoadKeyW(
|
||||
UInt32 hKey,
|
||||
string lpSubKey);
|
||||
}
|
||||
|
||||
public class Win32Exception : System.ComponentModel.Win32Exception
|
||||
{
|
||||
private string _msg;
|
||||
public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
|
||||
public Win32Exception(int errorCode, string message) : base(errorCode)
|
||||
{
|
||||
_msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode);
|
||||
}
|
||||
public override string Message { get { return _msg; } }
|
||||
public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
|
||||
}
|
||||
|
||||
public class Hive : IDisposable
|
||||
{
|
||||
private const UInt32 SCOPE = 0x80000002; // HKLM
|
||||
private string hiveKey;
|
||||
private bool loaded = false;
|
||||
|
||||
public Hive(string hiveKey, string hivePath)
|
||||
{
|
||||
this.hiveKey = hiveKey;
|
||||
int ret = NativeMethods.RegLoadKeyW(SCOPE, hiveKey, hivePath);
|
||||
if (ret != 0)
|
||||
throw new Win32Exception(ret, String.Format("Failed to load registry hive at {0}", hivePath));
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
public static void UnloadHive(string hiveKey)
|
||||
{
|
||||
int ret = NativeMethods.RegUnLoadKeyW(SCOPE, hiveKey);
|
||||
if (ret != 0)
|
||||
throw new Win32Exception(ret, String.Format("Failed to unload registry hive at {0}", hiveKey));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (loaded)
|
||||
{
|
||||
// Make sure the garbage collector disposes all unused handles and waits until it is complete
|
||||
GC.Collect();
|
||||
GC.WaitForPendingFinalizers();
|
||||
|
||||
UnloadHive(hiveKey);
|
||||
loaded = false;
|
||||
}
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
~Hive() { this.Dispose(); }
|
||||
}
|
||||
}
|
||||
'@
|
||||
|
||||
# fire a warning if the property name isn't specified, the (Default) key ($null) can only be a string
|
||||
if ($null -eq $name -and $type -ne "string") {
|
||||
Add-Warning -obj $result -message "the data type when name is not specified can only be 'string', the type has automatically been converted"
|
||||
$type = "string"
|
||||
}
|
||||
|
||||
# Check that the registry path is in PSDrive format: HKCC, HKCR, HKCU, HKLM, HKU
|
||||
if ($path -notmatch "^HK(CC|CR|CU|LM|U):\\") {
|
||||
Fail-Json $result "path: $path is not a valid powershell path, see module documentation for examples."
|
||||
}
|
||||
|
||||
# Add a warning if the path does not contains a \ and is not the leaf path
|
||||
$registry_path = (Split-Path -Path $path -NoQualifier).Substring(1) # removes the hive: and leading \
|
||||
$registry_leaf = Split-Path -Path $path -Leaf
|
||||
if ($registry_path -ne $registry_leaf -and -not $registry_path.Contains('\')) {
|
||||
$msg = "path is not using '\' as a separator, support for '/' as a separator will be removed in a future Ansible version"
|
||||
Add-DeprecationWarning -obj $result -message $msg -version 2.12
|
||||
$registry_path = $registry_path.Replace('/', '\')
|
||||
}
|
||||
|
||||
# Simplified version of Convert-HexStringToByteArray from
|
||||
# https://cyber-defense.sans.org/blog/2010/02/11/powershell-byte-array-hex-convert
|
||||
# Expects a hex in the format you get when you run reg.exe export,
|
||||
# and converts to a byte array so powershell can modify binary registry entries
|
||||
# import format is like 'hex:be,ef,be,ef,be,ef,be,ef,be,ef'
|
||||
Function Convert-RegExportHexStringToByteArray($string) {
|
||||
# Remove 'hex:' from the front of the string if present
|
||||
$string = $string.ToLower() -replace '^hex\:',''
|
||||
|
||||
# Remove whitespace and any other non-hex crud.
|
||||
$string = $string -replace '[^a-f0-9\\,x\-\:]',''
|
||||
|
||||
# Turn commas into colons
|
||||
$string = $string -replace ',',':'
|
||||
|
||||
# Maybe there's nothing left over to convert...
|
||||
if ($string.Length -eq 0) {
|
||||
return ,@()
|
||||
}
|
||||
|
||||
# Split string with or without colon delimiters.
|
||||
if ($string.Length -eq 1) {
|
||||
return ,@([System.Convert]::ToByte($string,16))
|
||||
} elseif (($string.Length % 2 -eq 0) -and ($string.IndexOf(":") -eq -1)) {
|
||||
return ,@($string -split '([a-f0-9]{2})' | foreach-object { if ($_) {[System.Convert]::ToByte($_,16)}})
|
||||
} elseif ($string.IndexOf(":") -ne -1) {
|
||||
return ,@($string -split ':+' | foreach-object {[System.Convert]::ToByte($_,16)})
|
||||
} else {
|
||||
return ,@()
|
||||
}
|
||||
}
|
||||
|
||||
Function Compare-RegistryProperties($existing, $new) {
|
||||
# Outputs $true if the property values don't match
|
||||
if ($existing -is [Array]) {
|
||||
(Compare-Object -ReferenceObject $existing -DifferenceObject $new -SyncWindow 0).Length -ne 0
|
||||
} else {
|
||||
$existing -cne $new
|
||||
}
|
||||
}
|
||||
|
||||
Function Get-DiffValue {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][Microsoft.Win32.RegistryValueKind]$Type,
|
||||
[Parameter(Mandatory=$true)][Object]$Value
|
||||
)
|
||||
|
||||
$diff = @{ type = $Type.ToString(); value = $Value }
|
||||
|
||||
$enum = [Microsoft.Win32.RegistryValueKind]
|
||||
if ($Type -in @($enum::Binary, $enum::None)) {
|
||||
$diff.value = [System.Collections.Generic.List`1[String]]@()
|
||||
foreach ($dec_value in $Value) {
|
||||
$diff.value.Add("0x{0:x2}" -f $dec_value)
|
||||
}
|
||||
} elseif ($Type -eq $enum::DWord) {
|
||||
$diff.value = "0x{0:x8}" -f $Value
|
||||
} elseif ($Type -eq $enum::QWord) {
|
||||
$diff.value = "0x{0:x16}" -f $Value
|
||||
}
|
||||
|
||||
return $diff
|
||||
}
|
||||
|
||||
Function Set-StateAbsent {
|
||||
param(
|
||||
# Used for diffs and exception messages to match up against Ansible input
|
||||
[Parameter(Mandatory=$true)][String]$PrintPath,
|
||||
[Parameter(Mandatory=$true)][Microsoft.Win32.RegistryKey]$Hive,
|
||||
[Parameter(Mandatory=$true)][String]$Path,
|
||||
[String]$Name,
|
||||
[Switch]$DeleteKey
|
||||
)
|
||||
|
||||
$key = $Hive.OpenSubKey($Path, $true)
|
||||
if ($null -eq $key) {
|
||||
# Key does not exist, no need to delete anything
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if ($DeleteKey -and -not $Name) {
|
||||
# delete_key=yes is set and name is null/empty, so delete the entire key
|
||||
$key.Dispose()
|
||||
$key = $null
|
||||
if (-not $check_mode) {
|
||||
try {
|
||||
$Hive.DeleteSubKeyTree($Path, $false)
|
||||
} catch {
|
||||
Fail-Json -obj $result -message "failed to delete registry key at $($PrintPath): $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
$result.changed = $true
|
||||
|
||||
if ($diff_mode) {
|
||||
$result.diff.before = @{$PrintPath = @{}}
|
||||
$result.diff.after = @{}
|
||||
}
|
||||
} else {
|
||||
# delete_key=no or name is not null/empty, delete the property not the full key
|
||||
$property = $key.GetValue($Name)
|
||||
if ($null -eq $property) {
|
||||
# property does not exist
|
||||
return
|
||||
}
|
||||
$property_type = $key.GetValueKind($Name) # used for the diff
|
||||
|
||||
if (-not $check_mode) {
|
||||
try {
|
||||
$key.DeleteValue($Name)
|
||||
} catch {
|
||||
Fail-Json -obj $result -message "failed to delete registry property '$Name' at $($PrintPath): $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
$result.changed = $true
|
||||
if ($diff_mode) {
|
||||
$diff_value = Get-DiffValue -Type $property_type -Value $property
|
||||
$result.diff.before = @{ $PrintPath = @{ $Name = $diff_value } }
|
||||
$result.diff.after = @{ $PrintPath = @{} }
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if ($key) {
|
||||
$key.Dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Function Set-StatePresent {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][String]$PrintPath,
|
||||
[Parameter(Mandatory=$true)][Microsoft.Win32.RegistryKey]$Hive,
|
||||
[Parameter(Mandatory=$true)][String]$Path,
|
||||
[String]$Name,
|
||||
[Object]$Data,
|
||||
[Microsoft.Win32.RegistryValueKind]$Type
|
||||
)
|
||||
|
||||
$key = $Hive.OpenSubKey($Path, $true)
|
||||
try {
|
||||
if ($null -eq $key) {
|
||||
# the key does not exist, create it so the next steps work
|
||||
if (-not $check_mode) {
|
||||
try {
|
||||
$key = $Hive.CreateSubKey($Path)
|
||||
} catch {
|
||||
Fail-Json -obj $result -message "failed to create registry key at $($PrintPath): $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
$result.changed = $true
|
||||
|
||||
if ($diff_mode) {
|
||||
$result.diff.before = @{}
|
||||
$result.diff.after = @{$PrintPath = @{}}
|
||||
}
|
||||
} elseif ($diff_mode) {
|
||||
# Make sure the diff is in an expected state for the key
|
||||
$result.diff.before = @{$PrintPath = @{}}
|
||||
$result.diff.after = @{$PrintPath = @{}}
|
||||
}
|
||||
|
||||
if ($null -eq $key -or $null -eq $Data) {
|
||||
# Check mode and key was created above, we cannot do any more work, or $Data is $null which happens when
|
||||
# we create a new key but haven't explicitly set the data
|
||||
return
|
||||
}
|
||||
|
||||
$property = $key.GetValue($Name, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
|
||||
if ($null -ne $property) {
|
||||
# property exists, need to compare the values and type
|
||||
$existing_type = $key.GetValueKind($name)
|
||||
$change_value = $false
|
||||
|
||||
if ($Type -ne $existing_type) {
|
||||
$change_value = $true
|
||||
$result.data_type_changed = $true
|
||||
$data_mismatch = Compare-RegistryProperties -existing $property -new $Data
|
||||
if ($data_mismatch) {
|
||||
$result.data_changed = $true
|
||||
}
|
||||
} else {
|
||||
$data_mismatch = Compare-RegistryProperties -existing $property -new $Data
|
||||
if ($data_mismatch) {
|
||||
$change_value = $true
|
||||
$result.data_changed = $true
|
||||
}
|
||||
}
|
||||
|
||||
if ($change_value) {
|
||||
if (-not $check_mode) {
|
||||
try {
|
||||
$key.SetValue($Name, $Data, $Type)
|
||||
} catch {
|
||||
Fail-Json -obj $result -message "failed to change registry property '$Name' at $($PrintPath): $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
$result.changed = $true
|
||||
|
||||
if ($diff_mode) {
|
||||
$result.diff.before.$PrintPath.$Name = Get-DiffValue -Type $existing_type -Value $property
|
||||
$result.diff.after.$PrintPath.$Name = Get-DiffValue -Type $Type -Value $Data
|
||||
}
|
||||
} elseif ($diff_mode) {
|
||||
$diff_value = Get-DiffValue -Type $existing_type -Value $property
|
||||
$result.diff.before.$PrintPath.$Name = $diff_value
|
||||
$result.diff.after.$PrintPath.$Name = $diff_value
|
||||
}
|
||||
} else {
|
||||
# property doesn't exist just create a new one
|
||||
if (-not $check_mode) {
|
||||
try {
|
||||
$key.SetValue($Name, $Data, $Type)
|
||||
} catch {
|
||||
Fail-Json -obj $result -message "failed to create registry property '$Name' at $($PrintPath): $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
$result.changed = $true
|
||||
|
||||
if ($diff_mode) {
|
||||
$result.diff.after.$PrintPath.$Name = Get-DiffValue -Type $Type -Value $Data
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if ($key) {
|
||||
$key.Dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# convert property names "" to $null as "" refers to (Default)
|
||||
if ($name -eq "") {
|
||||
$name = $null
|
||||
}
|
||||
|
||||
# convert the data to the required format
|
||||
if ($type -in @("binary", "none")) {
|
||||
if ($null -eq $data) {
|
||||
$data = ""
|
||||
}
|
||||
|
||||
# convert the data from string to byte array if in hex: format
|
||||
if ($data -is [String]) {
|
||||
$data = [byte[]](Convert-RegExportHexStringToByteArray -string $data)
|
||||
} elseif ($data -is [Int]) {
|
||||
if ($data -gt 255) {
|
||||
Fail-Json $result "cannot convert binary data '$data' to byte array, please specify this value as a yaml byte array or a comma separated hex value string"
|
||||
}
|
||||
$data = [byte[]]@([byte]$data)
|
||||
} elseif ($data -is [Array]) {
|
||||
$data = [byte[]]$data
|
||||
}
|
||||
} elseif ($type -in @("dword", "qword")) {
|
||||
# dword's and dword's don't allow null values, set to 0
|
||||
if ($null -eq $data) {
|
||||
$data = 0
|
||||
}
|
||||
|
||||
if ($data -is [String]) {
|
||||
# if the data is a string we need to convert it to an unsigned int64
|
||||
# it needs to be unsigned as Ansible passes in an unsigned value while
|
||||
# powershell uses a signed data type. The value will then be converted
|
||||
# below
|
||||
$data = [UInt64]$data
|
||||
}
|
||||
|
||||
if ($type -eq "dword") {
|
||||
if ($data -gt [UInt32]::MaxValue) {
|
||||
Fail-Json $result "data cannot be larger than 0xffffffff when type is dword"
|
||||
} elseif ($data -gt [Int32]::MaxValue) {
|
||||
# when dealing with larger int32 (> 2147483647 or 0x7FFFFFFF) powershell
|
||||
# automatically converts it to a signed int64. We need to convert this to
|
||||
# signed int32 by parsing the hex string value.
|
||||
$data = "0x$("{0:x}" -f $data)"
|
||||
}
|
||||
$data = [Int32]$data
|
||||
} else {
|
||||
if ($data -gt [UInt64]::MaxValue) {
|
||||
Fail-Json $result "data cannot be larger than 0xffffffffffffffff when type is qword"
|
||||
} elseif ($data -gt [Int64]::MaxValue) {
|
||||
$data = "0x$("{0:x}" -f $data)"
|
||||
}
|
||||
$data = [Int64]$data
|
||||
}
|
||||
} elseif ($type -in @("string", "expandstring") -and $name) {
|
||||
# a null string or expandstring must be empty quotes
|
||||
# Only do this if $name has been defined (not the default key)
|
||||
if ($null -eq $data) {
|
||||
$data = ""
|
||||
}
|
||||
} elseif ($type -eq "multistring") {
|
||||
# convert the data for a multistring to a String[] array
|
||||
if ($null -eq $data) {
|
||||
$data = [String[]]@()
|
||||
} elseif ($data -isnot [Array]) {
|
||||
$new_data = New-Object -TypeName String[] -ArgumentList 1
|
||||
$new_data[0] = $data.ToString([CultureInfo]::InvariantCulture)
|
||||
$data = $new_data
|
||||
} else {
|
||||
$new_data = New-Object -TypeName String[] -ArgumentList $data.Count
|
||||
foreach ($entry in $data) {
|
||||
$new_data[$data.IndexOf($entry)] = $entry.ToString([CultureInfo]::InvariantCulture)
|
||||
}
|
||||
$data = $new_data
|
||||
}
|
||||
}
|
||||
|
||||
# convert the type string to the .NET class
|
||||
$type = [System.Enum]::Parse([Microsoft.Win32.RegistryValueKind], $type, $true)
|
||||
|
||||
$registry_hive = switch(Split-Path -Path $path -Qualifier) {
|
||||
"HKCR:" { [Microsoft.Win32.Registry]::ClassesRoot }
|
||||
"HKCC:" { [Microsoft.Win32.Registry]::CurrentConfig }
|
||||
"HKCU:" { [Microsoft.Win32.Registry]::CurrentUser }
|
||||
"HKLM:" { [Microsoft.Win32.Registry]::LocalMachine }
|
||||
"HKU:" { [Microsoft.Win32.Registry]::Users }
|
||||
}
|
||||
$loaded_hive = $null
|
||||
try {
|
||||
if ($hive) {
|
||||
if (-not (Test-Path -LiteralPath $hive)) {
|
||||
Fail-Json -obj $result -message "hive at path '$hive' is not valid or accessible, cannot load hive"
|
||||
}
|
||||
|
||||
$original_tmp = $env:TMP
|
||||
$env:TMP = $_remote_tmp
|
||||
Add-Type -TypeDefinition $registry_util
|
||||
$env:TMP = $original_tmp
|
||||
|
||||
try {
|
||||
Set-AnsiblePrivilege -Name SeBackupPrivilege -Value $true
|
||||
Set-AnsiblePrivilege -Name SeRestorePrivilege -Value $true
|
||||
} catch [System.ComponentModel.Win32Exception] {
|
||||
Fail-Json -obj $result -message "failed to enable SeBackupPrivilege and SeRestorePrivilege for the current process: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
if (Test-Path -Path HKLM:\ANSIBLE) {
|
||||
Add-Warning -obj $result -message "hive already loaded at HKLM:\ANSIBLE, had to unload hive for win_regedit to continue"
|
||||
try {
|
||||
[Ansible.WinRegedit.Hive]::UnloadHive("ANSIBLE")
|
||||
} catch [System.ComponentModel.Win32Exception] {
|
||||
Fail-Json -obj $result -message "failed to unload registry hive HKLM:\ANSIBLE from $($hive): $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$loaded_hive = New-Object -TypeName Ansible.WinRegedit.Hive -ArgumentList "ANSIBLE", $hive
|
||||
} catch [System.ComponentModel.Win32Exception] {
|
||||
Fail-Json -obj $result -message "failed to load registry hive from '$hive' to HKLM:\ANSIBLE: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
if ($state -eq "present") {
|
||||
Set-StatePresent -PrintPath $path -Hive $registry_hive -Path $registry_path -Name $name -Data $data -Type $type
|
||||
} else {
|
||||
Set-StateAbsent -PrintPath $path -Hive $registry_hive -Path $registry_path -Name $name -DeleteKey:$delete_key
|
||||
}
|
||||
} finally {
|
||||
$registry_hive.Dispose()
|
||||
if ($loaded_hive) {
|
||||
$loaded_hive.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
Exit-Json $result
|
||||
|
||||
@ -1,210 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2015, Adam Keech <akeech@chathamfinancial.com>
|
||||
# Copyright: (c) 2015, Josh Ludwig <jludwig@chathamfinancial.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
# this is a windows documentation stub. actual code lives in the .ps1
|
||||
# file of the same name
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_regedit
|
||||
version_added: '2.0'
|
||||
short_description: Add, change, or remove registry keys and values
|
||||
description:
|
||||
- Add, modify or remove registry keys and values.
|
||||
- More information about the windows registry from Wikipedia
|
||||
U(https://en.wikipedia.org/wiki/Windows_Registry).
|
||||
options:
|
||||
path:
|
||||
description:
|
||||
- Name of the registry path.
|
||||
- 'Should be in one of the following registry hives: HKCC, HKCR, HKCU,
|
||||
HKLM, HKU.'
|
||||
type: str
|
||||
required: yes
|
||||
aliases: [ key ]
|
||||
name:
|
||||
description:
|
||||
- Name of the registry entry in the above C(path) parameters.
|
||||
- If not provided, or empty then the '(Default)' property for the key will
|
||||
be used.
|
||||
type: str
|
||||
aliases: [ entry, value ]
|
||||
data:
|
||||
description:
|
||||
- Value of the registry entry C(name) in C(path).
|
||||
- If not specified then the value for the property will be null for the
|
||||
corresponding C(type).
|
||||
- Binary and None data should be expressed in a yaml byte array or as comma
|
||||
separated hex values.
|
||||
- An easy way to generate this is to run C(regedit.exe) and use the
|
||||
I(export) option to save the registry values to a file.
|
||||
- In the exported file, binary value will look like C(hex:be,ef,be,ef), the
|
||||
C(hex:) prefix is optional.
|
||||
- DWORD and QWORD values should either be represented as a decimal number
|
||||
or a hex value.
|
||||
- Multistring values should be passed in as a list.
|
||||
- See the examples for more details on how to format this data.
|
||||
type: str
|
||||
type:
|
||||
description:
|
||||
- The registry value data type.
|
||||
type: str
|
||||
choices: [ binary, dword, expandstring, multistring, string, qword ]
|
||||
default: string
|
||||
aliases: [ datatype ]
|
||||
state:
|
||||
description:
|
||||
- The state of the registry entry.
|
||||
type: str
|
||||
choices: [ absent, present ]
|
||||
default: present
|
||||
delete_key:
|
||||
description:
|
||||
- When C(state) is 'absent' then this will delete the entire key.
|
||||
- If C(no) then it will only clear out the '(Default)' property for
|
||||
that key.
|
||||
type: bool
|
||||
default: yes
|
||||
version_added: '2.4'
|
||||
hive:
|
||||
description:
|
||||
- A path to a hive key like C:\Users\Default\NTUSER.DAT to load in the
|
||||
registry.
|
||||
- This hive is loaded under the HKLM:\ANSIBLE key which can then be used
|
||||
in I(name) like any other path.
|
||||
- This can be used to load the default user profile registry hive or any
|
||||
other hive saved as a file.
|
||||
- Using this function requires the user to have the C(SeRestorePrivilege)
|
||||
and C(SeBackupPrivilege) privileges enabled.
|
||||
type: path
|
||||
version_added: '2.5'
|
||||
notes:
|
||||
- Check-mode C(-C/--check) and diff output C(-D/--diff) are supported, so that you can test every change against the active configuration before
|
||||
applying changes.
|
||||
- Beware that some registry hives (C(HKEY_USERS) in particular) do not allow to create new registry paths in the root folder.
|
||||
- Since ansible 2.4, when checking if a string registry value has changed, a case-sensitive test is used. Previously the test was case-insensitive.
|
||||
seealso:
|
||||
- module: win_reg_stat
|
||||
- module: win_regmerge
|
||||
author:
|
||||
- Adam Keech (@smadam813)
|
||||
- Josh Ludwig (@joshludwig)
|
||||
- Jordan Borean (@jborean93)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Create registry path MyCompany
|
||||
win_regedit:
|
||||
path: HKCU:\Software\MyCompany
|
||||
|
||||
- name: Add or update registry path MyCompany, with entry 'hello', and containing 'world'
|
||||
win_regedit:
|
||||
path: HKCU:\Software\MyCompany
|
||||
name: hello
|
||||
data: world
|
||||
|
||||
- name: Add or update registry path MyCompany, with dword entry 'hello', and containing 1337 as the decimal value
|
||||
win_regedit:
|
||||
path: HKCU:\Software\MyCompany
|
||||
name: hello
|
||||
data: 1337
|
||||
type: dword
|
||||
|
||||
- name: Add or update registry path MyCompany, with dword entry 'hello', and containing 0xff2500ae as the hex value
|
||||
win_regedit:
|
||||
path: HKCU:\Software\MyCompany
|
||||
name: hello
|
||||
data: 0xff2500ae
|
||||
type: dword
|
||||
|
||||
- name: Add or update registry path MyCompany, with binary entry 'hello', and containing binary data in hex-string format
|
||||
win_regedit:
|
||||
path: HKCU:\Software\MyCompany
|
||||
name: hello
|
||||
data: hex:be,ef,be,ef,be,ef,be,ef,be,ef
|
||||
type: binary
|
||||
|
||||
- name: Add or update registry path MyCompany, with binary entry 'hello', and containing binary data in yaml format
|
||||
win_regedit:
|
||||
path: HKCU:\Software\MyCompany
|
||||
name: hello
|
||||
data: [0xbe,0xef,0xbe,0xef,0xbe,0xef,0xbe,0xef,0xbe,0xef]
|
||||
type: binary
|
||||
|
||||
- name: Add or update registry path MyCompany, with expand string entry 'hello'
|
||||
win_regedit:
|
||||
path: HKCU:\Software\MyCompany
|
||||
name: hello
|
||||
data: '%appdata%\local'
|
||||
type: expandstring
|
||||
|
||||
- name: Add or update registry path MyCompany, with multi string entry 'hello'
|
||||
win_regedit:
|
||||
path: HKCU:\Software\MyCompany
|
||||
name: hello
|
||||
data: ['hello', 'world']
|
||||
type: multistring
|
||||
|
||||
- name: Disable keyboard layout hotkey for all users (changes existing)
|
||||
win_regedit:
|
||||
path: HKU:\.DEFAULT\Keyboard Layout\Toggle
|
||||
name: Layout Hotkey
|
||||
data: 3
|
||||
type: dword
|
||||
|
||||
- name: Disable language hotkey for current users (adds new)
|
||||
win_regedit:
|
||||
path: HKCU:\Keyboard Layout\Toggle
|
||||
name: Language Hotkey
|
||||
data: 3
|
||||
type: dword
|
||||
|
||||
- name: Remove registry path MyCompany (including all entries it contains)
|
||||
win_regedit:
|
||||
path: HKCU:\Software\MyCompany
|
||||
state: absent
|
||||
delete_key: yes
|
||||
|
||||
- name: Clear the existing (Default) entry at path MyCompany
|
||||
win_regedit:
|
||||
path: HKCU:\Software\MyCompany
|
||||
state: absent
|
||||
delete_key: no
|
||||
|
||||
- name: Remove entry 'hello' from registry path MyCompany
|
||||
win_regedit:
|
||||
path: HKCU:\Software\MyCompany
|
||||
name: hello
|
||||
state: absent
|
||||
|
||||
- name: Change default mouse trailing settings for new users
|
||||
win_regedit:
|
||||
path: HKLM:\ANSIBLE\Control Panel\Mouse
|
||||
name: MouseTrails
|
||||
data: 10
|
||||
type: str
|
||||
state: present
|
||||
hive: C:\Users\Default\NTUSER.dat
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
data_changed:
|
||||
description: Whether this invocation changed the data in the registry value.
|
||||
returned: success
|
||||
type: bool
|
||||
sample: false
|
||||
data_type_changed:
|
||||
description: Whether this invocation changed the datatype of the registry value.
|
||||
returned: success
|
||||
type: bool
|
||||
sample: true
|
||||
'''
|
||||
@ -1,463 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2014, Chris Hoffman <choffman@chathamfinancial.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
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$params = Parse-Args $args -supports_check_mode $true
|
||||
$check_mode = Get-AnsibleParam -obj $params -name '_ansible_check_mode' -type 'bool' -default $false
|
||||
|
||||
$dependencies = Get-AnsibleParam -obj $params -name 'dependencies' -type 'list' -default $null
|
||||
$dependency_action = Get-AnsibleParam -obj $params -name 'dependency_action' -type 'str' -default 'set' -validateset 'add','remove','set'
|
||||
$description = Get-AnsibleParam -obj $params -name 'description' -type 'str'
|
||||
$desktop_interact = Get-AnsibleParam -obj $params -name 'desktop_interact' -type 'bool' -default $false
|
||||
$display_name = Get-AnsibleParam -obj $params -name 'display_name' -type 'str'
|
||||
$force_dependent_services = Get-AnsibleParam -obj $params -name 'force_dependent_services' -type 'bool' -default $false
|
||||
$name = Get-AnsibleParam -obj $params -name 'name' -type 'str' -failifempty $true
|
||||
$password = Get-AnsibleParam -obj $params -name 'password' -type 'str'
|
||||
$path = Get-AnsibleParam -obj $params -name 'path'
|
||||
$start_mode = Get-AnsibleParam -obj $params -name 'start_mode' -type 'str' -validateset 'auto','manual','disabled','delayed'
|
||||
$state = Get-AnsibleParam -obj $params -name 'state' -type 'str' -validateset 'started','stopped','restarted','absent','paused'
|
||||
$username = Get-AnsibleParam -obj $params -name 'username' -type 'str'
|
||||
|
||||
$result = @{
|
||||
changed = $false
|
||||
}
|
||||
|
||||
# parse the username to SID and back so we get the full username with domain in a way WMI understands
|
||||
if ($null -ne $username) {
|
||||
if ($username -eq "LocalSystem") {
|
||||
$username_sid = "S-1-5-18"
|
||||
} else {
|
||||
$username_sid = Convert-ToSID -account_name $username
|
||||
}
|
||||
|
||||
# the SYSTEM account is a special beast, Win32_Service Change requires StartName to be LocalSystem
|
||||
# to specify LocalSystem/NT AUTHORITY\SYSTEM
|
||||
if ($username_sid -eq "S-1-5-18") {
|
||||
$username = "LocalSystem"
|
||||
$password = $null
|
||||
} else {
|
||||
# Win32_Service, password must be "" and not $null when setting to LocalService or NetworkService
|
||||
if ($username_sid -in @("S-1-5-19", "S-1-5-20")) {
|
||||
$password = ""
|
||||
}
|
||||
$username = Convert-FromSID -sid $username_sid
|
||||
}
|
||||
}
|
||||
if ($null -ne $password -and $null -eq $username) {
|
||||
Fail-Json $result "The argument 'username' must be supplied with 'password'"
|
||||
}
|
||||
if ($desktop_interact -eq $true -and (-not ($username -eq "LocalSystem" -or $null -eq $username))) {
|
||||
Fail-Json $result "Can only set 'desktop_interact' to true when 'username' equals 'LocalSystem'"
|
||||
}
|
||||
if ($null -ne $path) {
|
||||
$path = [System.Environment]::ExpandEnvironmentVariables($path)
|
||||
}
|
||||
|
||||
Function Get-ServiceInfo($name) {
|
||||
# Need to get new objects so we have the latest info
|
||||
$svc = Get-Service | Where-Object { $_.Name -eq $name -or $_.DisplayName -eq $name }
|
||||
$wmi_svc = Get-CimInstance -ClassName Win32_Service -Filter "name='$($svc.Name)'"
|
||||
|
||||
# Delayed start_mode is in reality Automatic (Delayed), need to check reg key for type
|
||||
$delayed = Get-DelayedStatus -name $svc.Name
|
||||
$actual_start_mode = $wmi_svc.StartMode.ToString().ToLower()
|
||||
if ($delayed -and $actual_start_mode -eq 'auto') {
|
||||
$actual_start_mode = 'delayed'
|
||||
}
|
||||
|
||||
$existing_dependencies = @()
|
||||
$existing_depended_by = @()
|
||||
if ($svc.ServicesDependedOn.Count -gt 0) {
|
||||
foreach ($dependency in $svc.ServicesDependedOn.Name) {
|
||||
$existing_dependencies += $dependency
|
||||
}
|
||||
}
|
||||
if ($svc.DependentServices.Count -gt 0) {
|
||||
foreach ($dependency in $svc.DependentServices.Name) {
|
||||
$existing_depended_by += $dependency
|
||||
}
|
||||
}
|
||||
$description = $wmi_svc.Description
|
||||
if ($null -eq $description) {
|
||||
$description = ""
|
||||
}
|
||||
|
||||
$result.exists = $true
|
||||
$result.name = $svc.Name
|
||||
$result.display_name = $svc.DisplayName
|
||||
$result.state = $svc.Status.ToString().ToLower()
|
||||
$result.start_mode = $actual_start_mode
|
||||
$result.path = $wmi_svc.PathName
|
||||
$result.description = $description
|
||||
$result.username = $wmi_svc.StartName
|
||||
$result.desktop_interact = $wmi_svc.DesktopInteract
|
||||
$result.dependencies = $existing_dependencies
|
||||
$result.depended_by = $existing_depended_by
|
||||
$result.can_pause_and_continue = $svc.CanPauseAndContinue
|
||||
}
|
||||
|
||||
Function Get-WmiErrorMessage($return_value) {
|
||||
# These values are derived from https://msdn.microsoft.com/en-us/library/aa384901(v=vs.85).aspx
|
||||
switch ($return_value) {
|
||||
1 { "Not Supported: The request is not supported" }
|
||||
2 { "Access Denied: The user did not have the necessary access" }
|
||||
3 { "Dependent Services Running: The service cannot be stopped because other services that are running are dependent on it" }
|
||||
4 { "Invalid Service Control: The requested control code is not valid, or it is unacceptable to the service" }
|
||||
5 { "Service Cannot Accept Control: The requested control code cannot be sent to the service because the state of the service (Win32_BaseService.State property) is equal to 0, 1, or 2" }
|
||||
6 { "Service Not Active: The service has not been started" }
|
||||
7 { "Service Request Timeout: The service did not respond to the start request in a timely fashion" }
|
||||
8 { "Unknown Failure: Unknown failure when starting the service" }
|
||||
9 { "Path Not Found: The directory path to the service executable file was not found" }
|
||||
10 { "Service Already Running: The service is already running" }
|
||||
11 { "Service Database Locked: The database to add a new service is locked" }
|
||||
12 { "Service Dependency Deleted: A dependency this service relies on has been removed from the system" }
|
||||
13 { "Service Dependency Failure: The service failed to find the service needed from a dependent service" }
|
||||
14 { "Service Disabled: The service has been disabled from the system" }
|
||||
15 { "Service Logon Failed: The service does not have the correct authentication to run on the system" }
|
||||
16 { "Service Marked For Deletion: This service is being removed from the system" }
|
||||
17 { "Service No Thread: The service has no execution thread" }
|
||||
18 { "Status Circular Dependency: The service has circular dependencies when it starts" }
|
||||
19 { "Status Duplicate Name: A service is running under the same name" }
|
||||
20 { "Status Invalid Name: The service name has invalid characters" }
|
||||
21 { "Status Invalid Parameter: Invalid parameters have been passed to the service" }
|
||||
22 { "Status Invalid Service Account: The account under which this service runs is either invalid or lacks the permissions to run the service" }
|
||||
23 { "Status Service Exists: The service exists in the database of services available from the system" }
|
||||
24 { "Service Already Paused: The service is currently paused in the system" }
|
||||
default { "Other Error" }
|
||||
}
|
||||
}
|
||||
|
||||
Function Get-DelayedStatus($name) {
|
||||
$delayed_key = "HKLM:\System\CurrentControlSet\Services\$name"
|
||||
try {
|
||||
$delayed = ConvertTo-Bool ((Get-ItemProperty -LiteralPath $delayed_key).DelayedAutostart)
|
||||
} catch {
|
||||
$delayed = $false
|
||||
}
|
||||
|
||||
$delayed
|
||||
}
|
||||
|
||||
Function Set-ServiceStartMode($svc, $start_mode) {
|
||||
if ($result.start_mode -ne $start_mode) {
|
||||
try {
|
||||
$delayed_key = "HKLM:\System\CurrentControlSet\Services\$($svc.Name)"
|
||||
# Original start up type was auto (delayed) and we want auto, need to removed delayed key
|
||||
if ($start_mode -eq 'auto' -and $result.start_mode -eq 'delayed') {
|
||||
Set-ItemProperty -LiteralPath $delayed_key -Name "DelayedAutostart" -Value 0 -WhatIf:$check_mode
|
||||
# Original start up type was auto and we want auto (delayed), need to add delayed key
|
||||
} elseif ($start_mode -eq 'delayed' -and $result.start_mode -eq 'auto') {
|
||||
Set-ItemProperty -LiteralPath $delayed_key -Name "DelayedAutostart" -Value 1 -WhatIf:$check_mode
|
||||
# Original start up type was not auto or auto (delayed), need to change to auto and add delayed key
|
||||
} elseif ($start_mode -eq 'delayed') {
|
||||
$svc | Set-Service -StartupType "auto" -WhatIf:$check_mode
|
||||
Set-ItemProperty -LiteralPath $delayed_key -Name "DelayedAutostart" -Value 1 -WhatIf:$check_mode
|
||||
# Original start up type was not what we were looking for, just change to that type
|
||||
} else {
|
||||
$svc | Set-Service -StartupType $start_mode -WhatIf:$check_mode
|
||||
}
|
||||
} catch {
|
||||
Fail-Json $result $_.Exception.Message
|
||||
}
|
||||
|
||||
$result.changed = $true
|
||||
}
|
||||
}
|
||||
|
||||
Function Set-ServiceAccount($wmi_svc, $username_sid, $username, $password) {
|
||||
if ($result.username -eq "LocalSystem") {
|
||||
$actual_sid = "S-1-5-18"
|
||||
} else {
|
||||
$actual_sid = Convert-ToSID -account_name $result.username
|
||||
}
|
||||
|
||||
if ($actual_sid -ne $username_sid) {
|
||||
$change_arguments = @{
|
||||
StartName = $username
|
||||
StartPassword = $password
|
||||
DesktopInteract = $result.desktop_interact
|
||||
}
|
||||
# need to disable desktop interact when not using the SYSTEM account
|
||||
if ($username_sid -ne "S-1-5-18") {
|
||||
$change_arguments.DesktopInteract = $false
|
||||
}
|
||||
|
||||
#WMI.Change doesn't support -WhatIf, cannot fully test with check_mode
|
||||
if (-not $check_mode) {
|
||||
$return = $wmi_svc | Invoke-CimMethod -MethodName Change -Arguments $change_arguments
|
||||
if ($return.ReturnValue -ne 0) {
|
||||
$error_msg = Get-WmiErrorMessage -return_value $result.ReturnValue
|
||||
Fail-Json -obj $result -message "Failed to set service account to $($username): $($return.ReturnValue) - $error_msg"
|
||||
}
|
||||
}
|
||||
|
||||
$result.changed = $true
|
||||
}
|
||||
}
|
||||
|
||||
Function Set-ServiceDesktopInteract($wmi_svc, $desktop_interact) {
|
||||
if ($result.desktop_interact -ne $desktop_interact) {
|
||||
if (-not $check_mode) {
|
||||
$return = $wmi_svc | Invoke-CimMethod -MethodName Change -Arguments @{DesktopInteract = $desktop_interact}
|
||||
if ($return.ReturnValue -ne 0) {
|
||||
$error_msg = Get-WmiErrorMessage -return_value $return.ReturnValue
|
||||
Fail-Json -obj $result -message "Failed to set desktop interact $($desktop_interact): $($return.ReturnValue) - $error_msg"
|
||||
}
|
||||
}
|
||||
|
||||
$result.changed = $true
|
||||
}
|
||||
}
|
||||
|
||||
Function Set-ServiceDisplayName($svc, $display_name) {
|
||||
if ($result.display_name -ne $display_name) {
|
||||
try {
|
||||
$svc | Set-Service -DisplayName $display_name -WhatIf:$check_mode
|
||||
} catch {
|
||||
Fail-Json $result $_.Exception.Message
|
||||
}
|
||||
|
||||
$result.changed = $true
|
||||
}
|
||||
}
|
||||
|
||||
Function Set-ServiceDescription($svc, $description) {
|
||||
if ($result.description -ne $description) {
|
||||
try {
|
||||
$svc | Set-Service -Description $description -WhatIf:$check_mode
|
||||
} catch {
|
||||
Fail-Json $result $_.Exception.Message
|
||||
}
|
||||
|
||||
$result.changed = $true
|
||||
}
|
||||
}
|
||||
|
||||
Function Set-ServicePath($name, $path) {
|
||||
if ($result.path -ne $path) {
|
||||
try {
|
||||
Set-ItemProperty -LiteralPath "HKLM:\System\CurrentControlSet\Services\$name" -Name ImagePath -Value $path -WhatIf:$check_mode
|
||||
} catch {
|
||||
Fail-Json $result $_.Exception.Message
|
||||
}
|
||||
|
||||
$result.changed = $true
|
||||
}
|
||||
}
|
||||
|
||||
Function Set-ServiceDependencies($wmi_svc, $dependency_action, $dependencies) {
|
||||
$existing_dependencies = $result.dependencies
|
||||
[System.Collections.ArrayList]$new_dependencies = @()
|
||||
|
||||
if ($dependency_action -eq 'set') {
|
||||
foreach ($dependency in $dependencies) {
|
||||
$new_dependencies.Add($dependency)
|
||||
}
|
||||
} else {
|
||||
$new_dependencies = $existing_dependencies
|
||||
foreach ($dependency in $dependencies) {
|
||||
if ($dependency_action -eq 'remove') {
|
||||
if ($new_dependencies -contains $dependency) {
|
||||
$new_dependencies.Remove($dependency)
|
||||
}
|
||||
} elseif ($dependency_action -eq 'add') {
|
||||
if ($new_dependencies -notcontains $dependency) {
|
||||
$new_dependencies.Add($dependency)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$will_change = $false
|
||||
foreach ($dependency in $new_dependencies) {
|
||||
if ($existing_dependencies -notcontains $dependency) {
|
||||
$will_change = $true
|
||||
}
|
||||
}
|
||||
foreach ($dependency in $existing_dependencies) {
|
||||
if ($new_dependencies -notcontains $dependency) {
|
||||
$will_change = $true
|
||||
}
|
||||
}
|
||||
|
||||
if ($will_change -eq $true) {
|
||||
if (-not $check_mode) {
|
||||
$return = $wmi_svc | Invoke-CimMethod -MethodName Change -Arguments @{ServiceDependencies = $new_dependencies}
|
||||
if ($return.ReturnValue -ne 0) {
|
||||
$error_msg = Get-WmiErrorMessage -return_value $return.ReturnValue
|
||||
$dep_string = $new_dependencies -join ", "
|
||||
Fail-Json -obj $result -message "Failed to set service dependencies $($dep_string): $($return.ReturnValue) - $error_msg"
|
||||
}
|
||||
}
|
||||
|
||||
$result.changed = $true
|
||||
}
|
||||
}
|
||||
|
||||
Function Set-ServiceState($svc, $wmi_svc, $state) {
|
||||
if ($state -eq "started" -and $result.state -ne "running") {
|
||||
if ($result.state -eq "paused") {
|
||||
try {
|
||||
$svc | Resume-Service -WhatIf:$check_mode
|
||||
} catch {
|
||||
Fail-Json $result "failed to start service from paused state $($svc.Name): $($_.Exception.Message)"
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$svc | Start-Service -WhatIf:$check_mode
|
||||
} catch {
|
||||
Fail-Json $result $_.Exception.Message
|
||||
}
|
||||
}
|
||||
|
||||
$result.changed = $true
|
||||
}
|
||||
|
||||
if ($state -eq "stopped" -and $result.state -ne "stopped") {
|
||||
try {
|
||||
$svc | Stop-Service -Force:$force_dependent_services -WhatIf:$check_mode
|
||||
} catch {
|
||||
Fail-Json $result $_.Exception.Message
|
||||
}
|
||||
|
||||
$result.changed = $true
|
||||
}
|
||||
|
||||
if ($state -eq "restarted") {
|
||||
try {
|
||||
$svc | Restart-Service -Force:$force_dependent_services -WhatIf:$check_mode
|
||||
} catch {
|
||||
Fail-Json $result $_.Exception.Message
|
||||
}
|
||||
|
||||
$result.changed = $true
|
||||
}
|
||||
|
||||
if ($state -eq "paused" -and $result.state -ne "paused") {
|
||||
# check that we can actually pause the service
|
||||
if ($result.can_pause_and_continue -eq $false) {
|
||||
Fail-Json $result "failed to pause service $($svc.Name): The service does not support pausing"
|
||||
}
|
||||
|
||||
try {
|
||||
$svc | Suspend-Service -WhatIf:$check_mode
|
||||
} catch {
|
||||
Fail-Json $result "failed to pause service $($svc.Name): $($_.Exception.Message)"
|
||||
}
|
||||
$result.changed = $true
|
||||
}
|
||||
|
||||
if ($state -eq "absent") {
|
||||
try {
|
||||
$svc | Stop-Service -Force:$force_dependent_services -WhatIf:$check_mode
|
||||
} catch {
|
||||
Fail-Json $result $_.Exception.Message
|
||||
}
|
||||
if (-not $check_mode) {
|
||||
$return = $wmi_svc | Invoke-CimMethod -MethodName Delete
|
||||
if ($return.ReturnValue -ne 0) {
|
||||
$error_msg = Get-WmiErrorMessage -return_value $return.ReturnValue
|
||||
Fail-Json -obj $result -message "Failed to delete service $($svc.Name): $($return.ReturnValue) - $error_msg"
|
||||
}
|
||||
}
|
||||
|
||||
$result.changed = $true
|
||||
}
|
||||
}
|
||||
|
||||
Function Set-ServiceConfiguration($svc) {
|
||||
$wmi_svc = Get-CimInstance -ClassName Win32_Service -Filter "name='$($svc.Name)'"
|
||||
Get-ServiceInfo -name $svc.Name
|
||||
if ($desktop_interact -eq $true -and (-not ($result.username -eq 'LocalSystem' -or $username -eq 'LocalSystem'))) {
|
||||
Fail-Json $result "Can only set desktop_interact to true when service is run with/or 'username' equals 'LocalSystem'"
|
||||
}
|
||||
|
||||
if ($null -ne $start_mode) {
|
||||
Set-ServiceStartMode -svc $svc -start_mode $start_mode
|
||||
}
|
||||
|
||||
if ($null -ne $username) {
|
||||
Set-ServiceAccount -wmi_svc $wmi_svc -username_sid $username_sid -username $username -password $password
|
||||
}
|
||||
|
||||
if ($null -ne $display_name) {
|
||||
Set-ServiceDisplayName -svc $svc -display_name $display_name
|
||||
}
|
||||
|
||||
if ($null -ne $desktop_interact) {
|
||||
Set-ServiceDesktopInteract -wmi_svc $wmi_svc -desktop_interact $desktop_interact
|
||||
}
|
||||
|
||||
if ($null -ne $description) {
|
||||
Set-ServiceDescription -svc $svc -description $description
|
||||
}
|
||||
|
||||
if ($null -ne $path) {
|
||||
Set-ServicePath -name $svc.Name -path $path
|
||||
}
|
||||
|
||||
if ($null -ne $dependencies) {
|
||||
Set-ServiceDependencies -wmi_svc $wmi_svc -dependency_action $dependency_action -dependencies $dependencies
|
||||
}
|
||||
|
||||
if ($null -ne $state) {
|
||||
Set-ServiceState -svc $svc -wmi_svc $wmi_svc -state $state
|
||||
}
|
||||
}
|
||||
|
||||
# need to use Where-Object as -Name doesn't work with [] in the service name
|
||||
# https://github.com/ansible/ansible/issues/37621
|
||||
$svc = Get-Service | Where-Object { $_.Name -eq $name -or $_.DisplayName -eq $name }
|
||||
if ($svc) {
|
||||
Set-ServiceConfiguration -svc $svc
|
||||
} else {
|
||||
$result.exists = $false
|
||||
if ($state -ne 'absent') {
|
||||
# Check if path is defined, if so create the service
|
||||
if ($null -ne $path) {
|
||||
try {
|
||||
New-Service -Name $name -BinaryPathname $path -WhatIf:$check_mode
|
||||
} catch {
|
||||
Fail-Json $result $_.Exception.Message
|
||||
}
|
||||
$result.changed = $true
|
||||
|
||||
$svc = Get-Service | Where-Object { $_.Name -eq $name }
|
||||
Set-ServiceConfiguration -svc $svc
|
||||
} else {
|
||||
# We will only reach here if the service is installed and the state is not absent
|
||||
# Will check if any of the default actions are set and fail as we cannot action it
|
||||
if ($null -ne $start_mode -or
|
||||
$null -ne $state -or
|
||||
$null -ne $username -or
|
||||
$null -ne $password -or
|
||||
$null -ne $display_name -or
|
||||
$null -ne $description -or
|
||||
$desktop_interact -ne $false -or
|
||||
$null -ne $dependencies -or
|
||||
$dependency_action -ne 'set') {
|
||||
Fail-Json $result "Service '$name' is not installed, need to set 'path' to create a new service"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# After making a change, let's get the service info again unless we deleted it
|
||||
if ($state -eq 'absent') {
|
||||
# Recreate result so it doesn't have the extra meta data now that is has been deleted
|
||||
$changed = $result.changed
|
||||
$result = @{
|
||||
changed = $changed
|
||||
exists = $false
|
||||
}
|
||||
} elseif ($null -ne $svc) {
|
||||
Get-ServiceInfo -name $name
|
||||
}
|
||||
|
||||
Exit-Json -obj $result
|
||||
@ -1,307 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2014, Chris Hoffman <choffman@chathamfinancial.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['stableinterface'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_service
|
||||
version_added: '1.7'
|
||||
short_description: Manage and query Windows services
|
||||
description:
|
||||
- Manage and query Windows services.
|
||||
- For non-Windows targets, use the M(service) module instead.
|
||||
options:
|
||||
dependencies:
|
||||
description:
|
||||
- A list of service dependencies to set for this particular service.
|
||||
- This should be a list of service names and not the display name of the
|
||||
service.
|
||||
- This works by C(dependency_action) to either add/remove or set the
|
||||
services in this list.
|
||||
type: list
|
||||
version_added: '2.3'
|
||||
dependency_action:
|
||||
description:
|
||||
- Used in conjunction with C(dependency) to either add the dependencies to
|
||||
the existing service dependencies.
|
||||
- Remove the dependencies to the existing dependencies.
|
||||
- Set the dependencies to only the values in the list replacing the
|
||||
existing dependencies.
|
||||
type: str
|
||||
choices: [ add, remove, set ]
|
||||
default: set
|
||||
version_added: '2.3'
|
||||
desktop_interact:
|
||||
description:
|
||||
- Whether to allow the service user to interact with the desktop.
|
||||
- This should only be set to C(yes) when using the C(LocalSystem) username.
|
||||
type: bool
|
||||
default: no
|
||||
version_added: '2.3'
|
||||
description:
|
||||
description:
|
||||
- The description to set for the service.
|
||||
type: str
|
||||
version_added: '2.3'
|
||||
display_name:
|
||||
description:
|
||||
- The display name to set for the service.
|
||||
type: str
|
||||
version_added: '2.3'
|
||||
force_dependent_services:
|
||||
description:
|
||||
- If C(yes), stopping or restarting a service with dependent services will
|
||||
force the dependent services to stop or restart also.
|
||||
- If C(no), stopping or restarting a service with dependent services may
|
||||
fail.
|
||||
type: bool
|
||||
default: no
|
||||
version_added: '2.3'
|
||||
name:
|
||||
description:
|
||||
- Name of the service.
|
||||
- If only the name parameter is specified, the module will report
|
||||
on whether the service exists or not without making any changes.
|
||||
required: yes
|
||||
type: str
|
||||
path:
|
||||
description:
|
||||
- The path to the executable to set for the service.
|
||||
type: str
|
||||
version_added: '2.3'
|
||||
password:
|
||||
description:
|
||||
- The password to set the service to start as.
|
||||
- This and the C(username) argument must be supplied together.
|
||||
- If specifying C(LocalSystem), C(NetworkService) or C(LocalService) this field
|
||||
must be an empty string and not null.
|
||||
type: str
|
||||
version_added: '2.3'
|
||||
start_mode:
|
||||
description:
|
||||
- Set the startup type for the service.
|
||||
- A newly created service will default to C(auto).
|
||||
- C(delayed) added in Ansible 2.3
|
||||
type: str
|
||||
choices: [ auto, delayed, disabled, manual ]
|
||||
state:
|
||||
description:
|
||||
- The desired state of the service.
|
||||
- C(started)/C(stopped)/C(absent)/C(paused) are idempotent actions that will not run
|
||||
commands unless necessary.
|
||||
- C(restarted) will always bounce the service.
|
||||
- C(absent) was added in Ansible 2.3
|
||||
- C(paused) was added in Ansible 2.4
|
||||
- Only services that support the paused state can be paused, you can
|
||||
check the return value C(can_pause_and_continue).
|
||||
- You can only pause a service that is already started.
|
||||
- A newly created service will default to C(stopped).
|
||||
type: str
|
||||
choices: [ absent, paused, started, stopped, restarted ]
|
||||
username:
|
||||
description:
|
||||
- The username to set the service to start as.
|
||||
- This and the C(password) argument must be supplied together when using
|
||||
a local or domain account.
|
||||
- Set to C(LocalSystem) to use the SYSTEM account.
|
||||
- A newly created service will default to C(LocalSystem).
|
||||
- If using a custom user account, it must have the C(SeServiceLogonRight)
|
||||
granted to be able to start up. You can use the M(win_user_right) module
|
||||
to grant this user right for you.
|
||||
type: str
|
||||
version_added: '2.3'
|
||||
seealso:
|
||||
- module: service
|
||||
- module: win_nssm
|
||||
- module: win_user_right
|
||||
author:
|
||||
- Chris Hoffman (@chrishoffman)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Restart a service
|
||||
win_service:
|
||||
name: spooler
|
||||
state: restarted
|
||||
|
||||
- name: Set service startup mode to auto and ensure it is started
|
||||
win_service:
|
||||
name: spooler
|
||||
start_mode: auto
|
||||
state: started
|
||||
|
||||
- name: Pause a service
|
||||
win_service:
|
||||
name: Netlogon
|
||||
state: paused
|
||||
|
||||
- name: Ensure that WinRM is started when the system has settled
|
||||
win_service:
|
||||
name: WinRM
|
||||
start_mode: delayed
|
||||
|
||||
# A new service will also default to the following values:
|
||||
# - username: LocalSystem
|
||||
# - state: stopped
|
||||
# - start_mode: auto
|
||||
- name: Create a new service
|
||||
win_service:
|
||||
name: service name
|
||||
path: C:\temp\test.exe
|
||||
|
||||
- name: Create a new service with extra details
|
||||
win_service:
|
||||
name: service name
|
||||
path: C:\temp\test.exe
|
||||
display_name: Service Name
|
||||
description: A test service description
|
||||
|
||||
- name: Remove a service
|
||||
win_service:
|
||||
name: service name
|
||||
state: absent
|
||||
|
||||
- name: Check if a service is installed
|
||||
win_service:
|
||||
name: service name
|
||||
register: service_info
|
||||
|
||||
# This is required to be set for non-service accounts that need to run as a service
|
||||
- name: Grant domain account the SeServiceLogonRight user right
|
||||
win_user_right:
|
||||
name: SeServiceLogonRight
|
||||
users:
|
||||
- DOMAIN\User
|
||||
action: add
|
||||
|
||||
- name: Set the log on user to a domain account
|
||||
win_service:
|
||||
name: service name
|
||||
state: restarted
|
||||
username: DOMAIN\User
|
||||
password: Password
|
||||
|
||||
- name: Set the log on user to a local account
|
||||
win_service:
|
||||
name: service name
|
||||
state: restarted
|
||||
username: .\Administrator
|
||||
password: Password
|
||||
|
||||
- name: Set the log on user to Local System
|
||||
win_service:
|
||||
name: service name
|
||||
state: restarted
|
||||
username: LocalSystem
|
||||
password: ''
|
||||
|
||||
- name: Set the log on user to Local System and allow it to interact with the desktop
|
||||
win_service:
|
||||
name: service name
|
||||
state: restarted
|
||||
username: LocalSystem
|
||||
password: ""
|
||||
desktop_interact: yes
|
||||
|
||||
- name: Set the log on user to Network Service
|
||||
win_service:
|
||||
name: service name
|
||||
state: restarted
|
||||
username: NT AUTHORITY\NetworkService
|
||||
password: ''
|
||||
|
||||
- name: Set the log on user to Local Service
|
||||
win_service:
|
||||
name: service name
|
||||
state: restarted
|
||||
username: NT AUTHORITY\LocalService
|
||||
password: ''
|
||||
|
||||
- name: Set dependencies to ones only in the list
|
||||
win_service:
|
||||
name: service name
|
||||
dependencies: [ service1, service2 ]
|
||||
|
||||
- name: Add dependencies to existing dependencies
|
||||
win_service:
|
||||
name: service name
|
||||
dependencies: [ service1, service2 ]
|
||||
dependency_action: add
|
||||
|
||||
- name: Remove dependencies from existing dependencies
|
||||
win_service:
|
||||
name: service name
|
||||
dependencies:
|
||||
- service1
|
||||
- service2
|
||||
dependency_action: remove
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
exists:
|
||||
description: Whether the service exists or not.
|
||||
returned: success
|
||||
type: bool
|
||||
sample: true
|
||||
name:
|
||||
description: The service name or id of the service.
|
||||
returned: success and service exists
|
||||
type: str
|
||||
sample: CoreMessagingRegistrar
|
||||
display_name:
|
||||
description: The display name of the installed service.
|
||||
returned: success and service exists
|
||||
type: str
|
||||
sample: CoreMessaging
|
||||
state:
|
||||
description: The current running status of the service.
|
||||
returned: success and service exists
|
||||
type: str
|
||||
sample: stopped
|
||||
start_mode:
|
||||
description: The startup type of the service.
|
||||
returned: success and service exists
|
||||
type: str
|
||||
sample: manual
|
||||
path:
|
||||
description: The path to the service executable.
|
||||
returned: success and service exists
|
||||
type: str
|
||||
sample: C:\Windows\system32\svchost.exe -k LocalServiceNoNetwork
|
||||
can_pause_and_continue:
|
||||
description: Whether the service can be paused and unpaused.
|
||||
returned: success and service exists
|
||||
type: bool
|
||||
sample: true
|
||||
description:
|
||||
description: The description of the service.
|
||||
returned: success and service exists
|
||||
type: str
|
||||
sample: Manages communication between system components.
|
||||
username:
|
||||
description: The username that runs the service.
|
||||
returned: success and service exists
|
||||
type: str
|
||||
sample: LocalSystem
|
||||
desktop_interact:
|
||||
description: Whether the current user is allowed to interact with the desktop.
|
||||
returned: success and service exists
|
||||
type: bool
|
||||
sample: false
|
||||
dependencies:
|
||||
description: A list of services that is depended by this service.
|
||||
returned: success and service exists
|
||||
type: list
|
||||
sample: false
|
||||
depended_by:
|
||||
description: A list of services that depend on this service.
|
||||
returned: success and service exists
|
||||
type: list
|
||||
sample: false
|
||||
'''
|
||||
@ -1,207 +0,0 @@
|
||||
#!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
|
||||
#AnsibleRequires -CSharpUtil Ansible.Service
|
||||
|
||||
$spec = @{
|
||||
options = @{
|
||||
name = @{ type = "str" }
|
||||
}
|
||||
supports_check_mode = $true
|
||||
}
|
||||
|
||||
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||||
|
||||
$name = $module.Params.name
|
||||
|
||||
$module.Result.exists = $false
|
||||
$module.Result.services = @(foreach ($rawService in (Get-Service -Name $name -ErrorAction SilentlyContinue)) {
|
||||
try {
|
||||
$service = New-Object -TypeName Ansible.Service.Service -ArgumentList @(
|
||||
$rawService.Name, [Ansible.Service.ServiceRights]'EnumerateDependents, QueryConfig, QueryStatus'
|
||||
)
|
||||
} catch [Ansible.Service.ServiceManagerException] {
|
||||
# ERROR_ACCESS_DENIED, ignore the service and continue on.
|
||||
if ($_.Exception.InnerException -and $_.Exception.InnerException.NativeErrorCode -eq 5) {
|
||||
$module.Warn("Failed to access service '$($rawService.Name) to get more info, ignoring")
|
||||
continue
|
||||
}
|
||||
|
||||
throw
|
||||
}
|
||||
$module.Result.exists = $true
|
||||
|
||||
$controlsAccepted = @($service.ControlsAccepted.ToString() -split ',' | ForEach-Object -Process {
|
||||
switch ($_.Trim()) {
|
||||
Stop { 'stop' }
|
||||
PauseContinue { 'pause_continue' }
|
||||
Shutdown { 'shutdown' }
|
||||
ParamChange { 'param_change' }
|
||||
NetbindChange { 'netbind_change' }
|
||||
HardwareProfileChange { 'hardware_profile_change' }
|
||||
PowerEvent { 'power_event' }
|
||||
SessionChange { 'session_change' }
|
||||
PreShutdown { 'pre_shutdown' }
|
||||
}
|
||||
})
|
||||
|
||||
$rawFailureActions = $service.FailureActions
|
||||
$failureActions = @(foreach ($action in $rawFailureActions.Actions) {
|
||||
[Ordered]@{
|
||||
type = switch ($action.Type) {
|
||||
None { 'none' }
|
||||
Reboot { 'reboot' }
|
||||
Restart { 'restart' }
|
||||
RunCommand { 'run_command' }
|
||||
}
|
||||
delay_ms = $action.Delay
|
||||
}
|
||||
})
|
||||
|
||||
# LaunchProtection is only valid in Windows 8.1 (2012 R2) or above.
|
||||
$launchProtection = 'none'
|
||||
if ($service.LaunchProtection) {
|
||||
$launchProtection = switch ($service.LaunchProtection) {
|
||||
None { 'none' }
|
||||
Windows { 'windows' }
|
||||
WindowsLight { 'windows_light' }
|
||||
AntimalwareLight { 'antimalware_light' }
|
||||
}
|
||||
}
|
||||
|
||||
$serviceFlags = @($service.ServiceFlags.ToString() -split ',' | ForEach-Object -Process {
|
||||
switch ($_.Trim()) {
|
||||
RunsInSystemProcess { 'runs_in_system_process' }
|
||||
}
|
||||
})
|
||||
|
||||
# The ServiceType value can contain other flags which are represented by other properties, this strips them out
|
||||
# so we don't include them in the service_type return value.
|
||||
$serviceType = [uint32]$service.ServiceType -band -bnot [uint32][Ansible.Service.ServiceType]::InteractiveProcess
|
||||
$serviceType = $serviceType -band -bnot [uint32][Ansible.Service.ServiceType]::UserServiceInstance
|
||||
$serviceType = switch (([Ansible.Service.ServiceType]$serviceType).ToString()) {
|
||||
KernelDriver { 'kernel_driver' }
|
||||
FileSystemDriver { 'file_system_driver' }
|
||||
Adapter { 'adapter' }
|
||||
RecognizerDriver { 'recognizer_driver' }
|
||||
Win32OwnProcess { 'win32_own_process' }
|
||||
Win32ShareProcess { 'win32_share_process' }
|
||||
UserOwnprocess { 'user_own_process' }
|
||||
UserShareProcess { 'user_share_process' }
|
||||
PkgService { 'pkg_service' }
|
||||
}
|
||||
|
||||
$startType = switch ($service.StartType) {
|
||||
BootStart { 'boot_start' }
|
||||
SystemStart { 'system_start' }
|
||||
AutoStart { 'auto' }
|
||||
DemandStart { 'manual' }
|
||||
Disabled { 'disabled' }
|
||||
AutoStartDelayed { 'delayed' }
|
||||
}
|
||||
|
||||
$state = switch ($service.State) {
|
||||
Stopped { 'stopped' }
|
||||
StartPending { 'start_pending' }
|
||||
StopPending { 'stop_pending' }
|
||||
Running { 'started' }
|
||||
ContinuePending { 'continue_pending' }
|
||||
PausePending { 'pause_pending' }
|
||||
paused { 'paused' }
|
||||
}
|
||||
|
||||
$triggers = @(foreach ($trigger in $service.Triggers) {
|
||||
[Ordered]@{
|
||||
action = switch($trigger.Action) {
|
||||
ServiceStart { 'start_service' }
|
||||
ServiceStop { 'stop_service' }
|
||||
}
|
||||
type = switch($trigger.Type) {
|
||||
DeviceInterfaceArrival { 'device_interface_arrival' }
|
||||
IpAddressAvailability { 'ip_address_availability' }
|
||||
DomainJoin { 'domain_join' }
|
||||
FirewallPortEvent { 'firewall_port_event' }
|
||||
GroupPolicy { 'group_policy' }
|
||||
NetworkEndpoint { 'network_endpoint' }
|
||||
Custom { 'custom' }
|
||||
}
|
||||
sub_type = switch($trigger.SubType.ToString()) {
|
||||
([Ansible.Service.Trigger]::NAMED_PIPE_EVENT_GUID) { 'named_pipe_event' }
|
||||
([Ansible.Service.Trigger]::RPC_INTERFACE_EVENT_GUID) { 'rpc_interface_event' }
|
||||
([Ansible.Service.Trigger]::DOMAIN_JOIN_GUID) { 'domain_join' }
|
||||
([Ansible.Service.Trigger]::DOMAIN_LEAVE_GUID) { 'domain_leave' }
|
||||
([Ansible.Service.Trigger]::FIREWALL_PORT_OPEN_GUID) { 'firewall_port_open' }
|
||||
([Ansible.Service.Trigger]::FIREWALL_PORT_CLOSE_GUID) { 'firewall_port_close' }
|
||||
([Ansible.Service.Trigger]::MACHINE_POLICY_PRESENT_GUID) { 'machine_policy_present' }
|
||||
([Ansible.Service.Trigger]::USER_POLICY_PRESENT_GUID) { 'user_policy_present' }
|
||||
([Ansible.Service.Trigger]::NETWORK_MANAGER_FIRST_IP_ADDRESS_ARRIVAL_GUID) { 'network_first_ip_arrival' }
|
||||
([Ansible.Service.Trigger]::NETWORK_MANAGER_LAST_IP_ADDRESS_REMOVAL_GUID) { 'network_last_ip_removal' }
|
||||
default { 'custom' }
|
||||
}
|
||||
sub_type_guid = $trigger.SubType.ToString()
|
||||
data_items = @(foreach ($dataItem in $trigger.DataItems) {
|
||||
$dataValue = $dataItem.Data
|
||||
|
||||
# We only need to convert byte and byte[] to a Base64 string, the rest can be serialised as is.
|
||||
if ($dataValue -is [byte]) {
|
||||
$dataValue = [byte[]]@($dataValue)
|
||||
}
|
||||
|
||||
if ($dataValue -is [byte[]]) {
|
||||
$dataValue = [System.Convert]::ToBase64String($dataValue)
|
||||
}
|
||||
|
||||
[Ordered]@{
|
||||
type = switch ($dataItem.Type) {
|
||||
Binary { 'binary' }
|
||||
String { 'string' }
|
||||
Level { 'level' }
|
||||
KeywordAny { 'keyword_any' }
|
||||
KeywordAll { 'keyword_all' }
|
||||
}
|
||||
data = $dataValue
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
# These should closely reflect the options for win_service
|
||||
[Ordered]@{
|
||||
checkpoint = $service.Checkpoint
|
||||
controls_accepted = $controlsAccepted
|
||||
dependencies = $service.DependentOn
|
||||
dependency_of = $service.DependedBy
|
||||
description = $service.Description
|
||||
desktop_interact = $service.ServiceType.HasFlag([Ansible.Service.ServiceType]::InteractiveProcess)
|
||||
display_name = $service.DisplayName
|
||||
error_control = $service.ErrorControl.ToString().ToLowerInvariant()
|
||||
failure_actions = $failureActions
|
||||
failure_actions_on_non_crash_failure = $service.FailureActionsOnNonCrashFailures
|
||||
failure_command = $rawFailureActions.Command
|
||||
failure_reboot_msg = $rawFailureActions.RebootMsg
|
||||
failure_reset_period_sec = $rawFailureActions.ResetPeriod
|
||||
launch_protection = $launchProtection
|
||||
load_order_group = $service.LoadOrderGroup
|
||||
name = $service.ServiceName
|
||||
path = $service.Path
|
||||
pre_shutdown_timeout_ms = $service.PreShutdownTimeout
|
||||
preferred_node = $service.PreferredNode
|
||||
process_id = $service.ProcessId
|
||||
required_privileges = $service.RequiredPrivileges
|
||||
service_exit_code = $service.ServiceExitCode
|
||||
service_flags = $serviceFlags
|
||||
service_type = $serviceType
|
||||
sid_info = $service.ServiceSidInfo.ToString().ToLowerInvariant()
|
||||
start_mode = $startType
|
||||
state = $state
|
||||
triggers = $triggers
|
||||
username = $service.Account.Value
|
||||
wait_hint_ms = $service.WaitHint
|
||||
win32_exit_code = $service.Win32ExitCode
|
||||
}
|
||||
})
|
||||
|
||||
$module.ExitJson()
|
||||
@ -1,294 +0,0 @@
|
||||
#!/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)
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_service_info
|
||||
version_added: '2.10'
|
||||
short_description: Gather information about Windows services
|
||||
description:
|
||||
- Gather information about all or a specific installed Windows service(s).
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- If specified, this is used to match the C(name) or C(display_name) of the Windows service to get the info for.
|
||||
- Can be a wildcard to match multiple services but the wildcard will only be matched on the C(name) of the service
|
||||
and not C(display_name).
|
||||
- If omitted then all services will returned.
|
||||
type: str
|
||||
seealso:
|
||||
- module: win_service
|
||||
author:
|
||||
- Jordan Borean (@jborean93)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Get info for all installed services
|
||||
win_service_info:
|
||||
register: service_info
|
||||
|
||||
- name: Get info for a single service
|
||||
win_service_info:
|
||||
name: WinRM
|
||||
register: service_info
|
||||
|
||||
- name: Get info for a service using its display name
|
||||
win_service_info:
|
||||
name: Windows Remote Management (WS-Management)
|
||||
|
||||
- name: Find all services that start with 'win'
|
||||
win_service_info:
|
||||
name: win*
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
exists:
|
||||
description: Whether any services were found based on the criteria specified.
|
||||
returned: always
|
||||
type: bool
|
||||
sample: true
|
||||
services:
|
||||
description:
|
||||
- A list of service(s) that were found based on the criteria.
|
||||
- Will be an empty list if no services were found.
|
||||
returned: always
|
||||
type: list
|
||||
elements: dict
|
||||
contains:
|
||||
checkpoint:
|
||||
description:
|
||||
- A check-point value that the service increments periodically to report its progress.
|
||||
type: int
|
||||
sample: 0
|
||||
controls_accepted:
|
||||
description:
|
||||
- A list of controls that the service can accept.
|
||||
- Common controls are C(stop), C(pause_continue), C(shutdown).
|
||||
type: list
|
||||
elements: str
|
||||
sample: ['stop', 'shutdown']
|
||||
dependencies:
|
||||
description:
|
||||
- A list of services by their C(name) that this service is dependent on.
|
||||
type: list
|
||||
elements: str
|
||||
sample: ['HTTP', 'RPCSS']
|
||||
dependency_of:
|
||||
description:
|
||||
- A list of services by their C(name) that depend on this service.
|
||||
type: list
|
||||
elements: str
|
||||
sample: ['upnphost', 'WMPNetworkSvc']
|
||||
description:
|
||||
description:
|
||||
- The description of the service.
|
||||
type: str
|
||||
sample: Example description of the Windows service.
|
||||
desktop_interact:
|
||||
description:
|
||||
- Whether the service can interact with the desktop, only valid for services running as C(SYSTEM).
|
||||
type: bool
|
||||
sample: false
|
||||
display_name:
|
||||
description:
|
||||
- The display name to be used by SCM to identify the service.
|
||||
type: str
|
||||
sample: Windows Remote Management (WS-Management)
|
||||
error_control:
|
||||
description:
|
||||
- The action to take if a service fails to start.
|
||||
- Common values are C(critical), C(ignore), C(normal), C(severe).
|
||||
type: str
|
||||
sample: normal
|
||||
failure_actions:
|
||||
description:
|
||||
- A list of failure actions to run in the event of a failure.
|
||||
type: list
|
||||
elements: dict
|
||||
contains:
|
||||
delay_ms:
|
||||
description:
|
||||
- The time to wait, in milliseconds, before performing the specified action.
|
||||
type: int
|
||||
sample: 120000
|
||||
type:
|
||||
description:
|
||||
- The action that will be performed.
|
||||
- Common values are C(none), C(reboot), C(restart), C(run_command).
|
||||
type: str
|
||||
sample: run_command
|
||||
failure_action_on_non_crash_failure:
|
||||
description:
|
||||
- Controls when failure actions are fired based on how the service was stopped.
|
||||
type: bool
|
||||
sample: false
|
||||
failure_command:
|
||||
description:
|
||||
- The command line that will be run when a C(run_command) failure action is fired.
|
||||
type: str
|
||||
sample: runme.exe
|
||||
failure_reboot_msg:
|
||||
description:
|
||||
- The message to be broadcast to server users before rebooting when a C(reboot) failure action is fired.
|
||||
type: str
|
||||
sample: Service failed, rebooting host.
|
||||
failure_reset_period_sec:
|
||||
description:
|
||||
- The time, in seconds, after which to reset the failure count to zero.
|
||||
type: int
|
||||
sample: 86400
|
||||
launch_protection:
|
||||
description:
|
||||
- The protection type of the service.
|
||||
- Common values are C(none), C(windows), C(windows_light), or C(antimalware_light).
|
||||
type: str
|
||||
sample: none
|
||||
load_order_group:
|
||||
description:
|
||||
- The name of the load ordering group to which the service belongs.
|
||||
- Will be an empty string if it does not belong to any group.
|
||||
type: str
|
||||
sample: My group
|
||||
name:
|
||||
description:
|
||||
- The name of the service.
|
||||
type: str
|
||||
sample: WinRM
|
||||
path:
|
||||
description:
|
||||
- The path to the service binary and any arguments used when starting the service.
|
||||
- The binary part can be quoted to ensure any spaces in path are not treated as arguments.
|
||||
type: str
|
||||
sample: 'C:\Windows\System32\svchost.exe -k netsvcs -p'
|
||||
pre_shutdown_timeout_ms:
|
||||
description:
|
||||
- The preshutdown timeout out value in milliseconds.
|
||||
type: int
|
||||
sample: 10000
|
||||
preferred_node:
|
||||
description:
|
||||
- The node number for the preferred node.
|
||||
- This will be C(null) if the Windows host has no NUMA configuration.
|
||||
type: int
|
||||
sample: 0
|
||||
process_id:
|
||||
description:
|
||||
- The process identifier of the running service.
|
||||
type: int
|
||||
sample: 5135
|
||||
required_privileges:
|
||||
description:
|
||||
- A list of privileges that the service requires and will run with
|
||||
type: list
|
||||
elements: str
|
||||
sample: ['SeBackupPrivilege', 'SeRestorePrivilege']
|
||||
service_exit_code:
|
||||
description:
|
||||
- A service-specific error code that is set while the service is starting or stopping.
|
||||
type: int
|
||||
sample: 0
|
||||
service_flags:
|
||||
description:
|
||||
- Shows more information about the behaviour of a running service.
|
||||
- Currently the only flag that can be set is C(runs_in_system_process).
|
||||
type: list
|
||||
elements: str
|
||||
sample: [ 'runs_in_system_process' ]
|
||||
service_type:
|
||||
description:
|
||||
- The type of service.
|
||||
- Common types are C(win32_own_process), C(win32_share_process), C(user_own_process), C(user_share_process),
|
||||
C(kernel_driver).
|
||||
type: str
|
||||
sample: win32_own_process
|
||||
sid_info:
|
||||
description:
|
||||
- The behavior of how the service's access token is generated and how to add the service SID to the token.
|
||||
- Common values are C(none), C(restricted), or C(unrestricted).
|
||||
type: str
|
||||
sample: none
|
||||
start_mode:
|
||||
description:
|
||||
- When the service is set to start.
|
||||
- Common values are C(auto), C(manual), C(disabled), C(delayed).
|
||||
type: str
|
||||
sample: auto
|
||||
state:
|
||||
description:
|
||||
- The current running state of the service.
|
||||
- Common values are C(stopped), C(start_pending), C(stop_pending), C(started), C(continue_pending),
|
||||
C(pause_pending), C(paused).
|
||||
type: str
|
||||
sample: started
|
||||
triggers:
|
||||
description:
|
||||
- A list of triggers defined for the service.
|
||||
type: list
|
||||
elements: dict
|
||||
contains:
|
||||
action:
|
||||
description:
|
||||
- The action to perform once triggered, can be C(start_service) or C(stop_service).
|
||||
type: str
|
||||
sample: start_service
|
||||
data_items:
|
||||
description:
|
||||
- A list of trigger data items that contain trigger specific data.
|
||||
- A trigger can contain 0 or multiple data items.
|
||||
type: list
|
||||
elements: dict
|
||||
contains:
|
||||
data:
|
||||
description:
|
||||
- The trigger data item value.
|
||||
- Can be a string, list of string, int, or base64 string of binary data.
|
||||
type: complex
|
||||
sample: named pipe
|
||||
type:
|
||||
description:
|
||||
- The type of C(data) for the trigger.
|
||||
- Common values are C(string), C(binary), C(level), C(keyword_any), or C(keyword_all).
|
||||
type: str
|
||||
sample: string
|
||||
sub_type:
|
||||
description:
|
||||
- The trigger event sub type that is specific to each C(type).
|
||||
- Common values are C(named_pipe_event), C(domain_join), C(domain_leave), C(firewall_port_open), and others.
|
||||
type: str
|
||||
sample:
|
||||
sub_type_guid:
|
||||
description:
|
||||
- The guid which represents the trigger sub type.
|
||||
type: str
|
||||
sample: 1ce20aba-9851-4421-9430-1ddeb766e809
|
||||
type:
|
||||
description:
|
||||
- The trigger event type.
|
||||
- Common values are C(custom), C(rpc_interface_event), C(domain_join), C(group_policy), and others.
|
||||
type: str
|
||||
sample: domain_join
|
||||
username:
|
||||
description:
|
||||
- The username used to run the service.
|
||||
- Can be null for user services and certain driver services.
|
||||
type: str
|
||||
sample: NT AUTHORITY\SYSTEM
|
||||
wait_hint_ms:
|
||||
description:
|
||||
- The estimated time in milliseconds required for a pending start, stop, pause,or continue operations.
|
||||
type: int
|
||||
sample: 0
|
||||
win32_exitcode:
|
||||
description:
|
||||
- The error code returned from the service binary once it has stopped.
|
||||
- When set to C(1066) then a service specific error is returned on C(service_exit_code).
|
||||
type: int
|
||||
sample: 0
|
||||
'''
|
||||
@ -1,267 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2015, Hans-Joachim Kliemeck <git@kliemeck.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
#Requires -Module Ansible.ModuleUtils.SID
|
||||
|
||||
#Functions
|
||||
Function NormalizeAccounts {
|
||||
param(
|
||||
[parameter(valuefrompipeline=$true)]
|
||||
$users
|
||||
)
|
||||
|
||||
$users = $users.Trim()
|
||||
If ($users -eq "") {
|
||||
$splitUsers = [Collections.Generic.List[String]] @()
|
||||
}
|
||||
Else {
|
||||
$splitUsers = [Collections.Generic.List[String]] $users.Split(",")
|
||||
}
|
||||
|
||||
$normalizedUsers = [Collections.Generic.List[String]] @()
|
||||
ForEach($splitUser in $splitUsers) {
|
||||
$sid = Convert-ToSID -account_name $splitUser
|
||||
if (!$sid) {
|
||||
Fail-Json $result "$splitUser is not a valid user or group on the host machine or domain"
|
||||
}
|
||||
|
||||
$normalizedUser = (New-Object System.Security.Principal.SecurityIdentifier($sid)).Translate([System.Security.Principal.NTAccount])
|
||||
$normalizedUsers.Add($normalizedUser)
|
||||
}
|
||||
|
||||
return ,$normalizedUsers
|
||||
}
|
||||
|
||||
$result = @{
|
||||
changed = $false
|
||||
actions = @() # More for debug purposes
|
||||
}
|
||||
|
||||
$params = Parse-Args $args -supports_check_mode $true
|
||||
|
||||
# While the -SmbShare cmdlets have a -WhatIf parameter, they don't honor it, need to skip the cmdlet if in check mode
|
||||
$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","absent"
|
||||
$rule_action = Get-AnsibleParam -obj $params -name "rule_action" -type "str" -default "set" -validateset "set","add"
|
||||
|
||||
if (-not (Get-Command -Name Get-SmbShare -ErrorAction SilentlyContinue)) {
|
||||
Fail-Json $result "The current host does not support the -SmbShare cmdlets required by this module. Please run on Server 2012 or Windows 8 and later"
|
||||
}
|
||||
|
||||
$share = Get-SmbShare -Name $name -ErrorAction SilentlyContinue
|
||||
If ($state -eq "absent") {
|
||||
If ($share) {
|
||||
# See message around -WhatIf where $check_mode is defined
|
||||
if (-not $check_mode) {
|
||||
Remove-SmbShare -Force -Name $name | Out-Null
|
||||
}
|
||||
$result.actions += "Remove-SmbShare -Force -Name $name"
|
||||
$result.changed = $true
|
||||
}
|
||||
} Else {
|
||||
$path = Get-AnsibleParam -obj $params -name "path" -type "path" -failifempty $true
|
||||
$description = Get-AnsibleParam -obj $params -name "description" -type "str" -default ""
|
||||
|
||||
$permissionList = Get-AnsibleParam -obj $params -name "list" -type "bool" -default $false
|
||||
$folderEnum = if ($permissionList) { "Unrestricted" } else { "AccessBased" }
|
||||
|
||||
$permissionRead = Get-AnsibleParam -obj $params -name "read" -type "str" -default "" | NormalizeAccounts
|
||||
$permissionChange = Get-AnsibleParam -obj $params -name "change" -type "str" -default "" | NormalizeAccounts
|
||||
$permissionFull = Get-AnsibleParam -obj $params -name "full" -type "str" -default "" | NormalizeAccounts
|
||||
$permissionDeny = Get-AnsibleParam -obj $params -name "deny" -type "str" -default "" | NormalizeAccounts
|
||||
|
||||
$cachingMode = Get-AnsibleParam -obj $params -name "caching_mode" -type "str" -default "Manual" -validateSet "BranchCache","Documents","Manual","None","Programs","Unknown"
|
||||
$encrypt = Get-AnsibleParam -obj $params -name "encrypt" -type "bool" -default $false
|
||||
|
||||
If (-Not (Test-Path -Path $path)) {
|
||||
Fail-Json $result "$path directory does not exist on the host"
|
||||
}
|
||||
|
||||
# normalize path and remove slash at the end
|
||||
$path = (Get-Item $path).FullName -replace "\\$"
|
||||
|
||||
# need to (re-)create share
|
||||
If (-not $share) {
|
||||
if (-not $check_mode) {
|
||||
New-SmbShare -Name $name -Path $path | Out-Null
|
||||
}
|
||||
$share = Get-SmbShare -Name $name -ErrorAction SilentlyContinue
|
||||
|
||||
$result.changed = $true
|
||||
$result.actions += "New-SmbShare -Name $name -Path $path"
|
||||
# if in check mode we cannot run the below as no share exists so just
|
||||
# exit early
|
||||
if ($check_mode) {
|
||||
Exit-Json -obj $result
|
||||
}
|
||||
}
|
||||
If ($share.Path -ne $path) {
|
||||
if (-not $check_mode) {
|
||||
Remove-SmbShare -Force -Name $name | Out-Null
|
||||
New-SmbShare -Name $name -Path $path | Out-Null
|
||||
}
|
||||
$share = Get-SmbShare -Name $name -ErrorAction SilentlyContinue
|
||||
$result.changed = $true
|
||||
$result.actions += "Remove-SmbShare -Force -Name $name"
|
||||
$result.actions += "New-SmbShare -Name $name -Path $path"
|
||||
}
|
||||
|
||||
# updates
|
||||
If ($share.Description -ne $description) {
|
||||
if (-not $check_mode) {
|
||||
Set-SmbShare -Force -Name $name -Description $description | Out-Null
|
||||
}
|
||||
$result.changed = $true
|
||||
$result.actions += "Set-SmbShare -Force -Name $name -Description $description"
|
||||
}
|
||||
If ($share.FolderEnumerationMode -ne $folderEnum) {
|
||||
if (-not $check_mode) {
|
||||
Set-SmbShare -Force -Name $name -FolderEnumerationMode $folderEnum | Out-Null
|
||||
}
|
||||
$result.changed = $true
|
||||
$result.actions += "Set-SmbShare -Force -Name $name -FolderEnumerationMode $folderEnum"
|
||||
}
|
||||
if ($share.CachingMode -ne $cachingMode) {
|
||||
if (-not $check_mode) {
|
||||
Set-SmbShare -Force -Name $name -CachingMode $cachingMode | Out-Null
|
||||
}
|
||||
$result.changed = $true
|
||||
$result.actions += "Set-SmbShare -Force -Name $name -CachingMode $cachingMode"
|
||||
}
|
||||
if ($share.EncryptData -ne $encrypt) {
|
||||
if (-not $check_mode) {
|
||||
Set-SmbShare -Force -Name $name -EncryptData $encrypt | Out-Null
|
||||
}
|
||||
$result.changed = $true
|
||||
$result.actions += "Set-SmbShare -Force -Name $name -EncryptData $encrypt"
|
||||
}
|
||||
|
||||
# clean permissions that imply others
|
||||
ForEach ($user in $permissionFull) {
|
||||
$permissionChange.remove($user) | Out-Null
|
||||
$permissionRead.remove($user) | Out-Null
|
||||
}
|
||||
ForEach ($user in $permissionChange) {
|
||||
$permissionRead.remove($user) | Out-Null
|
||||
}
|
||||
|
||||
# remove permissions
|
||||
$permissions = Get-SmbShareAccess -Name $name
|
||||
if($rule_action -eq "set") {
|
||||
ForEach ($permission in $permissions) {
|
||||
If ($permission.AccessControlType -eq "Deny") {
|
||||
$cim_count = 0
|
||||
foreach ($count in $permissions) {
|
||||
$cim_count++
|
||||
}
|
||||
# Don't remove the Deny entry for Everyone if there are no other permissions set (cim_count == 1)
|
||||
if (-not ($permission.AccountName -eq 'Everyone' -and $cim_count -eq 1)) {
|
||||
If (-not ($permissionDeny.Contains($permission.AccountName))) {
|
||||
if (-not $check_mode) {
|
||||
Unblock-SmbShareAccess -Force -Name $name -AccountName $permission.AccountName | Out-Null
|
||||
}
|
||||
$result.changed = $true
|
||||
$result.actions += "Unblock-SmbShareAccess -Force -Name $name -AccountName $($permission.AccountName)"
|
||||
} else {
|
||||
# Remove from the deny list as it already has the permissions
|
||||
$permissionDeny.remove($permission.AccountName) | Out-Null
|
||||
}
|
||||
}
|
||||
} ElseIf ($permission.AccessControlType -eq "Allow") {
|
||||
If ($permission.AccessRight -eq "Full") {
|
||||
If (-not ($permissionFull.Contains($permission.AccountName))) {
|
||||
if (-not $check_mode) {
|
||||
Revoke-SmbShareAccess -Force -Name $name -AccountName $permission.AccountName | Out-Null
|
||||
}
|
||||
$result.changed = $true
|
||||
$result.actions += "Revoke-SmbShareAccess -Force -Name $name -AccountName $($permission.AccountName)"
|
||||
|
||||
Continue
|
||||
}
|
||||
|
||||
# user got requested permissions
|
||||
$permissionFull.remove($permission.AccountName) | Out-Null
|
||||
} ElseIf ($permission.AccessRight -eq "Change") {
|
||||
If (-not ($permissionChange.Contains($permission.AccountName))) {
|
||||
if (-not $check_mode) {
|
||||
Revoke-SmbShareAccess -Force -Name $name -AccountName $permission.AccountName | Out-Null
|
||||
}
|
||||
$result.changed = $true
|
||||
$result.actions += "Revoke-SmbShareAccess -Force -Name $name -AccountName $($permission.AccountName)"
|
||||
|
||||
Continue
|
||||
}
|
||||
|
||||
# user got requested permissions
|
||||
$permissionChange.remove($permission.AccountName) | Out-Null
|
||||
} ElseIf ($permission.AccessRight -eq "Read") {
|
||||
If (-not ($permissionRead.Contains($permission.AccountName))) {
|
||||
if (-not $check_mode) {
|
||||
Revoke-SmbShareAccess -Force -Name $name -AccountName $permission.AccountName | Out-Null
|
||||
}
|
||||
$result.changed = $true
|
||||
$result.actions += "Revoke-SmbShareAccess -Force -Name $name -AccountName $($permission.AccountName)"
|
||||
|
||||
Continue
|
||||
}
|
||||
|
||||
# user got requested permissions
|
||||
$permissionRead.Remove($permission.AccountName) | Out-Null
|
||||
}
|
||||
}
|
||||
}
|
||||
} ElseIf ($rule_action -eq "add") {
|
||||
ForEach($permission in $permissions) {
|
||||
If ($permission.AccessControlType -eq "Deny") {
|
||||
If ($permissionDeny.Contains($permission.AccountName)) {
|
||||
$permissionDeny.Remove($permission.AccountName)
|
||||
}
|
||||
} ElseIf ($permission.AccessControlType -eq "Allow") {
|
||||
If ($permissionFull.Contains($permission.AccountName) -and $permission.AccessRight -eq "Full") {
|
||||
$permissionFull.Remove($permission.AccountName)
|
||||
} ElseIf ($permissionChange.Contains($permission.AccountName) -and $permission.AccessRight -eq "Change") {
|
||||
$permissionChange.Remove($permission.AccountName)
|
||||
} ElseIf ($permissionRead.Contains($permission.AccountName) -and $permission.AccessRight -eq "Read") {
|
||||
$permissionRead.Remove($permission.AccountName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# add missing permissions
|
||||
ForEach ($user in $permissionRead) {
|
||||
if (-not $check_mode) {
|
||||
Grant-SmbShareAccess -Force -Name $name -AccountName $user -AccessRight "Read" | Out-Null
|
||||
}
|
||||
$result.changed = $true
|
||||
$result.actions += "Grant-SmbShareAccess -Force -Name $name -AccountName $user -AccessRight Read"
|
||||
}
|
||||
ForEach ($user in $permissionChange) {
|
||||
if (-not $check_mode) {
|
||||
Grant-SmbShareAccess -Force -Name $name -AccountName $user -AccessRight "Change" | Out-Null
|
||||
}
|
||||
$result.changed = $true
|
||||
$result.actions += "Grant-SmbShareAccess -Force -Name $name -AccountName $user -AccessRight Change"
|
||||
}
|
||||
ForEach ($user in $permissionFull) {
|
||||
if (-not $check_mode) {
|
||||
Grant-SmbShareAccess -Force -Name $name -AccountName $user -AccessRight "Full" | Out-Null
|
||||
}
|
||||
$result.changed = $true
|
||||
$result.actions += "Grant-SmbShareAccess -Force -Name $name -AccountName $user -AccessRight Full"
|
||||
}
|
||||
ForEach ($user in $permissionDeny) {
|
||||
if (-not $check_mode) {
|
||||
Block-SmbShareAccess -Force -Name $name -AccountName $user | Out-Null
|
||||
}
|
||||
$result.changed = $true
|
||||
$result.actions += "Block-SmbShareAccess -Force -Name $name -AccountName $user"
|
||||
}
|
||||
}
|
||||
|
||||
Exit-Json $result
|
||||
@ -1,125 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2015, Hans-Joachim Kliemeck <git@kliemeck.de>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_share
|
||||
version_added: "2.1"
|
||||
short_description: Manage Windows shares
|
||||
description:
|
||||
- Add, modify or remove Windows share and set share permissions.
|
||||
requirements:
|
||||
- As this module used newer cmdlets like New-SmbShare this can only run on
|
||||
Windows 8 / Windows 2012 or newer.
|
||||
- This is due to the reliance on the WMI provider MSFT_SmbShare
|
||||
U(https://msdn.microsoft.com/en-us/library/hh830471) which was only added
|
||||
with these Windows releases.
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Share name.
|
||||
type: str
|
||||
required: yes
|
||||
path:
|
||||
description:
|
||||
- Share directory.
|
||||
type: path
|
||||
required: yes
|
||||
state:
|
||||
description:
|
||||
- Specify whether to add C(present) or remove C(absent) the specified share.
|
||||
type: str
|
||||
choices: [ absent, present ]
|
||||
default: present
|
||||
description:
|
||||
description:
|
||||
- Share description.
|
||||
type: str
|
||||
list:
|
||||
description:
|
||||
- Specify whether to allow or deny file listing, in case user has no permission on share. Also known as Access-Based Enumeration.
|
||||
type: bool
|
||||
default: no
|
||||
read:
|
||||
description:
|
||||
- Specify user list that should get read access on share, separated by comma.
|
||||
type: str
|
||||
change:
|
||||
description:
|
||||
- Specify user list that should get read and write access on share, separated by comma.
|
||||
type: str
|
||||
full:
|
||||
description:
|
||||
- Specify user list that should get full access on share, separated by comma.
|
||||
type: str
|
||||
deny:
|
||||
description:
|
||||
- Specify user list that should get no access, regardless of implied access on share, separated by comma.
|
||||
type: str
|
||||
caching_mode:
|
||||
description:
|
||||
- Set the CachingMode for this share.
|
||||
type: str
|
||||
choices: [ BranchCache, Documents, Manual, None, Programs, Unknown ]
|
||||
default: Manual
|
||||
version_added: "2.3"
|
||||
encrypt:
|
||||
description: Sets whether to encrypt the traffic to the share or not.
|
||||
type: bool
|
||||
default: no
|
||||
version_added: "2.4"
|
||||
rule_action:
|
||||
description: Whether to add or set (replace) access control entries.
|
||||
type: str
|
||||
choices: [ set, add ]
|
||||
default: set
|
||||
version_added: "2.10"
|
||||
author:
|
||||
- Hans-Joachim Kliemeck (@h0nIg)
|
||||
- David Baumann (@daBONDi)
|
||||
- Shachaf Goldstein (@Shachaf92)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
# Playbook example
|
||||
# Add share and set permissions
|
||||
---
|
||||
- name: Add secret share
|
||||
win_share:
|
||||
name: internal
|
||||
description: top secret share
|
||||
path: C:\shares\internal
|
||||
list: no
|
||||
full: Administrators,CEO
|
||||
read: HR-Global
|
||||
deny: HR-External
|
||||
|
||||
- name: Add public company share
|
||||
win_share:
|
||||
name: company
|
||||
description: top secret share
|
||||
path: C:\shares\company
|
||||
list: yes
|
||||
full: Administrators,CEO
|
||||
read: Global
|
||||
|
||||
- name: Remove previously added share
|
||||
win_share:
|
||||
name: internal
|
||||
state: absent
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
actions:
|
||||
description: A list of action cmdlets that were run by the module.
|
||||
returned: success
|
||||
type: list
|
||||
sample: ['New-SmbShare -Name share -Path C:\temp']
|
||||
'''
|
||||
@ -1,138 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
#Requires -Module Ansible.ModuleUtils.CommandUtil
|
||||
#Requires -Module Ansible.ModuleUtils.FileUtil
|
||||
|
||||
# TODO: add check mode support
|
||||
|
||||
Set-StrictMode -Version 2
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Cleanse CLIXML from stderr (sift out error stream data, discard others for now)
|
||||
Function Cleanse-Stderr($raw_stderr) {
|
||||
Try {
|
||||
# NB: this regex isn't perfect, but is decent at finding CLIXML amongst other stderr noise
|
||||
If($raw_stderr -match "(?s)(?<prenoise1>.*)#< CLIXML(?<prenoise2>.*)(?<clixml><Objs.+</Objs>)(?<postnoise>.*)") {
|
||||
$clixml = [xml]$matches["clixml"]
|
||||
|
||||
$merged_stderr = "{0}{1}{2}{3}" -f @(
|
||||
$matches["prenoise1"],
|
||||
$matches["prenoise2"],
|
||||
# filter out just the Error-tagged strings for now, and zap embedded CRLF chars
|
||||
($clixml.Objs.ChildNodes | Where-Object { $_.Name -eq 'S' } | Where-Object { $_.S -eq 'Error' } | ForEach-Object { $_.'#text'.Replace('_x000D__x000A_','') } | Out-String),
|
||||
$matches["postnoise"]) | Out-String
|
||||
|
||||
return $merged_stderr.Trim()
|
||||
|
||||
# FUTURE: parse/return other streams
|
||||
}
|
||||
Else {
|
||||
$raw_stderr
|
||||
}
|
||||
}
|
||||
Catch {
|
||||
"***EXCEPTION PARSING CLIXML: $_***" + $raw_stderr
|
||||
}
|
||||
}
|
||||
|
||||
$params = Parse-Args $args -supports_check_mode $false
|
||||
|
||||
$raw_command_line = Get-AnsibleParam -obj $params -name "_raw_params" -type "str" -failifempty $true
|
||||
$chdir = Get-AnsibleParam -obj $params -name "chdir" -type "path"
|
||||
$executable = Get-AnsibleParam -obj $params -name "executable" -type "path"
|
||||
$creates = Get-AnsibleParam -obj $params -name "creates" -type "path"
|
||||
$removes = Get-AnsibleParam -obj $params -name "removes" -type "path"
|
||||
$stdin = Get-AnsibleParam -obj $params -name "stdin" -type "str"
|
||||
$no_profile = Get-AnsibleParam -obj $params -name "no_profile" -type "bool" -default $false
|
||||
$output_encoding_override = Get-AnsibleParam -obj $params -name "output_encoding_override" -type "str"
|
||||
|
||||
$raw_command_line = $raw_command_line.Trim()
|
||||
|
||||
$result = @{
|
||||
changed = $true
|
||||
cmd = $raw_command_line
|
||||
}
|
||||
|
||||
if ($creates -and $(Test-AnsiblePath -Path $creates)) {
|
||||
Exit-Json @{msg="skipped, since $creates exists";cmd=$raw_command_line;changed=$false;skipped=$true;rc=0}
|
||||
}
|
||||
|
||||
if ($removes -and -not $(Test-AnsiblePath -Path $removes)) {
|
||||
Exit-Json @{msg="skipped, since $removes does not exist";cmd=$raw_command_line;changed=$false;skipped=$true;rc=0}
|
||||
}
|
||||
|
||||
$exec_args = $null
|
||||
If(-not $executable -or $executable -eq "powershell") {
|
||||
$exec_application = "powershell.exe"
|
||||
|
||||
# force input encoding to preamble-free UTF8 so PS sub-processes (eg, Start-Job) don't blow up
|
||||
$raw_command_line = "[Console]::InputEncoding = New-Object Text.UTF8Encoding `$false; " + $raw_command_line
|
||||
|
||||
# Base64 encode the command so we don't have to worry about the various levels of escaping
|
||||
$encoded_command = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($raw_command_line))
|
||||
|
||||
if ($stdin) {
|
||||
$exec_args = "-encodedcommand $encoded_command"
|
||||
} else {
|
||||
$exec_args = "-noninteractive -encodedcommand $encoded_command"
|
||||
}
|
||||
|
||||
if ($no_profile) {
|
||||
$exec_args = "-noprofile $exec_args"
|
||||
}
|
||||
}
|
||||
Else {
|
||||
# FUTURE: support arg translation from executable (or executable_args?) to process arguments for arbitrary interpreter?
|
||||
$exec_application = $executable
|
||||
if (-not ($exec_application.EndsWith(".exe"))) {
|
||||
$exec_application = "$($exec_application).exe"
|
||||
}
|
||||
$exec_args = "/c $raw_command_line"
|
||||
}
|
||||
|
||||
$command = "`"$exec_application`" $exec_args"
|
||||
$run_command_arg = @{
|
||||
command = $command
|
||||
}
|
||||
if ($chdir) {
|
||||
$run_command_arg['working_directory'] = $chdir
|
||||
}
|
||||
if ($stdin) {
|
||||
$run_command_arg['stdin'] = $stdin
|
||||
}
|
||||
if ($output_encoding_override) {
|
||||
$run_command_arg['output_encoding_override'] = $output_encoding_override
|
||||
}
|
||||
|
||||
$start_datetime = [DateTime]::UtcNow
|
||||
try {
|
||||
$command_result = Run-Command @run_command_arg
|
||||
} catch {
|
||||
$result.changed = $false
|
||||
try {
|
||||
$result.rc = $_.Exception.NativeErrorCode
|
||||
} catch {
|
||||
$result.rc = 2
|
||||
}
|
||||
Fail-Json -obj $result -message $_.Exception.Message
|
||||
}
|
||||
|
||||
# TODO: decode CLIXML stderr output (and other streams?)
|
||||
$result.stdout = $command_result.stdout
|
||||
$result.stderr = Cleanse-Stderr $command_result.stderr
|
||||
$result.rc = $command_result.rc
|
||||
|
||||
$end_datetime = [DateTime]::UtcNow
|
||||
$result.start = $start_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff")
|
||||
$result.end = $end_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff")
|
||||
$result.delta = $($end_datetime - $start_datetime).ToString("h\:mm\:ss\.ffffff")
|
||||
|
||||
If ($result.rc -ne 0) {
|
||||
Fail-Json -obj $result -message "non-zero return code"
|
||||
}
|
||||
|
||||
Exit-Json $result
|
||||
@ -1,167 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2016, Ansible, inc
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_shell
|
||||
short_description: Execute shell commands on target hosts
|
||||
version_added: 2.2
|
||||
description:
|
||||
- The C(win_shell) module takes the command name followed by a list of space-delimited arguments.
|
||||
It is similar to the M(win_command) module, but runs
|
||||
the command via a shell (defaults to PowerShell) on the target host.
|
||||
- For non-Windows targets, use the M(shell) module instead.
|
||||
options:
|
||||
free_form:
|
||||
description:
|
||||
- The C(win_shell) module takes a free form command to run.
|
||||
- There is no parameter actually named 'free form'. See the examples!
|
||||
type: str
|
||||
required: yes
|
||||
creates:
|
||||
description:
|
||||
- A path or path filter pattern; when the referenced path exists on the target host, the task will be skipped.
|
||||
type: path
|
||||
removes:
|
||||
description:
|
||||
- A path or path filter pattern; when the referenced path B(does not) exist on the target host, the task will be skipped.
|
||||
type: path
|
||||
chdir:
|
||||
description:
|
||||
- Set the specified path as the current working directory before executing a command
|
||||
type: path
|
||||
executable:
|
||||
description:
|
||||
- Change the shell used to execute the command (eg, C(cmd)).
|
||||
- The target shell must accept a C(/c) parameter followed by the raw command line to be executed.
|
||||
type: path
|
||||
stdin:
|
||||
description:
|
||||
- Set the stdin of the command directly to the specified value.
|
||||
type: str
|
||||
version_added: '2.5'
|
||||
no_profile:
|
||||
description:
|
||||
- Do not load the user profile before running a command. This is only valid
|
||||
when using PowerShell as the executable.
|
||||
type: bool
|
||||
default: no
|
||||
version_added: '2.8'
|
||||
output_encoding_override:
|
||||
description:
|
||||
- This option overrides the encoding of stdout/stderr output.
|
||||
- You can use this option when you need to run a command which ignore the console's codepage.
|
||||
- You should only need to use this option in very rare circumstances.
|
||||
- This value can be any valid encoding C(Name) based on the output of C([System.Text.Encoding]::GetEncodings()).
|
||||
See U(https://docs.microsoft.com/dotnet/api/system.text.encoding.getencodings).
|
||||
type: str
|
||||
version_added: '2.10'
|
||||
notes:
|
||||
- If you want to run an executable securely and predictably, it may be
|
||||
better to use the M(win_command) module instead. Best practices when writing
|
||||
playbooks will follow the trend of using M(win_command) unless C(win_shell) is
|
||||
explicitly required. When running ad-hoc commands, use your best judgement.
|
||||
- WinRM will not return from a command execution until all child processes created have exited.
|
||||
Thus, it is not possible to use C(win_shell) to spawn long-running child or background processes.
|
||||
Consider creating a Windows service for managing background processes.
|
||||
seealso:
|
||||
- module: psexec
|
||||
- module: raw
|
||||
- module: script
|
||||
- module: shell
|
||||
- module: win_command
|
||||
- module: win_psexec
|
||||
author:
|
||||
- Matt Davis (@nitzmahone)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
# Execute a command in the remote shell; stdout goes to the specified
|
||||
# file on the remote.
|
||||
- win_shell: C:\somescript.ps1 >> C:\somelog.txt
|
||||
|
||||
# Change the working directory to somedir/ before executing the command.
|
||||
- win_shell: C:\somescript.ps1 >> C:\somelog.txt chdir=C:\somedir
|
||||
|
||||
# You can also use the 'args' form to provide the options. This command
|
||||
# will change the working directory to somedir/ and will only run when
|
||||
# somedir/somelog.txt doesn't exist.
|
||||
- win_shell: C:\somescript.ps1 >> C:\somelog.txt
|
||||
args:
|
||||
chdir: C:\somedir
|
||||
creates: C:\somelog.txt
|
||||
|
||||
# Run a command under a non-Powershell interpreter (cmd in this case)
|
||||
- win_shell: echo %HOMEDIR%
|
||||
args:
|
||||
executable: cmd
|
||||
register: homedir_out
|
||||
|
||||
- name: Run multi-lined shell commands
|
||||
win_shell: |
|
||||
$value = Test-Path -Path C:\temp
|
||||
if ($value) {
|
||||
Remove-Item -Path C:\temp -Force
|
||||
}
|
||||
New-Item -Path C:\temp -ItemType Directory
|
||||
|
||||
- name: Retrieve the input based on stdin
|
||||
win_shell: '$string = [Console]::In.ReadToEnd(); Write-Output $string.Trim()'
|
||||
args:
|
||||
stdin: Input message
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
msg:
|
||||
description: Changed.
|
||||
returned: always
|
||||
type: bool
|
||||
sample: true
|
||||
start:
|
||||
description: The command execution start time.
|
||||
returned: always
|
||||
type: str
|
||||
sample: '2016-02-25 09:18:26.429568'
|
||||
end:
|
||||
description: The command execution end time.
|
||||
returned: always
|
||||
type: str
|
||||
sample: '2016-02-25 09:18:26.755339'
|
||||
delta:
|
||||
description: The command execution delta time.
|
||||
returned: always
|
||||
type: str
|
||||
sample: '0:00:00.325771'
|
||||
stdout:
|
||||
description: The command standard output.
|
||||
returned: always
|
||||
type: str
|
||||
sample: 'Clustering node rabbit@slave1 with rabbit@master ...'
|
||||
stderr:
|
||||
description: The command standard error.
|
||||
returned: always
|
||||
type: str
|
||||
sample: 'ls: cannot access foo: No such file or directory'
|
||||
cmd:
|
||||
description: The command executed by the task.
|
||||
returned: always
|
||||
type: str
|
||||
sample: 'rabbitmqctl join_cluster rabbit@master'
|
||||
rc:
|
||||
description: The command return code (0 means success).
|
||||
returned: always
|
||||
type: int
|
||||
sample: 0
|
||||
stdout_lines:
|
||||
description: The command standard output split in lines.
|
||||
returned: always
|
||||
type: list
|
||||
sample: [u'Clustering node rabbit@slave1 with rabbit@master ...']
|
||||
'''
|
||||
@ -1,186 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#AnsibleRequires -CSharpUtil Ansible.Basic
|
||||
#Requires -Module Ansible.ModuleUtils.FileUtil
|
||||
#Requires -Module Ansible.ModuleUtils.LinkUtil
|
||||
|
||||
function ConvertTo-Timestamp($start_date, $end_date) {
|
||||
if ($start_date -and $end_date) {
|
||||
return (New-TimeSpan -Start $start_date -End $end_date).TotalSeconds
|
||||
}
|
||||
}
|
||||
|
||||
function Get-FileChecksum($path, $algorithm) {
|
||||
switch ($algorithm) {
|
||||
'md5' { $sp = New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider }
|
||||
'sha1' { $sp = New-Object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider }
|
||||
'sha256' { $sp = New-Object -TypeName System.Security.Cryptography.SHA256CryptoServiceProvider }
|
||||
'sha384' { $sp = New-Object -TypeName System.Security.Cryptography.SHA384CryptoServiceProvider }
|
||||
'sha512' { $sp = New-Object -TypeName System.Security.Cryptography.SHA512CryptoServiceProvider }
|
||||
default { Fail-Json -obj $result -message "Unsupported hash algorithm supplied '$algorithm'" }
|
||||
}
|
||||
|
||||
$fp = [System.IO.File]::Open($path, [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
|
||||
try {
|
||||
$hash = [System.BitConverter]::ToString($sp.ComputeHash($fp)).Replace("-", "").ToLower()
|
||||
} finally {
|
||||
$fp.Dispose()
|
||||
}
|
||||
|
||||
return $hash
|
||||
}
|
||||
|
||||
function Get-FileInfo {
|
||||
param([String]$Path, [Switch]$Follow)
|
||||
|
||||
$info = Get-AnsibleItem -Path $Path -ErrorAction SilentlyContinue
|
||||
$link_info = $null
|
||||
if ($null -ne $info) {
|
||||
try {
|
||||
$link_info = Get-Link -link_path $info.FullName
|
||||
} catch {
|
||||
$module.Warn("Failed to check/get link info for file: $($_.Exception.Message)")
|
||||
}
|
||||
|
||||
# If follow=true we want to follow the link all the way back to root object
|
||||
if ($Follow -and $null -ne $link_info -and $link_info.Type -in @("SymbolicLink", "JunctionPoint")) {
|
||||
$info, $link_info = Get-FileInfo -Path $link_info.AbsolutePath -Follow
|
||||
}
|
||||
}
|
||||
|
||||
return $info, $link_info
|
||||
}
|
||||
|
||||
$spec = @{
|
||||
options = @{
|
||||
path = @{ type='path'; required=$true; aliases=@( 'dest', 'name' ) }
|
||||
get_checksum = @{ type='bool'; default=$true }
|
||||
checksum_algorithm = @{ type='str'; default='sha1'; choices=@( 'md5', 'sha1', 'sha256', 'sha384', 'sha512' ) }
|
||||
follow = @{ type='bool'; default=$false }
|
||||
}
|
||||
supports_check_mode = $true
|
||||
}
|
||||
|
||||
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||||
|
||||
$path = $module.Params.path
|
||||
$get_checksum = $module.Params.get_checksum
|
||||
$checksum_algorithm = $module.Params.checksum_algorithm
|
||||
$follow = $module.Params.follow
|
||||
|
||||
$module.Result.stat = @{ exists=$false }
|
||||
|
||||
Load-LinkUtils
|
||||
$info, $link_info = Get-FileInfo -Path $path -Follow:$follow
|
||||
If ($null -ne $info) {
|
||||
$epoch_date = Get-Date -Date "01/01/1970"
|
||||
$attributes = @()
|
||||
foreach ($attribute in ($info.Attributes -split ',')) {
|
||||
$attributes += $attribute.Trim()
|
||||
}
|
||||
|
||||
# default values that are always set, specific values are set below this
|
||||
# but are kept commented for easier readability
|
||||
$stat = @{
|
||||
exists = $true
|
||||
attributes = $info.Attributes.ToString()
|
||||
isarchive = ($attributes -contains "Archive")
|
||||
isdir = $false
|
||||
ishidden = ($attributes -contains "Hidden")
|
||||
isjunction = $false
|
||||
islnk = $false
|
||||
isreadonly = ($attributes -contains "ReadOnly")
|
||||
isreg = $false
|
||||
isshared = $false
|
||||
nlink = 1 # Number of links to the file (hard links), overriden below if islnk
|
||||
# lnk_target = islnk or isjunction Target of the symlink. Note that relative paths remain relative
|
||||
# lnk_source = islnk os isjunction Target of the symlink normalized for the remote filesystem
|
||||
hlnk_targets = @()
|
||||
creationtime = (ConvertTo-Timestamp -start_date $epoch_date -end_date $info.CreationTime)
|
||||
lastaccesstime = (ConvertTo-Timestamp -start_date $epoch_date -end_date $info.LastAccessTime)
|
||||
lastwritetime = (ConvertTo-Timestamp -start_date $epoch_date -end_date $info.LastWriteTime)
|
||||
# size = a file and directory - calculated below
|
||||
path = $info.FullName
|
||||
filename = $info.Name
|
||||
# extension = a file
|
||||
# owner = set outsite this dict in case it fails
|
||||
# sharename = a directory and isshared is True
|
||||
# checksum = a file and get_checksum: True
|
||||
}
|
||||
try {
|
||||
$stat.owner = $info.GetAccessControl().Owner
|
||||
} catch {
|
||||
# may not have rights, historical behaviour was to just set to $null
|
||||
# due to ErrorActionPreference being set to "Continue"
|
||||
$stat.owner = $null
|
||||
}
|
||||
|
||||
# values that are set according to the type of file
|
||||
if ($info.Attributes.HasFlag([System.IO.FileAttributes]::Directory)) {
|
||||
$stat.isdir = $true
|
||||
$share_info = Get-CimInstance -ClassName Win32_Share -Filter "Path='$($stat.path -replace '\\', '\\')'"
|
||||
if ($null -ne $share_info) {
|
||||
$stat.isshared = $true
|
||||
$stat.sharename = $share_info.Name
|
||||
}
|
||||
|
||||
try {
|
||||
$size = 0
|
||||
foreach ($file in $info.EnumerateFiles("*", [System.IO.SearchOption]::AllDirectories)) {
|
||||
$size += $file.Length
|
||||
}
|
||||
$stat.size = $size
|
||||
} catch {
|
||||
$stat.size = 0
|
||||
}
|
||||
} else {
|
||||
$stat.extension = $info.Extension
|
||||
$stat.isreg = $true
|
||||
$stat.size = $info.Length
|
||||
|
||||
if ($get_checksum) {
|
||||
try {
|
||||
$stat.checksum = Get-FileChecksum -path $path -algorithm $checksum_algorithm
|
||||
} catch {
|
||||
$module.FailJson("Failed to get hash of file, set get_checksum to False to ignore this error: $($_.Exception.Message)", $_)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Get symbolic link, junction point, hard link info
|
||||
if ($null -ne $link_info) {
|
||||
switch ($link_info.Type) {
|
||||
"SymbolicLink" {
|
||||
$stat.islnk = $true
|
||||
$stat.isreg = $false
|
||||
$stat.lnk_target = $link_info.TargetPath
|
||||
$stat.lnk_source = $link_info.AbsolutePath
|
||||
break
|
||||
}
|
||||
"JunctionPoint" {
|
||||
$stat.isjunction = $true
|
||||
$stat.isreg = $false
|
||||
$stat.lnk_target = $link_info.TargetPath
|
||||
$stat.lnk_source = $link_info.AbsolutePath
|
||||
break
|
||||
}
|
||||
"HardLink" {
|
||||
$stat.lnk_type = "hard"
|
||||
$stat.nlink = $link_info.HardTargets.Count
|
||||
|
||||
# remove current path from the targets
|
||||
$hlnk_targets = $link_info.HardTargets | Where-Object { $_ -ne $stat.path }
|
||||
$stat.hlnk_targets = @($hlnk_targets)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$module.Result.stat = $stat
|
||||
}
|
||||
|
||||
$module.ExitJson()
|
||||
|
||||
@ -1,236 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
# this is a windows documentation stub. actual code lives in the .ps1
|
||||
# file of the same name
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['stableinterface'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_stat
|
||||
version_added: "1.7"
|
||||
short_description: Get information about Windows files
|
||||
description:
|
||||
- Returns information about a Windows file.
|
||||
- For non-Windows targets, use the M(stat) module instead.
|
||||
options:
|
||||
path:
|
||||
description:
|
||||
- The full path of the file/object to get the facts of; both forward and
|
||||
back slashes are accepted.
|
||||
type: path
|
||||
required: yes
|
||||
aliases: [ dest, name ]
|
||||
get_checksum:
|
||||
description:
|
||||
- Whether to return a checksum of the file (default sha1)
|
||||
type: bool
|
||||
default: yes
|
||||
version_added: "2.1"
|
||||
checksum_algorithm:
|
||||
description:
|
||||
- Algorithm to determine checksum of file.
|
||||
- Will throw an error if the host is unable to use specified algorithm.
|
||||
type: str
|
||||
default: sha1
|
||||
choices: [ md5, sha1, sha256, sha384, sha512 ]
|
||||
version_added: "2.3"
|
||||
follow:
|
||||
description:
|
||||
- Whether to follow symlinks or junction points.
|
||||
- In the case of C(path) pointing to another link, then that will
|
||||
be followed until no more links are found.
|
||||
type: bool
|
||||
default: no
|
||||
version_added: "2.8"
|
||||
seealso:
|
||||
- module: stat
|
||||
- module: win_acl
|
||||
- module: win_file
|
||||
- module: win_owner
|
||||
author:
|
||||
- Chris Church (@cchurch)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Obtain information about a file
|
||||
win_stat:
|
||||
path: C:\foo.ini
|
||||
register: file_info
|
||||
|
||||
- name: Obtain information about a folder
|
||||
win_stat:
|
||||
path: C:\bar
|
||||
register: folder_info
|
||||
|
||||
- name: Get MD5 checksum of a file
|
||||
win_stat:
|
||||
path: C:\foo.ini
|
||||
get_checksum: yes
|
||||
checksum_algorithm: md5
|
||||
register: md5_checksum
|
||||
|
||||
- debug:
|
||||
var: md5_checksum.stat.checksum
|
||||
|
||||
- name: Get SHA1 checksum of file
|
||||
win_stat:
|
||||
path: C:\foo.ini
|
||||
get_checksum: yes
|
||||
register: sha1_checksum
|
||||
|
||||
- debug:
|
||||
var: sha1_checksum.stat.checksum
|
||||
|
||||
- name: Get SHA256 checksum of file
|
||||
win_stat:
|
||||
path: C:\foo.ini
|
||||
get_checksum: yes
|
||||
checksum_algorithm: sha256
|
||||
register: sha256_checksum
|
||||
|
||||
- debug:
|
||||
var: sha256_checksum.stat.checksum
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
changed:
|
||||
description: Whether anything was changed
|
||||
returned: always
|
||||
type: bool
|
||||
sample: true
|
||||
stat:
|
||||
description: dictionary containing all the stat data
|
||||
returned: success
|
||||
type: complex
|
||||
contains:
|
||||
attributes:
|
||||
description: Attributes of the file at path in raw form.
|
||||
returned: success, path exists
|
||||
type: str
|
||||
sample: "Archive, Hidden"
|
||||
checksum:
|
||||
description: The checksum of a file based on checksum_algorithm specified.
|
||||
returned: success, path exist, path is a file, get_checksum == True
|
||||
checksum_algorithm specified is supported
|
||||
type: str
|
||||
sample: 09cb79e8fc7453c84a07f644e441fd81623b7f98
|
||||
creationtime:
|
||||
description: The create time of the file represented in seconds since epoch.
|
||||
returned: success, path exists
|
||||
type: float
|
||||
sample: 1477984205.15
|
||||
exists:
|
||||
description: If the path exists or not.
|
||||
returned: success
|
||||
type: bool
|
||||
sample: true
|
||||
extension:
|
||||
description: The extension of the file at path.
|
||||
returned: success, path exists, path is a file
|
||||
type: str
|
||||
sample: ".ps1"
|
||||
filename:
|
||||
description: The name of the file (without path).
|
||||
returned: success, path exists, path is a file
|
||||
type: str
|
||||
sample: foo.ini
|
||||
hlnk_targets:
|
||||
description: List of other files pointing to the same file (hard links), excludes the current file.
|
||||
returned: success, path exists
|
||||
type: list
|
||||
sample:
|
||||
- C:\temp\file.txt
|
||||
- C:\Windows\update.log
|
||||
isarchive:
|
||||
description: If the path is ready for archiving or not.
|
||||
returned: success, path exists
|
||||
type: bool
|
||||
sample: true
|
||||
isdir:
|
||||
description: If the path is a directory or not.
|
||||
returned: success, path exists
|
||||
type: bool
|
||||
sample: true
|
||||
ishidden:
|
||||
description: If the path is hidden or not.
|
||||
returned: success, path exists
|
||||
type: bool
|
||||
sample: true
|
||||
isjunction:
|
||||
description: If the path is a junction point or not.
|
||||
returned: success, path exists
|
||||
type: bool
|
||||
sample: true
|
||||
islnk:
|
||||
description: If the path is a symbolic link or not.
|
||||
returned: success, path exists
|
||||
type: bool
|
||||
sample: true
|
||||
isreadonly:
|
||||
description: If the path is read only or not.
|
||||
returned: success, path exists
|
||||
type: bool
|
||||
sample: true
|
||||
isreg:
|
||||
description: If the path is a regular file.
|
||||
returned: success, path exists
|
||||
type: bool
|
||||
sample: true
|
||||
isshared:
|
||||
description: If the path is shared or not.
|
||||
returned: success, path exists
|
||||
type: bool
|
||||
sample: true
|
||||
lastaccesstime:
|
||||
description: The last access time of the file represented in seconds since epoch.
|
||||
returned: success, path exists
|
||||
type: float
|
||||
sample: 1477984205.15
|
||||
lastwritetime:
|
||||
description: The last modification time of the file represented in seconds since epoch.
|
||||
returned: success, path exists
|
||||
type: float
|
||||
sample: 1477984205.15
|
||||
lnk_source:
|
||||
description: Target of the symlink normalized for the remote filesystem.
|
||||
returned: success, path exists and the path is a symbolic link or junction point
|
||||
type: str
|
||||
sample: C:\temp\link
|
||||
lnk_target:
|
||||
description: Target of the symlink. Note that relative paths remain relative.
|
||||
returned: success, path exists and the path is a symbolic link or junction point
|
||||
type: str
|
||||
sample: ..\link
|
||||
nlink:
|
||||
description: Number of links to the file (hard links).
|
||||
returned: success, path exists
|
||||
type: int
|
||||
sample: 1
|
||||
owner:
|
||||
description: The owner of the file.
|
||||
returned: success, path exists
|
||||
type: str
|
||||
sample: BUILTIN\Administrators
|
||||
path:
|
||||
description: The full absolute path to the file.
|
||||
returned: success, path exists, file exists
|
||||
type: str
|
||||
sample: C:\foo.ini
|
||||
sharename:
|
||||
description: The name of share if folder is shared.
|
||||
returned: success, path exists, file is a directory and isshared == True
|
||||
type: str
|
||||
sample: file-share
|
||||
size:
|
||||
description: The size in bytes of a file or folder.
|
||||
returned: success, path exists, file is not a link
|
||||
type: int
|
||||
sample: 1024
|
||||
'''
|
||||
@ -1,72 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2017, Dag Wieers (@dagwieers) <dag@wieers.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#AnsibleRequires -CSharpUtil Ansible.Basic
|
||||
|
||||
Function New-TempFile {
|
||||
Param ([string]$path, [string]$prefix, [string]$suffix, [string]$type, [bool]$checkmode)
|
||||
$temppath = $null
|
||||
$curerror = $null
|
||||
$attempt = 0
|
||||
|
||||
# Since we don't know if the file already exists, we try 5 times with a random name
|
||||
do {
|
||||
$attempt += 1
|
||||
$randomname = [System.IO.Path]::GetRandomFileName()
|
||||
$temppath = (Join-Path -Path $path -ChildPath "$prefix$randomname$suffix")
|
||||
Try {
|
||||
$file = New-Item -Path $temppath -ItemType $type -WhatIf:$checkmode
|
||||
# Makes sure we get the full absolute path of the created temp file and not a relative or DOS 8.3 dir
|
||||
if (-not $checkmode) {
|
||||
$temppath = $file.FullName
|
||||
} else {
|
||||
# Just rely on GetFulLpath for check mode
|
||||
$temppath = [System.IO.Path]::GetFullPath($temppath)
|
||||
}
|
||||
} Catch {
|
||||
$temppath = $null
|
||||
$curerror = $_
|
||||
}
|
||||
} until (($null -ne $temppath) -or ($attempt -ge 5))
|
||||
|
||||
# If it fails 5 times, something is wrong and we have to report the details
|
||||
if ($null -eq $temppath) {
|
||||
$module.FailJson("No random temporary file worked in $attempt attempts. Error: $($curerror.Exception.Message)", $curerror)
|
||||
}
|
||||
|
||||
return $temppath.ToString()
|
||||
}
|
||||
|
||||
$spec = @{
|
||||
options = @{
|
||||
path = @{ type='path'; default='%TEMP%'; aliases=@( 'dest' ) }
|
||||
state = @{ type='str'; default='file'; choices=@( 'directory', 'file') }
|
||||
prefix = @{ type='str'; default='ansible.' }
|
||||
suffix = @{ type='str' }
|
||||
}
|
||||
supports_check_mode = $true
|
||||
}
|
||||
|
||||
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||||
|
||||
$path = $module.Params.path
|
||||
$state = $module.Params.state
|
||||
$prefix = $module.Params.prefix
|
||||
$suffix = $module.Params.suffix
|
||||
|
||||
# Expand environment variables on non-path types
|
||||
if ($null -ne $prefix) {
|
||||
$prefix = [System.Environment]::ExpandEnvironmentVariables($prefix)
|
||||
}
|
||||
if ($null -ne $suffix) {
|
||||
$suffix = [System.Environment]::ExpandEnvironmentVariables($suffix)
|
||||
}
|
||||
|
||||
$module.Result.changed = $true
|
||||
$module.Result.state = $state
|
||||
|
||||
$module.Result.path = New-TempFile -Path $path -Prefix $prefix -Suffix $suffix -Type $state -CheckMode $module.CheckMode
|
||||
|
||||
$module.ExitJson()
|
||||
@ -1,67 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2017, Dag Wieers <dag@wieers.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_tempfile
|
||||
version_added: "2.3"
|
||||
short_description: Creates temporary files and directories
|
||||
description:
|
||||
- Creates temporary files and directories.
|
||||
- For non-Windows targets, please use the M(tempfile) module instead.
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- Whether to create file or directory.
|
||||
type: str
|
||||
choices: [ directory, file ]
|
||||
default: file
|
||||
path:
|
||||
description:
|
||||
- Location where temporary file or directory should be created.
|
||||
- If path is not specified default system temporary directory (%TEMP%) will be used.
|
||||
type: path
|
||||
default: '%TEMP%'
|
||||
aliases: [ dest ]
|
||||
prefix:
|
||||
description:
|
||||
- Prefix of file/directory name created by module.
|
||||
type: str
|
||||
default: ansible.
|
||||
suffix:
|
||||
description:
|
||||
- Suffix of file/directory name created by module.
|
||||
type: str
|
||||
default: ''
|
||||
seealso:
|
||||
- module: tempfile
|
||||
author:
|
||||
- Dag Wieers (@dagwieers)
|
||||
'''
|
||||
|
||||
EXAMPLES = r"""
|
||||
- name: Create temporary build directory
|
||||
win_tempfile:
|
||||
state: directory
|
||||
suffix: build
|
||||
|
||||
- name: Create temporary file
|
||||
win_tempfile:
|
||||
state: file
|
||||
suffix: temp
|
||||
"""
|
||||
|
||||
RETURN = r'''
|
||||
path:
|
||||
description: The absolute path to the created file or directory.
|
||||
returned: success
|
||||
type: str
|
||||
sample: C:\Users\Administrator\AppData\Local\Temp\ansible.bMlvdk
|
||||
'''
|
||||
@ -1,66 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
# this is a virtual module that is entirely implemented server side
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['stableinterface'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_template
|
||||
version_added: "1.9.2"
|
||||
short_description: Template a file out to a remote server
|
||||
options:
|
||||
backup:
|
||||
description:
|
||||
- Determine whether a backup should be created.
|
||||
- When set to C(yes), create a backup file including the timestamp information
|
||||
so you can get the original file back if you somehow clobbered it incorrectly.
|
||||
type: bool
|
||||
default: no
|
||||
version_added: '2.8'
|
||||
newline_sequence:
|
||||
default: '\r\n'
|
||||
force:
|
||||
version_added: '2.4'
|
||||
notes:
|
||||
- Beware fetching files from windows machines when creating templates because certain tools, such as Powershell ISE,
|
||||
and regedit's export facility add a Byte Order Mark as the first character of the file, which can cause tracebacks.
|
||||
- You can use the M(win_copy) module with the C(content:) option if you prefer the template inline, as part of the
|
||||
playbook.
|
||||
- For Linux you can use M(template) which uses '\\n' as C(newline_sequence) by default.
|
||||
seealso:
|
||||
- module: win_copy
|
||||
- module: copy
|
||||
- module: template
|
||||
author:
|
||||
- Jon Hawkesworth (@jhawkesworth)
|
||||
extends_documentation_fragment:
|
||||
- template_common
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Create a file from a Jinja2 template
|
||||
win_template:
|
||||
src: /mytemplates/file.conf.j2
|
||||
dest: C:\Temp\file.conf
|
||||
|
||||
- name: Create a Unix-style file from a Jinja2 template
|
||||
win_template:
|
||||
src: unix/config.conf.j2
|
||||
dest: C:\share\unix\config.conf
|
||||
newline_sequence: '\n'
|
||||
backup: yes
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
backup_file:
|
||||
description: Name of the backup file that was created.
|
||||
returned: if backup=yes
|
||||
type: str
|
||||
sample: C:\Path\To\File.txt.11540.20150212-220915.bak
|
||||
'''
|
||||
@ -1,591 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2015, Matt Davis <mdavis@rolpdog.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"
|
||||
|
||||
$params = Parse-Args -arguments $args -supports_check_mode $true
|
||||
$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
|
||||
|
||||
$category_names = Get-AnsibleParam -obj $params -name "category_names" -type "list" -default @("CriticalUpdates", "SecurityUpdates", "UpdateRollups")
|
||||
$log_path = Get-AnsibleParam -obj $params -name "log_path" -type "path"
|
||||
$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "installed" -validateset "installed", "searched", "downloaded"
|
||||
$blacklist = Get-AnsibleParam -obj $params -name "blacklist" -type "list"
|
||||
$whitelist = Get-AnsibleParam -obj $params -name "whitelist" -type "list"
|
||||
$server_selection = Get-AnsibleParam -obj $params -name "server_selection" -type "string" -default "default" -validateset "default", "managed_server", "windows_update"
|
||||
|
||||
# For backwards compatibility
|
||||
Function Get-CategoryMapping ($category_name) {
|
||||
switch -exact ($category_name) {
|
||||
"CriticalUpdates" {return "Critical Updates"}
|
||||
"DefinitionUpdates" {return "Definition Updates"}
|
||||
"DeveloperKits" {return "Developer Kits"}
|
||||
"FeaturePacks" {return "Feature Packs"}
|
||||
"SecurityUpdates" {return "Security Updates"}
|
||||
"ServicePacks" {return "Service Packs"}
|
||||
"UpdateRollups" {return "Update Rollups"}
|
||||
default {return $category_name}
|
||||
}
|
||||
}
|
||||
|
||||
$category_names = $category_names | ForEach-Object { Get-CategoryMapping -category_name $_ }
|
||||
|
||||
$common_functions = {
|
||||
Function Write-DebugLog($msg) {
|
||||
$date_str = Get-Date -Format u
|
||||
$msg = "$date_str $msg"
|
||||
|
||||
Write-Debug -Message $msg
|
||||
if ($null -ne $log_path -and (-not $check_mode)) {
|
||||
Add-Content -Path $log_path -Value $msg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$update_script_block = {
|
||||
Param(
|
||||
[hashtable]$arguments
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$DebugPreference = "Continue"
|
||||
|
||||
Function Start-Updates {
|
||||
Param(
|
||||
$category_names,
|
||||
$log_path,
|
||||
$state,
|
||||
$blacklist,
|
||||
$whitelist,
|
||||
$server_selection
|
||||
)
|
||||
|
||||
$result = @{
|
||||
changed = $false
|
||||
updates = @{}
|
||||
filtered_updates = @{}
|
||||
}
|
||||
|
||||
Write-DebugLog -msg "Creating Windows Update session..."
|
||||
try {
|
||||
$session = New-Object -ComObject Microsoft.Update.Session
|
||||
} catch {
|
||||
$result.failed = $true
|
||||
$result.msg = "Failed to create Microsoft.Update.Session COM object: $($_.Exception.Message)"
|
||||
return $result
|
||||
}
|
||||
|
||||
Write-DebugLog -msg "Create Windows Update searcher..."
|
||||
try {
|
||||
$searcher = $session.CreateUpdateSearcher()
|
||||
} catch {
|
||||
$result.failed = $true
|
||||
$result.msg = "Failed to create Windows Update search from session: $($_.Exception.Message)"
|
||||
return $result
|
||||
}
|
||||
|
||||
Write-DebugLog -msg "Setting the Windows Update Agent source catalog..."
|
||||
Write-DebugLog -msg "Requested search source is '$($server_selection)'"
|
||||
try {
|
||||
$server_selection_value = switch ($server_selection) {
|
||||
"default" { 0 ; break }
|
||||
"managed_server" { 1 ; break }
|
||||
"windows_update" { 2 ; break }
|
||||
}
|
||||
$searcher.serverselection = $server_selection_value
|
||||
Write-DebugLog -msg "Search source set to '$($server_selection)' (ServerSelection = $($server_selection_value))"
|
||||
}
|
||||
catch {
|
||||
$result.failed = $true
|
||||
$result.msg = "Failed to set Windows Update Agent search source: $($_.Exception.Message)"
|
||||
return $result
|
||||
}
|
||||
|
||||
Write-DebugLog -msg "Searching for updates to install"
|
||||
try {
|
||||
$search_result = $searcher.Search("IsInstalled = 0")
|
||||
} catch {
|
||||
$result.failed = $true
|
||||
$result.msg = "Failed to search for updates: $($_.Exception.Message)"
|
||||
return $result
|
||||
}
|
||||
Write-DebugLog -msg "Found $($search_result.Updates.Count) updates"
|
||||
|
||||
Write-DebugLog -msg "Creating update collection..."
|
||||
try {
|
||||
$updates_to_install = New-Object -ComObject Microsoft.Update.UpdateColl
|
||||
} catch {
|
||||
$result.failed = $true
|
||||
$result.msg = "Failed to create update collection object: $($_.Exception.Message)"
|
||||
return $result
|
||||
}
|
||||
|
||||
foreach ($update in $search_result.Updates) {
|
||||
$update_info = @{
|
||||
title = $update.Title
|
||||
# TODO: pluck the first KB out (since most have just one)?
|
||||
kb = $update.KBArticleIDs
|
||||
id = $update.Identity.UpdateId
|
||||
installed = $false
|
||||
categories = @($update.Categories | ForEach-Object { $_.Name })
|
||||
}
|
||||
|
||||
# validate update again blacklist/whitelist/post_category_names/hidden
|
||||
$whitelist_match = $false
|
||||
foreach ($whitelist_entry in $whitelist) {
|
||||
if ($update_info.title -imatch $whitelist_entry) {
|
||||
$whitelist_match = $true
|
||||
break
|
||||
}
|
||||
foreach ($kb in $update_info.kb) {
|
||||
if ("KB$kb" -imatch $whitelist_entry) {
|
||||
$whitelist_match = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($whitelist.Length -gt 0 -and -not $whitelist_match) {
|
||||
Write-DebugLog -msg "Skipping update $($update_info.id) - $($update_info.title) as it was not found in the whitelist"
|
||||
$update_info.filtered_reason = "whitelist"
|
||||
$result.filtered_updates[$update_info.id] = $update_info
|
||||
continue
|
||||
}
|
||||
|
||||
$blacklist_match = $false
|
||||
foreach ($blacklist_entry in $blacklist) {
|
||||
if ($update_info.title -imatch $blacklist_entry) {
|
||||
$blacklist_match = $true
|
||||
break
|
||||
}
|
||||
foreach ($kb in $update_info.kb) {
|
||||
if ("KB$kb" -imatch $blacklist_entry) {
|
||||
$blacklist_match = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($blacklist_match) {
|
||||
Write-DebugLog -msg "Skipping update $($update_info.id) - $($update_info.title) as it was found in the blacklist"
|
||||
$update_info.filtered_reason = "blacklist"
|
||||
$result.filtered_updates[$update_info.id] = $update_info
|
||||
continue
|
||||
}
|
||||
|
||||
if ($update.IsHidden) {
|
||||
Write-DebugLog -msg "Skipping update $($update_info.title) as it was hidden"
|
||||
$update_info.filtered_reason = "skip_hidden"
|
||||
$result.filtered_updates[$update_info.id] = $update_info
|
||||
continue
|
||||
}
|
||||
|
||||
$category_match = $false
|
||||
foreach ($match_cat in $category_names) {
|
||||
if ($update_info.categories -ieq $match_cat) {
|
||||
$category_match = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
if ($category_names.Length -gt 0 -and -not $category_match) {
|
||||
Write-DebugLog -msg "Skipping update $($update_info.id) - $($update_info.title) as it was not found in the category names filter"
|
||||
$update_info.filtered_reason = "category_names"
|
||||
$result.filtered_updates[$update_info.id] = $update_info
|
||||
continue
|
||||
}
|
||||
|
||||
if (-not $update.EulaAccepted) {
|
||||
Write-DebugLog -msg "Accepting EULA for $($update_info.id)"
|
||||
try {
|
||||
$update.AcceptEula()
|
||||
} catch {
|
||||
$result.failed = $true
|
||||
$result.msg = "Failed to accept EULA for update $($update_info.id) - $($update_info.title)"
|
||||
return $result
|
||||
}
|
||||
}
|
||||
|
||||
Write-DebugLog -msg "Adding update $($update_info.id) - $($update_info.title)"
|
||||
$updates_to_install.Add($update) > $null
|
||||
|
||||
$result.updates[$update_info.id] = $update_info
|
||||
}
|
||||
|
||||
Write-DebugLog -msg "Calculating pre-install reboot requirement..."
|
||||
|
||||
# calculate this early for check mode, and to see if we should allow updates to continue
|
||||
$result.reboot_required = (New-Object -ComObject Microsoft.Update.SystemInfo).RebootRequired
|
||||
$result.found_update_count = $updates_to_install.Count
|
||||
$result.installed_update_count = 0
|
||||
|
||||
# Early exit of check mode/state=searched as it cannot do more after this
|
||||
if ($check_mode -or $state -eq "searched") {
|
||||
Write-DebugLog -msg "Check mode: exiting..."
|
||||
Write-DebugLog -msg "Return value:`r`n$(ConvertTo-Json -InputObject $result -Depth 99)"
|
||||
|
||||
if ($updates_to_install.Count -gt 0 -and ($state -ne "searched")) {
|
||||
$result.changed = $true
|
||||
}
|
||||
return $result
|
||||
}
|
||||
|
||||
if ($updates_to_install.Count -gt 0) {
|
||||
if ($result.reboot_required) {
|
||||
Write-DebugLog -msg "FATAL: A reboot is required before more updates can be installed"
|
||||
$result.failed = $true
|
||||
$result.msg = "A reboot is required before more updates can be installed"
|
||||
return $result
|
||||
}
|
||||
Write-DebugLog -msg "No reboot is pending..."
|
||||
} else {
|
||||
# no updates to install exit here
|
||||
return $result
|
||||
}
|
||||
|
||||
Write-DebugLog -msg "Downloading updates..."
|
||||
$update_index = 1
|
||||
foreach ($update in $updates_to_install) {
|
||||
$update_number = "($update_index of $($updates_to_install.Count))"
|
||||
if ($update.IsDownloaded) {
|
||||
Write-DebugLog -msg "Update $update_number $($update.Identity.UpdateId) already downloaded, skipping..."
|
||||
$update_index++
|
||||
continue
|
||||
}
|
||||
|
||||
Write-DebugLog -msg "Creating downloader object..."
|
||||
try {
|
||||
$dl = $session.CreateUpdateDownloader()
|
||||
} catch {
|
||||
$result.failed = $true
|
||||
$result.msg = "Failed to create downloader object: $($_.Exception.Message)"
|
||||
return $result
|
||||
}
|
||||
|
||||
Write-DebugLog -msg "Creating download collection..."
|
||||
try {
|
||||
$dl.Updates = New-Object -ComObject Microsoft.Update.UpdateColl
|
||||
} catch {
|
||||
$result.failed = $true
|
||||
$result.msg = "Failed to create download collection object: $($_.Exception.Message)"
|
||||
return $result
|
||||
}
|
||||
|
||||
Write-DebugLog -msg "Adding update $update_number $($update.Identity.UpdateId)"
|
||||
$dl.Updates.Add($update) > $null
|
||||
|
||||
Write-DebugLog -msg "Downloading $update_number $($update.Identity.UpdateId)"
|
||||
try {
|
||||
$download_result = $dl.Download()
|
||||
} catch {
|
||||
$result.failed = $true
|
||||
$result.msg = "Failed to download update $update_number $($update.Identity.UpdateId) - $($update.Title): $($_.Exception.Message)"
|
||||
return $result
|
||||
}
|
||||
|
||||
Write-DebugLog -msg "Download result code for $update_number $($update.Identity.UpdateId) = $($download_result.ResultCode)"
|
||||
# FUTURE: configurable download retry
|
||||
if ($download_result.ResultCode -ne 2) { # OperationResultCode orcSucceeded
|
||||
$result.failed = $true
|
||||
$result.msg = "Failed to download update $update_number $($update.Identity.UpdateId) - $($update.Title): Download Result $($download_result.ResultCode)"
|
||||
return $result
|
||||
}
|
||||
|
||||
$result.changed = $true
|
||||
$update_index++
|
||||
}
|
||||
|
||||
# Early exit for download-only
|
||||
if ($state -eq "downloaded") {
|
||||
Write-DebugLog -msg "Downloaded $($updates_to_install.Count) updates..."
|
||||
$result.failed = $false
|
||||
$result.msg = "Downloaded $($updates_to_install.Count) updates"
|
||||
return $result
|
||||
}
|
||||
|
||||
Write-DebugLog -msg "Installing updates..."
|
||||
# install as a batch so the reboot manager will suppress intermediate reboots
|
||||
|
||||
Write-DebugLog -msg "Creating installer object..."
|
||||
try {
|
||||
$installer = $session.CreateUpdateInstaller()
|
||||
} catch {
|
||||
$result.failed = $true
|
||||
$result.msg = "Failed to create Update Installer object: $($_.Exception.Message)"
|
||||
return $result
|
||||
}
|
||||
|
||||
Write-DebugLog -msg "Creating install collection..."
|
||||
try {
|
||||
$installer.Updates = New-Object -ComObject Microsoft.Update.UpdateColl
|
||||
} catch {
|
||||
$result.failed = $true
|
||||
$result.msg = "Failed to create Update Collection object: $($_.Exception.Message)"
|
||||
return $result
|
||||
}
|
||||
|
||||
foreach ($update in $updates_to_install) {
|
||||
Write-DebugLog -msg "Adding update $($update.Identity.UpdateID)"
|
||||
$installer.Updates.Add($update) > $null
|
||||
}
|
||||
|
||||
# FUTURE: use BeginInstall w/ progress reporting so we can at least log intermediate install results
|
||||
try {
|
||||
$install_result = $installer.Install()
|
||||
} catch {
|
||||
$result.failed = $true
|
||||
$result.msg = "Failed to install update from Update Collection: $($_.Exception.Message)"
|
||||
return $result
|
||||
}
|
||||
|
||||
$update_success_count = 0
|
||||
$update_fail_count = 0
|
||||
|
||||
# WU result API requires us to index in to get the install results
|
||||
$update_index = 0
|
||||
foreach ($update in $updates_to_install) {
|
||||
$update_number = "($($update_index + 1) of $($updates_to_install.Count))"
|
||||
try {
|
||||
$update_result = $install_result.GetUpdateResult($update_index)
|
||||
} catch {
|
||||
$result.failed = $true
|
||||
$result.msg = "Failed to get update result for update $update_number $($update.Identity.UpdateID) - $($update.Title): $($_.Exception.Message)"
|
||||
return $result
|
||||
}
|
||||
$update_resultcode = $update_result.ResultCode
|
||||
$update_hresult = $update_result.HResult
|
||||
|
||||
$update_index++
|
||||
|
||||
$update_dict = $result.updates[$update.Identity.UpdateID]
|
||||
if ($update_resultcode -eq 2) { # OperationResultCode orcSucceeded
|
||||
$update_success_count++
|
||||
$update_dict.installed = $true
|
||||
Write-DebugLog -msg "Update $update_number $($update.Identity.UpdateID) succeeded"
|
||||
} else {
|
||||
$update_fail_count++
|
||||
$update_dict.installed = $false
|
||||
$update_dict.failed = $true
|
||||
$update_dict.failure_hresult_code = $update_hresult
|
||||
Write-DebugLog -msg "Update $update_number $($update.Identity.UpdateID) failed, resultcode: $update_resultcode, hresult: $update_hresult"
|
||||
}
|
||||
}
|
||||
|
||||
Write-DebugLog -msg "Performing post-install reboot requirement check..."
|
||||
$result.reboot_required = (New-Object -ComObject Microsoft.Update.SystemInfo).RebootRequired
|
||||
$result.installed_update_count = $update_success_count
|
||||
$result.failed_update_count = $update_fail_count
|
||||
|
||||
if ($updates_success_count -gt 0) {
|
||||
$result.changed = $true
|
||||
}
|
||||
|
||||
if ($update_fail_count -gt 0) {
|
||||
$result.failed = $true
|
||||
$result.msg = "Failed to install one or more updates"
|
||||
return $result
|
||||
}
|
||||
|
||||
Write-DebugLog -msg "Return value:`r`n$(ConvertTo-Json -InputObject $result -Depth 99)"
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
$check_mode = $arguments.check_mode
|
||||
try {
|
||||
return @{
|
||||
job_output = Start-Updates @arguments
|
||||
}
|
||||
} catch {
|
||||
Write-DebugLog -msg "Fatal exception: $($_.Exception.Message) at $($_.ScriptStackTrace)"
|
||||
return @{
|
||||
job_output = @{
|
||||
failed = $true
|
||||
msg = $_.Exception.Message
|
||||
location = $_.ScriptStackTrace
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Function Start-Natively($common_functions, $script) {
|
||||
$runspace_pool = [RunspaceFactory]::CreateRunspacePool()
|
||||
$runspace_pool.Open()
|
||||
|
||||
try {
|
||||
$ps_pipeline = [PowerShell]::Create()
|
||||
$ps_pipeline.RunspacePool = $runspace_pool
|
||||
|
||||
# add the common script functions
|
||||
$ps_pipeline.AddScript($common_functions) > $null
|
||||
|
||||
# add the update script block and required parameters
|
||||
$ps_pipeline.AddStatement().AddScript($script) > $null
|
||||
$ps_pipeline.AddParameter("arguments", @{
|
||||
category_names = $category_names
|
||||
log_path = $log_path
|
||||
state = $state
|
||||
blacklist = $blacklist
|
||||
whitelist = $whitelist
|
||||
check_mode = $check_mode
|
||||
server_selection = $server_selection
|
||||
}) > $null
|
||||
|
||||
$output = $ps_pipeline.Invoke()
|
||||
} finally {
|
||||
$runspace_pool.Close()
|
||||
}
|
||||
|
||||
$result = $output[0].job_output
|
||||
if ($ps_pipeline.HadErrors) {
|
||||
$result.failed = $true
|
||||
|
||||
# if the msg wasn't set, then add a generic error to at least tell the user something
|
||||
if (-not ($result.ContainsKey("msg"))) {
|
||||
$result.msg = "Unknown failure when executing native update script block"
|
||||
$result.errors = $ps_pipeline.Streams.Error
|
||||
}
|
||||
}
|
||||
|
||||
Write-DebugLog -msg "Native job completed with output: $($result | Out-String -Width 300)"
|
||||
|
||||
return ,$result
|
||||
}
|
||||
|
||||
Function Remove-ScheduledJob($name) {
|
||||
$scheduled_job = Get-ScheduledJob -Name $name -ErrorAction SilentlyContinue
|
||||
|
||||
if ($null -ne $scheduled_job) {
|
||||
Write-DebugLog -msg "Scheduled Job $name exists, ensuring it is not running..."
|
||||
$scheduler = New-Object -ComObject Schedule.Service
|
||||
Write-DebugLog -msg "Connecting to scheduler service..."
|
||||
$scheduler.Connect()
|
||||
Write-DebugLog -msg "Getting running tasks named $name"
|
||||
$running_tasks = @($scheduler.GetRunningTasks(0) | Where-Object { $_.Name -eq $name })
|
||||
|
||||
foreach ($task_to_stop in $running_tasks) {
|
||||
Write-DebugLog -msg "Stopping running task $($task_to_stop.InstanceGuid)..."
|
||||
$task_to_stop.Stop()
|
||||
}
|
||||
|
||||
<# FUTURE: add a global waithandle for this to release any other waiters. Wait-Job
|
||||
and/or polling will block forever, since the killed job object in the parent
|
||||
session doesn't know it's been killed :( #>
|
||||
Unregister-ScheduledJob -Name $name
|
||||
}
|
||||
}
|
||||
|
||||
Function Start-AsScheduledTask($common_functions, $script) {
|
||||
$job_name = "ansible-win-updates"
|
||||
Remove-ScheduledJob -name $job_name
|
||||
|
||||
$job_args = @{
|
||||
ScriptBlock = $script
|
||||
Name = $job_name
|
||||
ArgumentList = @(
|
||||
@{
|
||||
category_names = $category_names
|
||||
log_path = $log_path
|
||||
state = $state
|
||||
blacklist = $blacklist
|
||||
whitelist = $whitelist
|
||||
check_mode = $check_mode
|
||||
server_selection = $server_selection
|
||||
}
|
||||
)
|
||||
ErrorAction = "Stop"
|
||||
ScheduledJobOption = @{ RunElevated=$True; StartIfOnBatteries=$True; StopIfGoingOnBatteries=$False }
|
||||
InitializationScript = $common_functions
|
||||
}
|
||||
|
||||
Write-DebugLog -msg "Registering scheduled job with args $($job_args | Out-String -Width 300)"
|
||||
$scheduled_job = Register-ScheduledJob @job_args
|
||||
|
||||
# RunAsTask isn't available in PS3 - fall back to a 2s future trigger
|
||||
if ($scheduled_job | Get-Member -Name RunAsTask) {
|
||||
Write-DebugLog -msg "Starting scheduled job (PS4+ method)"
|
||||
$scheduled_job.RunAsTask()
|
||||
} else {
|
||||
Write-DebugLog -msg "Starting scheduled job (PS3 method)"
|
||||
Add-JobTrigger -InputObject $scheduled_job -trigger $(New-JobTrigger -Once -At $(Get-Date).AddSeconds(2))
|
||||
}
|
||||
|
||||
$sw = [System.Diagnostics.Stopwatch]::StartNew()
|
||||
$job = $null
|
||||
|
||||
Write-DebugLog -msg "Waiting for job completion..."
|
||||
|
||||
# Wait-Job can fail for a few seconds until the scheduled task starts - poll for it...
|
||||
while ($null -eq $job) {
|
||||
Start-Sleep -Milliseconds 100
|
||||
if ($sw.ElapsedMilliseconds -ge 30000) { # tasks scheduled right after boot on 2008R2 can take awhile to start...
|
||||
Fail-Json -msg "Timed out waiting for scheduled task to start"
|
||||
}
|
||||
|
||||
# FUTURE: configurable timeout so we don't block forever?
|
||||
# FUTURE: add a global WaitHandle in case another instance kills our job, so we don't block forever
|
||||
$job = Wait-Job -Name $scheduled_job.Name -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
$sw = [System.Diagnostics.Stopwatch]::StartNew()
|
||||
|
||||
# NB: output from scheduled jobs is delayed after completion (including the sub-objects after the primary Output object is available)
|
||||
while (($null -eq $job.Output -or -not ($job.Output | Get-Member -Name Key -ErrorAction Ignore) -or -not $job.Output.Key.Contains("job_output")) -and $sw.ElapsedMilliseconds -lt 15000) {
|
||||
Write-DebugLog -msg "Waiting for job output to populate..."
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
|
||||
# NB: fallthru on both timeout and success
|
||||
$ret = @{
|
||||
ErrorOutput = $job.Error
|
||||
WarningOutput = $job.Warning
|
||||
VerboseOutput = $job.Verbose
|
||||
DebugOutput = $job.Debug
|
||||
}
|
||||
|
||||
if ($null -eq $job.Output -or -not $job.Output.Keys.Contains('job_output')) {
|
||||
$ret.Output = @{failed = $true; msg = "job output was lost"}
|
||||
} else {
|
||||
$ret.Output = $job.Output.job_output # sub-object returned, can only be accessed as a property for some reason
|
||||
}
|
||||
|
||||
try { # this shouldn't be fatal, but can fail with both Powershell errors and COM Exceptions, hence the dual error-handling...
|
||||
Unregister-ScheduledJob -Name $job_name -Force -ErrorAction Continue
|
||||
} catch {
|
||||
Write-DebugLog "Error unregistering job after execution: $($_.Exception.ToString()) $($_.ScriptStackTrace)"
|
||||
}
|
||||
Write-DebugLog -msg "Scheduled job completed with output: $($re.Output | Out-String -Width 300)"
|
||||
|
||||
return $ret.Output
|
||||
}
|
||||
|
||||
# source the common code into the current scope so we can call it
|
||||
. $common_functions
|
||||
|
||||
<# Most of the Windows Update Agent API will not run under a remote token,
|
||||
which a remote WinRM session always has. Using become can bypass this
|
||||
limitation but it is not always an option with older hosts. win_updates checks
|
||||
if WUA is available in the current logon process and does either of the below;
|
||||
|
||||
* If become is used then it will run the windows update process natively
|
||||
without any of the scheduled task hackery
|
||||
* If become is not used then it will run the windows update process under
|
||||
a scheduled job.
|
||||
#>
|
||||
try {
|
||||
(New-Object -ComObject Microsoft.Update.Session).CreateUpdateInstaller().IsBusy > $null
|
||||
$wua_available = $true
|
||||
} catch {
|
||||
$wua_available = $false
|
||||
}
|
||||
|
||||
if ($wua_available) {
|
||||
Write-DebugLog -msg "WUA is available in current logon process, running natively"
|
||||
$result = Start-Natively -common_functions $common_functions -script $update_script_block
|
||||
} else {
|
||||
Write-DebugLog -msg "WUA is not available in current logon process, running with scheduled task"
|
||||
$result = Start-AsScheduledTask -common_functions $common_functions -script $update_script_block
|
||||
}
|
||||
|
||||
Exit-Json -obj $result
|
||||
@ -1,273 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2015, Matt Davis <mdavis_ansible@rolpdog.com>
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
# this is a windows documentation stub. actual code lives in the .ps1
|
||||
# file of the same name
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_updates
|
||||
version_added: "2.0"
|
||||
short_description: Download and install Windows updates
|
||||
description:
|
||||
- Searches, downloads, and installs Windows updates synchronously by automating the Windows Update client.
|
||||
options:
|
||||
blacklist:
|
||||
description:
|
||||
- A list of update titles or KB numbers that can be used to specify
|
||||
which updates are to be excluded from installation.
|
||||
- If an available update does match one of the entries, then it is
|
||||
skipped and not installed.
|
||||
- Each entry can either be the KB article or Update title as a regex
|
||||
according to the PowerShell regex rules.
|
||||
type: list
|
||||
version_added: '2.5'
|
||||
category_names:
|
||||
description:
|
||||
- A scalar or list of categories to install updates from. To get the list
|
||||
of categories, run the module with C(state=searched). The category must
|
||||
be the full category string, but is case insensitive.
|
||||
- Some possible categories are Application, Connectors, Critical Updates,
|
||||
Definition Updates, Developer Kits, Feature Packs, Guidance, Security
|
||||
Updates, Service Packs, Tools, Update Rollups and Updates.
|
||||
type: list
|
||||
default: [ CriticalUpdates, SecurityUpdates, UpdateRollups ]
|
||||
reboot:
|
||||
description:
|
||||
- Ansible will automatically reboot the remote host if it is required
|
||||
and continue to install updates after the reboot.
|
||||
- This can be used instead of using a M(win_reboot) task after this one
|
||||
and ensures all updates for that category is installed in one go.
|
||||
- Async does not work when C(reboot=yes).
|
||||
type: bool
|
||||
default: no
|
||||
version_added: '2.5'
|
||||
reboot_timeout:
|
||||
description:
|
||||
- The time in seconds to wait until the host is back online from a
|
||||
reboot.
|
||||
- This is only used if C(reboot=yes) and a reboot is required.
|
||||
default: 1200
|
||||
version_added: '2.5'
|
||||
server_selection:
|
||||
description:
|
||||
- Defines the Windows Update source catalog.
|
||||
- C(default) Use the default search source. For many systems default is
|
||||
set to the Microsoft Windows Update catalog. Systems participating in
|
||||
Windows Server Update Services (WSUS), Systems Center Configuration
|
||||
Manager (SCCM), or similar corporate update server environments may
|
||||
default to those managed update sources instead of the Windows Update
|
||||
catalog.
|
||||
- C(managed_server) Use a managed server catalog. For environments
|
||||
utilizing Windows Server Update Services (WSUS), Systems Center
|
||||
Configuration Manager (SCCM), or similar corporate update servers, this
|
||||
option selects the defined corporate update source.
|
||||
- C(windows_update) Use the Microsoft Windows Update catalog.
|
||||
type: str
|
||||
choices: [ default, managed_server, windows_update ]
|
||||
default: default
|
||||
version_added: '2.8'
|
||||
state:
|
||||
description:
|
||||
- Controls whether found updates are downloaded or installed or listed
|
||||
- This module also supports Ansible check mode, which has the same effect as setting state=searched
|
||||
type: str
|
||||
choices: [ installed, searched, downloaded ]
|
||||
default: installed
|
||||
log_path:
|
||||
description:
|
||||
- If set, C(win_updates) will append update progress to the specified file. The directory must already exist.
|
||||
type: path
|
||||
whitelist:
|
||||
description:
|
||||
- A list of update titles or KB numbers that can be used to specify
|
||||
which updates are to be searched or installed.
|
||||
- If an available update does not match one of the entries, then it
|
||||
is skipped and not installed.
|
||||
- Each entry can either be the KB article or Update title as a regex
|
||||
according to the PowerShell regex rules.
|
||||
- The whitelist is only validated on updates that were found based on
|
||||
I(category_names). It will not force the module to install an update
|
||||
if it was not in the category specified.
|
||||
type: list
|
||||
version_added: '2.5'
|
||||
use_scheduled_task:
|
||||
description:
|
||||
- Will not auto elevate the remote process with I(become) and use a
|
||||
scheduled task instead.
|
||||
- Set this to C(yes) when using this module with async on Server 2008,
|
||||
2008 R2, or Windows 7, or on Server 2008 that is not authenticated
|
||||
with basic or credssp.
|
||||
- Can also be set to C(yes) on newer hosts where become does not work
|
||||
due to further privilege restrictions from the OS defaults.
|
||||
type: bool
|
||||
default: no
|
||||
version_added: '2.6'
|
||||
notes:
|
||||
- C(win_updates) must be run by a user with membership in the local Administrators group.
|
||||
- C(win_updates) will use the default update service configured for the machine (Windows Update, Microsoft Update, WSUS, etc).
|
||||
- C(win_updates) will I(become) SYSTEM using I(runas) unless C(use_scheduled_task) is C(yes)
|
||||
- By default C(win_updates) does not manage reboots, but will signal when a
|
||||
reboot is required with the I(reboot_required) return value, as of Ansible v2.5
|
||||
C(reboot) can be used to reboot the host if required in the one task.
|
||||
- C(win_updates) can take a significant amount of time to complete (hours, in some cases).
|
||||
Performance depends on many factors, including OS version, number of updates, system load, and update server load.
|
||||
- Beware that just after C(win_updates) reboots the system, the Windows system may not have settled yet
|
||||
and some base services could be in limbo. This can result in unexpected behavior.
|
||||
Check the examples for ways to mitigate this.
|
||||
- More information about PowerShell and how it handles RegEx strings can be
|
||||
found at U(https://technet.microsoft.com/en-us/library/2007.11.powershell.aspx).
|
||||
seealso:
|
||||
- module: win_chocolatey
|
||||
- module: win_feature
|
||||
- module: win_hotfix
|
||||
- module: win_package
|
||||
author:
|
||||
- Matt Davis (@nitzmahone)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Install all security, critical, and rollup updates without a scheduled task
|
||||
win_updates:
|
||||
category_names:
|
||||
- SecurityUpdates
|
||||
- CriticalUpdates
|
||||
- UpdateRollups
|
||||
|
||||
- name: Install only security updates as a scheduled task for Server 2008
|
||||
win_updates:
|
||||
category_names: SecurityUpdates
|
||||
use_scheduled_task: yes
|
||||
|
||||
- name: Search-only, return list of found updates (if any), log to C:\ansible_wu.txt
|
||||
win_updates:
|
||||
category_names: SecurityUpdates
|
||||
state: searched
|
||||
log_path: C:\ansible_wu.txt
|
||||
|
||||
- name: Install all security updates with automatic reboots
|
||||
win_updates:
|
||||
category_names:
|
||||
- SecurityUpdates
|
||||
reboot: yes
|
||||
|
||||
- name: Install only particular updates based on the KB numbers
|
||||
win_updates:
|
||||
category_name:
|
||||
- SecurityUpdates
|
||||
whitelist:
|
||||
- KB4056892
|
||||
- KB4073117
|
||||
|
||||
- name: Exclude updates based on the update title
|
||||
win_updates:
|
||||
category_name:
|
||||
- SecurityUpdates
|
||||
- CriticalUpdates
|
||||
blacklist:
|
||||
- Windows Malicious Software Removal Tool for Windows
|
||||
- \d{4}-\d{2} Cumulative Update for Windows Server 2016
|
||||
|
||||
# One way to ensure the system is reliable just after a reboot, is to set WinRM to a delayed startup
|
||||
- name: Ensure WinRM starts when the system has settled and is ready to work reliably
|
||||
win_service:
|
||||
name: WinRM
|
||||
start_mode: delayed
|
||||
|
||||
# Optionally, you can increase the reboot_timeout to survive long updates during reboot
|
||||
- name: Ensure we wait long enough for the updates to be applied during reboot
|
||||
win_updates:
|
||||
reboot: yes
|
||||
reboot_timeout: 3600
|
||||
|
||||
# Search and download Windows updates
|
||||
- name: Search and download Windows updates without installing them
|
||||
win_updates:
|
||||
state: downloaded
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
reboot_required:
|
||||
description: True when the target server requires a reboot to complete updates (no further updates can be installed until after a reboot).
|
||||
returned: success
|
||||
type: bool
|
||||
sample: true
|
||||
|
||||
updates:
|
||||
description: List of updates that were found/installed.
|
||||
returned: success
|
||||
type: complex
|
||||
sample:
|
||||
contains:
|
||||
title:
|
||||
description: Display name.
|
||||
returned: always
|
||||
type: str
|
||||
sample: "Security Update for Windows Server 2012 R2 (KB3004365)"
|
||||
kb:
|
||||
description: A list of KB article IDs that apply to the update.
|
||||
returned: always
|
||||
type: list
|
||||
elements: str
|
||||
sample: [ '3004365' ]
|
||||
id:
|
||||
description: Internal Windows Update GUID.
|
||||
returned: always
|
||||
type: str
|
||||
sample: "fb95c1c8-de23-4089-ae29-fd3351d55421"
|
||||
installed:
|
||||
description: Was the update successfully installed.
|
||||
returned: always
|
||||
type: bool
|
||||
sample: true
|
||||
categories:
|
||||
description: A list of category strings for this update.
|
||||
returned: always
|
||||
type: list
|
||||
elements: str
|
||||
sample: [ 'Critical Updates', 'Windows Server 2012 R2' ]
|
||||
failure_hresult_code:
|
||||
description: The HRESULT code from a failed update.
|
||||
returned: on install failure
|
||||
type: bool
|
||||
sample: 2147942402
|
||||
|
||||
filtered_updates:
|
||||
description: List of updates that were found but were filtered based on
|
||||
I(blacklist), I(whitelist) or I(category_names). The return value is in
|
||||
the same form as I(updates), along with I(filtered_reason).
|
||||
returned: success
|
||||
type: complex
|
||||
sample: see the updates return value
|
||||
contains:
|
||||
filtered_reason:
|
||||
description: The reason why this update was filtered.
|
||||
returned: always
|
||||
type: str
|
||||
sample: 'skip_hidden'
|
||||
|
||||
found_update_count:
|
||||
description: The number of updates found needing to be applied.
|
||||
returned: success
|
||||
type: int
|
||||
sample: 3
|
||||
installed_update_count:
|
||||
description: The number of updates successfully installed or downloaded.
|
||||
returned: success
|
||||
type: int
|
||||
sample: 2
|
||||
failed_update_count:
|
||||
description: The number of updates that failed to install.
|
||||
returned: always
|
||||
type: int
|
||||
sample: 0
|
||||
'''
|
||||
@ -1,185 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2015, Corwin Brown <corwin@corwinbrown.com>
|
||||
# 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.CamelConversion
|
||||
#Requires -Module Ansible.ModuleUtils.FileUtil
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
#Requires -Module Ansible.ModuleUtils.WebRequest
|
||||
|
||||
$spec = @{
|
||||
options = @{
|
||||
url = @{ type = "str"; required = $true }
|
||||
content_type = @{ type = "str" }
|
||||
body = @{ type = "raw" }
|
||||
dest = @{ type = "path" }
|
||||
creates = @{ type = "path" }
|
||||
method = @{ default = "GET" }
|
||||
removes = @{ type = "path" }
|
||||
return_content = @{ type = "bool"; default = $false }
|
||||
status_code = @{ type = "list"; elements = "int"; default = @(200) }
|
||||
|
||||
# Defined for the alias backwards compatibility, remove once aliases are removed
|
||||
url_username = @{
|
||||
aliases = @("user", "username")
|
||||
deprecated_aliases = @(
|
||||
@{ name = "user"; version = "2.14" },
|
||||
@{ name = "username"; version = "2.14" }
|
||||
)
|
||||
}
|
||||
url_password = @{
|
||||
aliases = @("password")
|
||||
deprecated_aliases = @(
|
||||
@{ name = "password"; version = "2.14" }
|
||||
)
|
||||
}
|
||||
}
|
||||
supports_check_mode = $true
|
||||
}
|
||||
$spec = Merge-WebRequestSpec -ModuleSpec $spec
|
||||
|
||||
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||||
|
||||
$url = $module.Params.url
|
||||
$method = $module.Params.method.ToUpper()
|
||||
$content_type = $module.Params.content_type
|
||||
$body = $module.Params.body
|
||||
$dest = $module.Params.dest
|
||||
$creates = $module.Params.creates
|
||||
$removes = $module.Params.removes
|
||||
$return_content = $module.Params.return_content
|
||||
$status_code = $module.Params.status_code
|
||||
|
||||
$JSON_CANDIDATES = @('text', 'json', 'javascript')
|
||||
|
||||
$module.Result.elapsed = 0
|
||||
$module.Result.url = $url
|
||||
|
||||
if (-not ($method -cmatch '^[A-Z]+$')) {
|
||||
$module.FailJson("Parameter 'method' needs to be a single word in uppercase, like GET or POST.")
|
||||
}
|
||||
|
||||
if ($creates -and (Test-AnsiblePath -Path $creates)) {
|
||||
$module.Result.skipped = $true
|
||||
$module.Result.msg = "The 'creates' file or directory ($creates) already exists."
|
||||
$module.ExitJson()
|
||||
}
|
||||
|
||||
if ($removes -and -not (Test-AnsiblePath -Path $removes)) {
|
||||
$module.Result.skipped = $true
|
||||
$module.Result.msg = "The 'removes' file or directory ($removes) does not exist."
|
||||
$module.ExitJson()
|
||||
}
|
||||
|
||||
$client = Get-AnsibleWebRequest -Uri $url -Module $module
|
||||
|
||||
if ($null -ne $content_type) {
|
||||
$client.ContentType = $content_type
|
||||
}
|
||||
|
||||
$response_script = {
|
||||
param($Response, $Stream)
|
||||
|
||||
ForEach ($prop in $Response.PSObject.Properties) {
|
||||
$result_key = Convert-StringToSnakeCase -string $prop.Name
|
||||
$prop_value = $prop.Value
|
||||
# convert and DateTime values to ISO 8601 standard
|
||||
if ($prop_value -is [System.DateTime]) {
|
||||
$prop_value = $prop_value.ToString("o", [System.Globalization.CultureInfo]::InvariantCulture)
|
||||
}
|
||||
$module.Result.$result_key = $prop_value
|
||||
}
|
||||
|
||||
# manually get the headers as not all of them are in the response properties
|
||||
foreach ($header_key in $Response.Headers.GetEnumerator()) {
|
||||
$header_value = $Response.Headers[$header_key]
|
||||
$header_key = $header_key.Replace("-", "") # replace - with _ for snake conversion
|
||||
$header_key = Convert-StringToSnakeCase -string $header_key
|
||||
$module.Result.$header_key = $header_value
|
||||
}
|
||||
|
||||
# we only care about the return body if we need to return the content or create a file
|
||||
if ($return_content -or $dest) {
|
||||
# copy to a MemoryStream so we can read it multiple times
|
||||
$memory_st = New-Object -TypeName System.IO.MemoryStream
|
||||
try {
|
||||
$Stream.CopyTo($memory_st)
|
||||
|
||||
if ($return_content) {
|
||||
$memory_st.Seek(0, [System.IO.SeekOrigin]::Begin) > $null
|
||||
$content_bytes = $memory_st.ToArray()
|
||||
$module.Result.content = [System.Text.Encoding]::UTF8.GetString($content_bytes)
|
||||
if ($module.Result.ContainsKey("content_type") -and $module.Result.content_type -Match ($JSON_CANDIDATES -join '|')) {
|
||||
try {
|
||||
$module.Result.json = ([Ansible.Basic.AnsibleModule]::FromJson($module.Result.content))
|
||||
} catch [System.ArgumentException] {
|
||||
# Simply continue, since 'text' might be anything
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($dest) {
|
||||
$memory_st.Seek(0, [System.IO.SeekOrigin]::Begin) > $null
|
||||
$changed = $true
|
||||
|
||||
if (Test-AnsiblePath -Path $dest) {
|
||||
$actual_checksum = Get-FileChecksum -path $dest -algorithm "sha1"
|
||||
|
||||
$sp = New-Object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider
|
||||
$content_checksum = [System.BitConverter]::ToString($sp.ComputeHash($memory_st)).Replace("-", "").ToLower()
|
||||
|
||||
if ($actual_checksum -eq $content_checksum) {
|
||||
$changed = $false
|
||||
}
|
||||
}
|
||||
|
||||
$module.Result.changed = $changed
|
||||
if ($changed -and (-not $module.CheckMode)) {
|
||||
$memory_st.Seek(0, [System.IO.SeekOrigin]::Begin) > $null
|
||||
$file_stream = [System.IO.File]::Create($dest)
|
||||
try {
|
||||
$memory_st.CopyTo($file_stream)
|
||||
} finally {
|
||||
$file_stream.Flush()
|
||||
$file_stream.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
$memory_st.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if ($status_code -notcontains $Response.StatusCode) {
|
||||
$module.FailJson("Status code of request '$([int]$Response.StatusCode)' is not in list of valid status codes $status_code : $($Response.StatusCode)'.")
|
||||
}
|
||||
}
|
||||
|
||||
$body_st = $null
|
||||
if ($null -ne $body) {
|
||||
if ($body -is [System.Collections.IDictionary] -or $body -is [System.Collections.IList]) {
|
||||
$body_string = ConvertTo-Json -InputObject $body -Compress
|
||||
} elseif ($body -isnot [String]) {
|
||||
$body_string = $body.ToString()
|
||||
} else {
|
||||
$body_string = $body
|
||||
}
|
||||
$buffer = [System.Text.Encoding]::UTF8.GetBytes($body_string)
|
||||
|
||||
$body_st = New-Object -TypeName System.IO.MemoryStream -ArgumentList @(,$buffer)
|
||||
}
|
||||
|
||||
try {
|
||||
Invoke-WithWebRequest -Module $module -Request $client -Script $response_script -Body $body_st -IgnoreBadResponse
|
||||
} catch {
|
||||
$module.FailJson("Unhandled exception occurred when sending web request. Exception: $($_.Exception.Message)", $_)
|
||||
} finally {
|
||||
if ($null -ne $body_st) {
|
||||
$body_st.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
$module.ExitJson()
|
||||
@ -1,180 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2015, Corwin Brown <corwin@corwinbrown.com>
|
||||
# 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)
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_uri
|
||||
version_added: '2.1'
|
||||
short_description: Interacts with webservices
|
||||
description:
|
||||
- Interacts with FTP, HTTP and HTTPS web services.
|
||||
- Supports Digest, Basic and WSSE HTTP authentication mechanisms.
|
||||
- For non-Windows targets, use the M(uri) module instead.
|
||||
options:
|
||||
url:
|
||||
description:
|
||||
- Supports FTP, HTTP or HTTPS URLs in the form of (ftp|http|https)://host.domain:port/path.
|
||||
type: str
|
||||
required: yes
|
||||
method:
|
||||
description:
|
||||
- The HTTP Method of the request or response.
|
||||
type: str
|
||||
default: GET
|
||||
content_type:
|
||||
description:
|
||||
- Sets the "Content-Type" header.
|
||||
type: str
|
||||
body:
|
||||
description:
|
||||
- The body of the HTTP request/response to the web service.
|
||||
type: raw
|
||||
dest:
|
||||
description:
|
||||
- Output the response body to a file.
|
||||
type: path
|
||||
version_added: '2.3'
|
||||
creates:
|
||||
description:
|
||||
- A filename, when it already exists, this step will be skipped.
|
||||
type: path
|
||||
version_added: '2.4'
|
||||
removes:
|
||||
description:
|
||||
- A filename, when it does not exist, this step will be skipped.
|
||||
type: path
|
||||
version_added: '2.4'
|
||||
return_content:
|
||||
description:
|
||||
- Whether or not to return the body of the response as a "content" key in
|
||||
the dictionary result. If the reported Content-type is
|
||||
"application/json", then the JSON is additionally loaded into a key
|
||||
called C(json) in the dictionary results.
|
||||
type: bool
|
||||
default: no
|
||||
version_added: '2.4'
|
||||
status_code:
|
||||
description:
|
||||
- A valid, numeric, HTTP status code that signifies success of the request.
|
||||
- Can also be comma separated list of status codes.
|
||||
type: list
|
||||
elements: int
|
||||
default: [ 200 ]
|
||||
version_added: '2.4'
|
||||
url_username:
|
||||
description:
|
||||
- The username to use for authentication.
|
||||
- Was originally called I(user) but was changed to I(url_username) in
|
||||
Ansible 2.9.
|
||||
- The aliases I(user) and I(username) are deprecated and will be removed in
|
||||
Ansible 2.14.
|
||||
aliases:
|
||||
- user
|
||||
- username
|
||||
version_added: "2.4"
|
||||
url_password:
|
||||
description:
|
||||
- The password for I(url_username).
|
||||
- Was originally called I(password) but was changed to I(url_password) in
|
||||
Ansible 2.9.
|
||||
- The alias I(password) is deprecated and will be removed in Ansible 2.14.
|
||||
aliases:
|
||||
- password
|
||||
version_added: "2.4"
|
||||
follow_redirects:
|
||||
version_added: "2.4"
|
||||
maximum_redirection:
|
||||
version_added: "2.4"
|
||||
client_cert:
|
||||
version_added: "2.4"
|
||||
client_cert_password:
|
||||
version_added: "2.5"
|
||||
use_proxy:
|
||||
version_added: "2.9"
|
||||
proxy_url:
|
||||
version_added: "2.9"
|
||||
proxy_username:
|
||||
version_added: "2.9"
|
||||
proxy_password:
|
||||
version_added: "2.9"
|
||||
extends_documentation_fragment:
|
||||
- url_windows
|
||||
seealso:
|
||||
- module: uri
|
||||
- module: win_get_url
|
||||
author:
|
||||
- Corwin Brown (@blakfeld)
|
||||
- Dag Wieers (@dagwieers)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Perform a GET and Store Output
|
||||
win_uri:
|
||||
url: http://example.com/endpoint
|
||||
register: http_output
|
||||
|
||||
# Set a HOST header to hit an internal webserver:
|
||||
- name: Hit a Specific Host on the Server
|
||||
win_uri:
|
||||
url: http://example.com/
|
||||
method: GET
|
||||
headers:
|
||||
host: www.somesite.com
|
||||
|
||||
- name: Perform a HEAD on an Endpoint
|
||||
win_uri:
|
||||
url: http://www.example.com/
|
||||
method: HEAD
|
||||
|
||||
- name: POST a Body to an Endpoint
|
||||
win_uri:
|
||||
url: http://www.somesite.com/
|
||||
method: POST
|
||||
body: "{ 'some': 'json' }"
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
elapsed:
|
||||
description: The number of seconds that elapsed while performing the download.
|
||||
returned: always
|
||||
type: float
|
||||
sample: 23.2
|
||||
url:
|
||||
description: The Target URL.
|
||||
returned: always
|
||||
type: str
|
||||
sample: https://www.ansible.com
|
||||
status_code:
|
||||
description: The HTTP Status Code of the response.
|
||||
returned: success
|
||||
type: int
|
||||
sample: 200
|
||||
status_description:
|
||||
description: A summary of the status.
|
||||
returned: success
|
||||
type: str
|
||||
sample: OK
|
||||
content:
|
||||
description: The raw content of the HTTP response.
|
||||
returned: success and return_content is True
|
||||
type: str
|
||||
sample: '{"foo": "bar"}'
|
||||
content_length:
|
||||
description: The byte size of the response.
|
||||
returned: success
|
||||
type: int
|
||||
sample: 54447
|
||||
json:
|
||||
description: The json structure returned under content as a dictionary.
|
||||
returned: success and Content-Type is "application/json" or "application/javascript" and return_content is True
|
||||
type: dict
|
||||
sample: {"this-is-dependent": "on the actual return content"}
|
||||
'''
|
||||
@ -1,273 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2014, Paul Durivage <paul.durivage@rackspace.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#AnsibleRequires -CSharpUtil Ansible.AccessToken
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
|
||||
########
|
||||
$ADS_UF_PASSWD_CANT_CHANGE = 64
|
||||
$ADS_UF_DONT_EXPIRE_PASSWD = 65536
|
||||
|
||||
$adsi = [ADSI]"WinNT://$env:COMPUTERNAME"
|
||||
|
||||
function Get-User($user) {
|
||||
$adsi.Children | Where-Object {$_.SchemaClassName -eq 'user' -and $_.Name -eq $user }
|
||||
return
|
||||
}
|
||||
|
||||
function Get-UserFlag($user, $flag) {
|
||||
If ($user.UserFlags[0] -band $flag) {
|
||||
$true
|
||||
}
|
||||
Else {
|
||||
$false
|
||||
}
|
||||
}
|
||||
|
||||
function Set-UserFlag($user, $flag) {
|
||||
$user.UserFlags = ($user.UserFlags[0] -BOR $flag)
|
||||
}
|
||||
|
||||
function Clear-UserFlag($user, $flag) {
|
||||
$user.UserFlags = ($user.UserFlags[0] -BXOR $flag)
|
||||
}
|
||||
|
||||
function Get-Group($grp) {
|
||||
$adsi.Children | Where-Object { $_.SchemaClassName -eq 'Group' -and $_.Name -eq $grp }
|
||||
return
|
||||
}
|
||||
|
||||
Function Test-LocalCredential {
|
||||
param([String]$Username, [String]$Password)
|
||||
|
||||
try {
|
||||
$handle = [Ansible.AccessToken.TokenUtil]::LogonUser($Username, $null, $Password, "Network", "Default")
|
||||
$handle.Dispose()
|
||||
$valid_credentials = $true
|
||||
} catch [Ansible.AccessToken.Win32Exception] {
|
||||
# following errors indicate the creds are correct but the user was
|
||||
# unable to log on for other reasons, which we don't care about
|
||||
$success_codes = @(
|
||||
0x0000052F, # ERROR_ACCOUNT_RESTRICTION
|
||||
0x00000530, # ERROR_INVALID_LOGON_HOURS
|
||||
0x00000531, # ERROR_INVALID_WORKSTATION
|
||||
0x00000569 # ERROR_LOGON_TYPE_GRANTED
|
||||
)
|
||||
|
||||
if ($_.Exception.NativeErrorCode -eq 0x0000052E) {
|
||||
# ERROR_LOGON_FAILURE - the user or pass was incorrect
|
||||
$valid_credentials = $false
|
||||
} elseif ($_.Exception.NativeErrorCode -in $success_codes) {
|
||||
$valid_credentials = $true
|
||||
} else {
|
||||
# an unknown failure, reraise exception
|
||||
throw $_
|
||||
}
|
||||
}
|
||||
return $valid_credentials
|
||||
}
|
||||
|
||||
########
|
||||
|
||||
$params = Parse-Args $args;
|
||||
|
||||
$result = @{
|
||||
changed = $false
|
||||
};
|
||||
|
||||
$username = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
|
||||
$fullname = Get-AnsibleParam -obj $params -name "fullname" -type "str"
|
||||
$description = Get-AnsibleParam -obj $params -name "description" -type "str"
|
||||
$password = Get-AnsibleParam -obj $params -name "password" -type "str"
|
||||
$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present","absent","query"
|
||||
$update_password = Get-AnsibleParam -obj $params -name "update_password" -type "str" -default "always" -validateset "always","on_create"
|
||||
$password_expired = Get-AnsibleParam -obj $params -name "password_expired" -type "bool"
|
||||
$password_never_expires = Get-AnsibleParam -obj $params -name "password_never_expires" -type "bool"
|
||||
$user_cannot_change_password = Get-AnsibleParam -obj $params -name "user_cannot_change_password" -type "bool"
|
||||
$account_disabled = Get-AnsibleParam -obj $params -name "account_disabled" -type "bool"
|
||||
$account_locked = Get-AnsibleParam -obj $params -name "account_locked" -type "bool"
|
||||
$groups = Get-AnsibleParam -obj $params -name "groups"
|
||||
$groups_action = Get-AnsibleParam -obj $params -name "groups_action" -type "str" -default "replace" -validateset "add","remove","replace"
|
||||
|
||||
If ($null -ne $account_locked -and $account_locked) {
|
||||
Fail-Json $result "account_locked must be set to 'no' if provided"
|
||||
}
|
||||
|
||||
If ($null -ne $groups) {
|
||||
If ($groups -is [System.String]) {
|
||||
[string[]]$groups = $groups.Split(",")
|
||||
}
|
||||
ElseIf ($groups -isnot [System.Collections.IList]) {
|
||||
Fail-Json $result "groups must be a string or array"
|
||||
}
|
||||
$groups = $groups | ForEach-Object { ([string]$_).Trim() } | Where-Object { $_ }
|
||||
If ($null -eq $groups) {
|
||||
$groups = @()
|
||||
}
|
||||
}
|
||||
|
||||
$user_obj = Get-User $username
|
||||
|
||||
If ($state -eq 'present') {
|
||||
# Add or update user
|
||||
try {
|
||||
If (-not $user_obj) {
|
||||
$user_obj = $adsi.Create("User", $username)
|
||||
If ($null -ne $password) {
|
||||
$user_obj.SetPassword($password)
|
||||
}
|
||||
$user_obj.SetInfo()
|
||||
$result.changed = $true
|
||||
}
|
||||
ElseIf (($null -ne $password) -and ($update_password -eq 'always')) {
|
||||
# ValidateCredentials will fail if either of these are true- just force update...
|
||||
If($user_obj.AccountDisabled -or $user_obj.PasswordExpired) {
|
||||
$password_match = $false
|
||||
}
|
||||
Else {
|
||||
try {
|
||||
$password_match = Test-LocalCredential -Username $username -Password $password
|
||||
} catch [System.ComponentModel.Win32Exception] {
|
||||
Fail-Json -obj $result -message "Failed to validate the user's credentials: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
If (-not $password_match) {
|
||||
$user_obj.SetPassword($password)
|
||||
$result.changed = $true
|
||||
}
|
||||
}
|
||||
If (($null -ne $fullname) -and ($fullname -ne $user_obj.FullName[0])) {
|
||||
$user_obj.FullName = $fullname
|
||||
$result.changed = $true
|
||||
}
|
||||
If (($null -ne $description) -and ($description -ne $user_obj.Description[0])) {
|
||||
$user_obj.Description = $description
|
||||
$result.changed = $true
|
||||
}
|
||||
If (($null -ne $password_expired) -and ($password_expired -ne ($user_obj.PasswordExpired | ConvertTo-Bool))) {
|
||||
$user_obj.PasswordExpired = If ($password_expired) { 1 } Else { 0 }
|
||||
$result.changed = $true
|
||||
}
|
||||
If (($null -ne $password_never_expires) -and ($password_never_expires -ne (Get-UserFlag $user_obj $ADS_UF_DONT_EXPIRE_PASSWD))) {
|
||||
If ($password_never_expires) {
|
||||
Set-UserFlag $user_obj $ADS_UF_DONT_EXPIRE_PASSWD
|
||||
}
|
||||
Else {
|
||||
Clear-UserFlag $user_obj $ADS_UF_DONT_EXPIRE_PASSWD
|
||||
}
|
||||
$result.changed = $true
|
||||
}
|
||||
If (($null -ne $user_cannot_change_password) -and ($user_cannot_change_password -ne (Get-UserFlag $user_obj $ADS_UF_PASSWD_CANT_CHANGE))) {
|
||||
If ($user_cannot_change_password) {
|
||||
Set-UserFlag $user_obj $ADS_UF_PASSWD_CANT_CHANGE
|
||||
}
|
||||
Else {
|
||||
Clear-UserFlag $user_obj $ADS_UF_PASSWD_CANT_CHANGE
|
||||
}
|
||||
$result.changed = $true
|
||||
}
|
||||
If (($null -ne $account_disabled) -and ($account_disabled -ne $user_obj.AccountDisabled)) {
|
||||
$user_obj.AccountDisabled = $account_disabled
|
||||
$result.changed = $true
|
||||
}
|
||||
If (($null -ne $account_locked) -and ($account_locked -ne $user_obj.IsAccountLocked)) {
|
||||
$user_obj.IsAccountLocked = $account_locked
|
||||
$result.changed = $true
|
||||
}
|
||||
If ($result.changed) {
|
||||
$user_obj.SetInfo()
|
||||
}
|
||||
If ($null -ne $groups) {
|
||||
[string[]]$current_groups = $user_obj.Groups() | ForEach-Object { $_.GetType().InvokeMember("Name", "GetProperty", $null, $_, $null) }
|
||||
If (($groups_action -eq "remove") -or ($groups_action -eq "replace")) {
|
||||
ForEach ($grp in $current_groups) {
|
||||
If ((($groups_action -eq "remove") -and ($groups -contains $grp)) -or (($groups_action -eq "replace") -and ($groups -notcontains $grp))) {
|
||||
$group_obj = Get-Group $grp
|
||||
If ($group_obj) {
|
||||
$group_obj.Remove($user_obj.Path)
|
||||
$result.changed = $true
|
||||
}
|
||||
Else {
|
||||
Fail-Json $result "group '$grp' not found"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
If (($groups_action -eq "add") -or ($groups_action -eq "replace")) {
|
||||
ForEach ($grp in $groups) {
|
||||
If ($current_groups -notcontains $grp) {
|
||||
$group_obj = Get-Group $grp
|
||||
If ($group_obj) {
|
||||
$group_obj.Add($user_obj.Path)
|
||||
$result.changed = $true
|
||||
}
|
||||
Else {
|
||||
Fail-Json $result "group '$grp' not found"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Fail-Json $result $_.Exception.Message
|
||||
}
|
||||
}
|
||||
ElseIf ($state -eq 'absent') {
|
||||
# Remove user
|
||||
try {
|
||||
If ($user_obj) {
|
||||
$username = $user_obj.Name.Value
|
||||
$adsi.delete("User", $user_obj.Name.Value)
|
||||
$result.changed = $true
|
||||
$result.msg = "User '$username' deleted successfully"
|
||||
$user_obj = $null
|
||||
} else {
|
||||
$result.msg = "User '$username' was not found"
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Fail-Json $result $_.Exception.Message
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
If ($user_obj -and $user_obj -is [System.DirectoryServices.DirectoryEntry]) {
|
||||
$user_obj.RefreshCache()
|
||||
$result.name = $user_obj.Name[0]
|
||||
$result.fullname = $user_obj.FullName[0]
|
||||
$result.path = $user_obj.Path
|
||||
$result.description = $user_obj.Description[0]
|
||||
$result.password_expired = ($user_obj.PasswordExpired | ConvertTo-Bool)
|
||||
$result.password_never_expires = (Get-UserFlag $user_obj $ADS_UF_DONT_EXPIRE_PASSWD)
|
||||
$result.user_cannot_change_password = (Get-UserFlag $user_obj $ADS_UF_PASSWD_CANT_CHANGE)
|
||||
$result.account_disabled = $user_obj.AccountDisabled
|
||||
$result.account_locked = $user_obj.IsAccountLocked
|
||||
$result.sid = (New-Object System.Security.Principal.SecurityIdentifier($user_obj.ObjectSid.Value, 0)).Value
|
||||
$user_groups = @()
|
||||
ForEach ($grp in $user_obj.Groups()) {
|
||||
$group_result = @{
|
||||
name = $grp.GetType().InvokeMember("Name", "GetProperty", $null, $grp, $null)
|
||||
path = $grp.GetType().InvokeMember("ADsPath", "GetProperty", $null, $grp, $null)
|
||||
}
|
||||
$user_groups += $group_result;
|
||||
}
|
||||
$result.groups = $user_groups
|
||||
$result.state = "present"
|
||||
}
|
||||
Else {
|
||||
$result.name = $username
|
||||
if ($state -eq 'query') {
|
||||
$result.msg = "User '$username' was not found"
|
||||
}
|
||||
$result.state = "absent"
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Fail-Json $result $_.Exception.Message
|
||||
}
|
||||
|
||||
Exit-Json $result
|
||||
@ -1,194 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2014, Matt Martz <matt@sivel.net>, and others
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
# this is a windows documentation stub. actual code lives in the .ps1
|
||||
# file of the same name
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['stableinterface'],
|
||||
'supported_by': 'core'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_user
|
||||
version_added: "1.7"
|
||||
short_description: Manages local Windows user accounts
|
||||
description:
|
||||
- Manages local Windows user accounts.
|
||||
- For non-Windows targets, use the M(user) module instead.
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of the user to create, remove or modify.
|
||||
type: str
|
||||
required: yes
|
||||
fullname:
|
||||
description:
|
||||
- Full name of the user.
|
||||
type: str
|
||||
version_added: "1.9"
|
||||
description:
|
||||
description:
|
||||
- Description of the user.
|
||||
type: str
|
||||
version_added: "1.9"
|
||||
password:
|
||||
description:
|
||||
- Optionally set the user's password to this (plain text) value.
|
||||
type: str
|
||||
update_password:
|
||||
description:
|
||||
- C(always) will update passwords if they differ. C(on_create) will
|
||||
only set the password for newly created users.
|
||||
type: str
|
||||
choices: [ always, on_create ]
|
||||
default: always
|
||||
version_added: "1.9"
|
||||
password_expired:
|
||||
description:
|
||||
- C(yes) will require the user to change their password at next login.
|
||||
- C(no) will clear the expired password flag.
|
||||
type: bool
|
||||
version_added: "1.9"
|
||||
password_never_expires:
|
||||
description:
|
||||
- C(yes) will set the password to never expire.
|
||||
- C(no) will allow the password to expire.
|
||||
type: bool
|
||||
version_added: "1.9"
|
||||
user_cannot_change_password:
|
||||
description:
|
||||
- C(yes) will prevent the user from changing their password.
|
||||
- C(no) will allow the user to change their password.
|
||||
type: bool
|
||||
version_added: "1.9"
|
||||
account_disabled:
|
||||
description:
|
||||
- C(yes) will disable the user account.
|
||||
- C(no) will clear the disabled flag.
|
||||
type: bool
|
||||
version_added: "1.9"
|
||||
account_locked:
|
||||
description:
|
||||
- C(no) will unlock the user account if locked.
|
||||
choices: [ 'no' ]
|
||||
version_added: "1.9"
|
||||
groups:
|
||||
description:
|
||||
- Adds or removes the user from this comma-separated list of groups,
|
||||
depending on the value of I(groups_action).
|
||||
- When I(groups_action) is C(replace) and I(groups) is set to the empty
|
||||
string ('groups='), the user is removed from all groups.
|
||||
version_added: "1.9"
|
||||
groups_action:
|
||||
description:
|
||||
- If C(add), the user is added to each group in I(groups) where not
|
||||
already a member.
|
||||
- If C(replace), the user is added as a member of each group in
|
||||
I(groups) and removed from any other groups.
|
||||
- If C(remove), the user is removed from each group in I(groups).
|
||||
type: str
|
||||
choices: [ add, replace, remove ]
|
||||
default: replace
|
||||
version_added: "1.9"
|
||||
state:
|
||||
description:
|
||||
- When C(absent), removes the user account if it exists.
|
||||
- When C(present), creates or updates the user account.
|
||||
- When C(query) (new in 1.9), retrieves the user account details
|
||||
without making any changes.
|
||||
type: str
|
||||
choices: [ absent, present, query ]
|
||||
default: present
|
||||
seealso:
|
||||
- module: user
|
||||
- module: win_domain_membership
|
||||
- module: win_domain_user
|
||||
- module: win_group
|
||||
- module: win_group_membership
|
||||
- module: win_user_profile
|
||||
author:
|
||||
- Paul Durivage (@angstwad)
|
||||
- Chris Church (@cchurch)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Ensure user bob is present
|
||||
win_user:
|
||||
name: bob
|
||||
password: B0bP4ssw0rd
|
||||
state: present
|
||||
groups:
|
||||
- Users
|
||||
|
||||
- name: Ensure user bob is absent
|
||||
win_user:
|
||||
name: bob
|
||||
state: absent
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
account_disabled:
|
||||
description: Whether the user is disabled.
|
||||
returned: user exists
|
||||
type: bool
|
||||
sample: false
|
||||
account_locked:
|
||||
description: Whether the user is locked.
|
||||
returned: user exists
|
||||
type: bool
|
||||
sample: false
|
||||
description:
|
||||
description: The description set for the user.
|
||||
returned: user exists
|
||||
type: str
|
||||
sample: Username for test
|
||||
fullname:
|
||||
description: The full name set for the user.
|
||||
returned: user exists
|
||||
type: str
|
||||
sample: Test Username
|
||||
groups:
|
||||
description: A list of groups and their ADSI path the user is a member of.
|
||||
returned: user exists
|
||||
type: list
|
||||
sample: [
|
||||
{
|
||||
"name": "Administrators",
|
||||
"path": "WinNT://WORKGROUP/USER-PC/Administrators"
|
||||
}
|
||||
]
|
||||
name:
|
||||
description: The name of the user
|
||||
returned: always
|
||||
type: str
|
||||
sample: username
|
||||
password_expired:
|
||||
description: Whether the password is expired.
|
||||
returned: user exists
|
||||
type: bool
|
||||
sample: false
|
||||
password_never_expires:
|
||||
description: Whether the password is set to never expire.
|
||||
returned: user exists
|
||||
type: bool
|
||||
sample: true
|
||||
path:
|
||||
description: The ADSI path for the user.
|
||||
returned: user exists
|
||||
type: str
|
||||
sample: "WinNT://WORKGROUP/USER-PC/username"
|
||||
sid:
|
||||
description: The SID for the user.
|
||||
returned: user exists
|
||||
type: str
|
||||
sample: S-1-5-21-3322259488-2828151810-3939402796-1001
|
||||
user_cannot_change_password:
|
||||
description: Whether the user can change their own password.
|
||||
returned: user exists
|
||||
type: bool
|
||||
sample: false
|
||||
'''
|
||||
@ -1,349 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
#Requires -Module Ansible.ModuleUtils.SID
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$params = Parse-Args $args -supports_check_mode $true
|
||||
$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
|
||||
$diff_mode = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false
|
||||
$_remote_tmp = Get-AnsibleParam $params "_ansible_remote_tmp" -type "path" -default $env:TMP
|
||||
|
||||
$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
|
||||
$users = Get-AnsibleParam -obj $params -name "users" -type "list" -failifempty $true
|
||||
$action = Get-AnsibleParam -obj $params -name "action" -type "str" -default "set" -validateset "add","remove","set"
|
||||
|
||||
$result = @{
|
||||
changed = $false
|
||||
added = @()
|
||||
removed = @()
|
||||
}
|
||||
|
||||
if ($diff_mode) {
|
||||
$result.diff = @{}
|
||||
}
|
||||
|
||||
$sec_helper_util = @"
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Principal;
|
||||
|
||||
namespace Ansible
|
||||
{
|
||||
public class LsaRightHelper : IDisposable
|
||||
{
|
||||
// Code modified from https://gallery.technet.microsoft.com/scriptcenter/Grant-Revoke-Query-user-26e259b0
|
||||
|
||||
enum Access : int
|
||||
{
|
||||
POLICY_READ = 0x20006,
|
||||
POLICY_ALL_ACCESS = 0x00F0FFF,
|
||||
POLICY_EXECUTE = 0X20801,
|
||||
POLICY_WRITE = 0X207F8
|
||||
}
|
||||
|
||||
IntPtr lsaHandle;
|
||||
|
||||
const string LSA_DLL = "advapi32.dll";
|
||||
const CharSet DEFAULT_CHAR_SET = CharSet.Unicode;
|
||||
|
||||
const uint STATUS_NO_MORE_ENTRIES = 0x8000001a;
|
||||
const uint STATUS_NO_SUCH_PRIVILEGE = 0xc0000060;
|
||||
|
||||
internal sealed class Sid : IDisposable
|
||||
{
|
||||
public IntPtr pSid = IntPtr.Zero;
|
||||
public SecurityIdentifier sid = null;
|
||||
|
||||
public Sid(string sidString)
|
||||
{
|
||||
try
|
||||
{
|
||||
sid = new SecurityIdentifier(sidString);
|
||||
} catch
|
||||
{
|
||||
throw new ArgumentException(String.Format("SID string {0} could not be converted to SecurityIdentifier", sidString));
|
||||
}
|
||||
|
||||
Byte[] buffer = new Byte[sid.BinaryLength];
|
||||
sid.GetBinaryForm(buffer, 0);
|
||||
|
||||
pSid = Marshal.AllocHGlobal(sid.BinaryLength);
|
||||
Marshal.Copy(buffer, 0, pSid, sid.BinaryLength);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (pSid != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeHGlobal(pSid);
|
||||
pSid = IntPtr.Zero;
|
||||
}
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
~Sid() { Dispose(); }
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct LSA_OBJECT_ATTRIBUTES
|
||||
{
|
||||
public int Length;
|
||||
public IntPtr RootDirectory;
|
||||
public IntPtr ObjectName;
|
||||
public int Attributes;
|
||||
public IntPtr SecurityDescriptor;
|
||||
public IntPtr SecurityQualityOfService;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = DEFAULT_CHAR_SET)]
|
||||
private struct LSA_UNICODE_STRING
|
||||
{
|
||||
public ushort Length;
|
||||
public ushort MaximumLength;
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public string Buffer;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct LSA_ENUMERATION_INFORMATION
|
||||
{
|
||||
public IntPtr Sid;
|
||||
}
|
||||
|
||||
[DllImport(LSA_DLL, CharSet = DEFAULT_CHAR_SET, SetLastError = true)]
|
||||
private static extern uint LsaOpenPolicy(
|
||||
LSA_UNICODE_STRING[] SystemName,
|
||||
ref LSA_OBJECT_ATTRIBUTES ObjectAttributes,
|
||||
int AccessMask,
|
||||
out IntPtr PolicyHandle
|
||||
);
|
||||
|
||||
[DllImport(LSA_DLL, CharSet = DEFAULT_CHAR_SET, SetLastError = true)]
|
||||
private static extern uint LsaAddAccountRights(
|
||||
IntPtr PolicyHandle,
|
||||
IntPtr pSID,
|
||||
LSA_UNICODE_STRING[] UserRights,
|
||||
int CountOfRights
|
||||
);
|
||||
|
||||
[DllImport(LSA_DLL, CharSet = DEFAULT_CHAR_SET, SetLastError = true)]
|
||||
private static extern uint LsaRemoveAccountRights(
|
||||
IntPtr PolicyHandle,
|
||||
IntPtr pSID,
|
||||
bool AllRights,
|
||||
LSA_UNICODE_STRING[] UserRights,
|
||||
int CountOfRights
|
||||
);
|
||||
|
||||
[DllImport(LSA_DLL, CharSet = DEFAULT_CHAR_SET, SetLastError = true)]
|
||||
private static extern uint LsaEnumerateAccountsWithUserRight(
|
||||
IntPtr PolicyHandle,
|
||||
LSA_UNICODE_STRING[] UserRights,
|
||||
out IntPtr EnumerationBuffer,
|
||||
out ulong CountReturned
|
||||
);
|
||||
|
||||
[DllImport(LSA_DLL)]
|
||||
private static extern int LsaNtStatusToWinError(int NTSTATUS);
|
||||
|
||||
[DllImport(LSA_DLL)]
|
||||
private static extern int LsaClose(IntPtr PolicyHandle);
|
||||
|
||||
[DllImport(LSA_DLL)]
|
||||
private static extern int LsaFreeMemory(IntPtr Buffer);
|
||||
|
||||
public LsaRightHelper()
|
||||
{
|
||||
LSA_OBJECT_ATTRIBUTES lsaAttr;
|
||||
lsaAttr.RootDirectory = IntPtr.Zero;
|
||||
lsaAttr.ObjectName = IntPtr.Zero;
|
||||
lsaAttr.Attributes = 0;
|
||||
lsaAttr.SecurityDescriptor = IntPtr.Zero;
|
||||
lsaAttr.SecurityQualityOfService = IntPtr.Zero;
|
||||
lsaAttr.Length = Marshal.SizeOf(typeof(LSA_OBJECT_ATTRIBUTES));
|
||||
|
||||
lsaHandle = IntPtr.Zero;
|
||||
|
||||
LSA_UNICODE_STRING[] system = new LSA_UNICODE_STRING[1];
|
||||
system[0] = InitLsaString("");
|
||||
|
||||
uint ret = LsaOpenPolicy(system, ref lsaAttr, (int)Access.POLICY_ALL_ACCESS, out lsaHandle);
|
||||
if (ret != 0)
|
||||
throw new Win32Exception(LsaNtStatusToWinError((int)ret));
|
||||
}
|
||||
|
||||
public void AddPrivilege(string sidString, string privilege)
|
||||
{
|
||||
uint ret = 0;
|
||||
using (Sid sid = new Sid(sidString))
|
||||
{
|
||||
LSA_UNICODE_STRING[] privileges = new LSA_UNICODE_STRING[1];
|
||||
privileges[0] = InitLsaString(privilege);
|
||||
ret = LsaAddAccountRights(lsaHandle, sid.pSid, privileges, 1);
|
||||
}
|
||||
if (ret != 0)
|
||||
throw new Win32Exception(LsaNtStatusToWinError((int)ret));
|
||||
}
|
||||
|
||||
public void RemovePrivilege(string sidString, string privilege)
|
||||
{
|
||||
uint ret = 0;
|
||||
using (Sid sid = new Sid(sidString))
|
||||
{
|
||||
LSA_UNICODE_STRING[] privileges = new LSA_UNICODE_STRING[1];
|
||||
privileges[0] = InitLsaString(privilege);
|
||||
ret = LsaRemoveAccountRights(lsaHandle, sid.pSid, false, privileges, 1);
|
||||
}
|
||||
if (ret != 0)
|
||||
throw new Win32Exception(LsaNtStatusToWinError((int)ret));
|
||||
}
|
||||
|
||||
public string[] EnumerateAccountsWithUserRight(string privilege)
|
||||
{
|
||||
uint ret = 0;
|
||||
ulong count = 0;
|
||||
LSA_UNICODE_STRING[] rights = new LSA_UNICODE_STRING[1];
|
||||
rights[0] = InitLsaString(privilege);
|
||||
IntPtr buffer = IntPtr.Zero;
|
||||
|
||||
ret = LsaEnumerateAccountsWithUserRight(lsaHandle, rights, out buffer, out count);
|
||||
switch (ret)
|
||||
{
|
||||
case 0:
|
||||
string[] accounts = new string[count];
|
||||
for (int i = 0; i < (int)count; i++)
|
||||
{
|
||||
LSA_ENUMERATION_INFORMATION LsaInfo = (LSA_ENUMERATION_INFORMATION)Marshal.PtrToStructure(
|
||||
IntPtr.Add(buffer, i * Marshal.SizeOf(typeof(LSA_ENUMERATION_INFORMATION))),
|
||||
typeof(LSA_ENUMERATION_INFORMATION));
|
||||
|
||||
accounts[i] = new SecurityIdentifier(LsaInfo.Sid).ToString();
|
||||
}
|
||||
LsaFreeMemory(buffer);
|
||||
return accounts;
|
||||
|
||||
case STATUS_NO_MORE_ENTRIES:
|
||||
return new string[0];
|
||||
|
||||
case STATUS_NO_SUCH_PRIVILEGE:
|
||||
throw new ArgumentException(String.Format("Invalid privilege {0} not found in LSA database", privilege));
|
||||
|
||||
default:
|
||||
throw new Win32Exception(LsaNtStatusToWinError((int)ret));
|
||||
}
|
||||
}
|
||||
|
||||
static LSA_UNICODE_STRING InitLsaString(string s)
|
||||
{
|
||||
// Unicode strings max. 32KB
|
||||
if (s.Length > 0x7ffe)
|
||||
throw new ArgumentException("String too long");
|
||||
|
||||
LSA_UNICODE_STRING lus = new LSA_UNICODE_STRING();
|
||||
lus.Buffer = s;
|
||||
lus.Length = (ushort)(s.Length * sizeof(char));
|
||||
lus.MaximumLength = (ushort)(lus.Length + sizeof(char));
|
||||
|
||||
return lus;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (lsaHandle != IntPtr.Zero)
|
||||
{
|
||||
LsaClose(lsaHandle);
|
||||
lsaHandle = IntPtr.Zero;
|
||||
}
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
~LsaRightHelper() { Dispose(); }
|
||||
}
|
||||
}
|
||||
"@
|
||||
|
||||
$original_tmp = $env:TMP
|
||||
$env:TMP = $_remote_tmp
|
||||
Add-Type -TypeDefinition $sec_helper_util
|
||||
$env:TMP = $original_tmp
|
||||
|
||||
Function Compare-UserList($existing_users, $new_users) {
|
||||
$added_users = [String[]]@()
|
||||
$removed_users = [String[]]@()
|
||||
if ($action -eq "add") {
|
||||
$added_users = [Linq.Enumerable]::Except($new_users, $existing_users)
|
||||
} elseif ($action -eq "remove") {
|
||||
$removed_users = [Linq.Enumerable]::Intersect($new_users, $existing_users)
|
||||
} else {
|
||||
$added_users = [Linq.Enumerable]::Except($new_users, $existing_users)
|
||||
$removed_users = [Linq.Enumerable]::Except($existing_users, $new_users)
|
||||
}
|
||||
|
||||
$change_result = @{
|
||||
added = $added_users
|
||||
removed = $removed_users
|
||||
}
|
||||
|
||||
return $change_result
|
||||
}
|
||||
|
||||
# C# class we can use to enumerate/add/remove rights
|
||||
$lsa_helper = New-Object -TypeName Ansible.LsaRightHelper
|
||||
|
||||
$new_users = [System.Collections.ArrayList]@()
|
||||
foreach ($user in $users) {
|
||||
$user_sid = Convert-ToSID -account_name $user
|
||||
$new_users.Add($user_sid) > $null
|
||||
}
|
||||
$new_users = [String[]]$new_users.ToArray()
|
||||
try {
|
||||
$existing_users = $lsa_helper.EnumerateAccountsWithUserRight($name)
|
||||
} catch [ArgumentException] {
|
||||
Fail-Json -obj $result -message "the specified right $name is not a valid right"
|
||||
} catch {
|
||||
Fail-Json -obj $result -message "failed to enumerate existing accounts with right: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
$change_result = Compare-UserList -existing_users $existing_users -new_user $new_users
|
||||
if (($change_result.added.Length -gt 0) -or ($change_result.removed.Length -gt 0)) {
|
||||
$result.changed = $true
|
||||
$diff_text = "[$name]`n"
|
||||
|
||||
# used in diff mode calculation
|
||||
$new_user_list = [System.Collections.ArrayList]$existing_users
|
||||
foreach ($user in $change_result.removed) {
|
||||
if (-not $check_mode) {
|
||||
$lsa_helper.RemovePrivilege($user, $name)
|
||||
}
|
||||
$user_name = Convert-FromSID -sid $user
|
||||
$result.removed += $user_name
|
||||
$diff_text += "-$user_name`n"
|
||||
$new_user_list.Remove($user) > $null
|
||||
}
|
||||
foreach ($user in $change_result.added) {
|
||||
if (-not $check_mode) {
|
||||
$lsa_helper.AddPrivilege($user, $name)
|
||||
}
|
||||
$user_name = Convert-FromSID -sid $user
|
||||
$result.added += $user_name
|
||||
$diff_text += "+$user_name`n"
|
||||
$new_user_list.Add($user) > $null
|
||||
}
|
||||
|
||||
if ($diff_mode) {
|
||||
if ($new_user_list.Count -eq 0) {
|
||||
$diff_text = "-$diff_text"
|
||||
} else {
|
||||
if ($existing_users.Count -eq 0) {
|
||||
$diff_text = "+$diff_text"
|
||||
}
|
||||
}
|
||||
$result.diff.prepared = $diff_text
|
||||
}
|
||||
}
|
||||
|
||||
Exit-Json $result
|
||||
@ -1,108 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
# this is a windows documentation stub. actual code lives in the .ps1
|
||||
# file of the same name
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_user_right
|
||||
version_added: '2.4'
|
||||
short_description: Manage Windows User Rights
|
||||
description:
|
||||
- Add, remove or set User Rights for a group or users or groups.
|
||||
- You can set user rights for both local and domain accounts.
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- The name of the User Right as shown by the C(Constant Name) value from
|
||||
U(https://technet.microsoft.com/en-us/library/dd349804.aspx).
|
||||
- The module will return an error if the right is invalid.
|
||||
type: str
|
||||
required: yes
|
||||
users:
|
||||
description:
|
||||
- A list of users or groups to add/remove on the User Right.
|
||||
- These can be in the form DOMAIN\user-group, user-group@DOMAIN.COM for
|
||||
domain users/groups.
|
||||
- For local users/groups it can be in the form user-group, .\user-group,
|
||||
SERVERNAME\user-group where SERVERNAME is the name of the remote server.
|
||||
- You can also add special local accounts like SYSTEM and others.
|
||||
- Can be set to an empty list with I(action=set) to remove all accounts
|
||||
from the right.
|
||||
type: list
|
||||
required: yes
|
||||
action:
|
||||
description:
|
||||
- C(add) will add the users/groups to the existing right.
|
||||
- C(remove) will remove the users/groups from the existing right.
|
||||
- C(set) will replace the users/groups of the existing right.
|
||||
type: str
|
||||
default: set
|
||||
choices: [ add, remove, set ]
|
||||
notes:
|
||||
- If the server is domain joined this module can change a right but if a GPO
|
||||
governs this right then the changes won't last.
|
||||
seealso:
|
||||
- module: win_group
|
||||
- module: win_group_membership
|
||||
- module: win_user
|
||||
author:
|
||||
- Jordan Borean (@jborean93)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
---
|
||||
- name: Replace the entries of Deny log on locally
|
||||
win_user_right:
|
||||
name: SeDenyInteractiveLogonRight
|
||||
users:
|
||||
- Guest
|
||||
- Users
|
||||
action: set
|
||||
|
||||
- name: Add account to Log on as a service
|
||||
win_user_right:
|
||||
name: SeServiceLogonRight
|
||||
users:
|
||||
- .\Administrator
|
||||
- '{{ansible_hostname}}\local-user'
|
||||
action: add
|
||||
|
||||
- name: Remove accounts who can create Symbolic links
|
||||
win_user_right:
|
||||
name: SeCreateSymbolicLinkPrivilege
|
||||
users:
|
||||
- SYSTEM
|
||||
- Administrators
|
||||
- DOMAIN\User
|
||||
- group@DOMAIN.COM
|
||||
action: remove
|
||||
|
||||
- name: Remove all accounts who cannot log on remote interactively
|
||||
win_user_right:
|
||||
name: SeDenyRemoteInteractiveLogonRight
|
||||
users: []
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
added:
|
||||
description: A list of accounts that were added to the right, this is empty
|
||||
if no accounts were added.
|
||||
returned: success
|
||||
type: list
|
||||
sample: ["NT AUTHORITY\\SYSTEM", "DOMAIN\\User"]
|
||||
removed:
|
||||
description: A list of accounts that were removed from the right, this is
|
||||
empty if no accounts were removed.
|
||||
returned: success
|
||||
type: list
|
||||
sample: ["SERVERNAME\\Administrator", "BUILTIN\\Administrators"]
|
||||
'''
|
||||
@ -1,259 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
#Requires -Module Ansible.ModuleUtils.FileUtil
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$params = Parse-Args -arguments $args -supports_check_mode $true
|
||||
|
||||
$connect_timeout = Get-AnsibleParam -obj $params -name "connect_timeout" -type "int" -default 5
|
||||
$delay = Get-AnsibleParam -obj $params -name "delay" -type "int"
|
||||
$exclude_hosts = Get-AnsibleParam -obj $params -name "exclude_hosts" -type "list"
|
||||
$hostname = Get-AnsibleParam -obj $params -name "host" -type "str" -default "127.0.0.1"
|
||||
$path = Get-AnsibleParam -obj $params -name "path" -type "path"
|
||||
$port = Get-AnsibleParam -obj $params -name "port" -type "int"
|
||||
$regex = Get-AnsibleParam -obj $params -name "regex" -type "str" -aliases "search_regex","regexp"
|
||||
$sleep = Get-AnsibleParam -obj $params -name "sleep" -type "int" -default 1
|
||||
$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "started" -validateset "present","started","stopped","absent","drained"
|
||||
$timeout = Get-AnsibleParam -obj $params -name "timeout" -type "int" -default 300
|
||||
|
||||
$result = @{
|
||||
changed = $false
|
||||
elapsed = 0
|
||||
}
|
||||
|
||||
# validate the input with the various options
|
||||
if ($null -ne $port -and $null -ne $path) {
|
||||
Fail-Json $result "port and path parameter can not both be passed to win_wait_for"
|
||||
}
|
||||
if ($null -ne $exclude_hosts -and $state -ne "drained") {
|
||||
Fail-Json $result "exclude_hosts should only be with state=drained"
|
||||
}
|
||||
if ($null -ne $path) {
|
||||
if ($state -in @("stopped","drained")) {
|
||||
Fail-Json $result "state=$state should only be used for checking a port in the win_wait_for module"
|
||||
}
|
||||
|
||||
if ($null -ne $exclude_hosts) {
|
||||
Fail-Json $result "exclude_hosts should only be used when checking a port and state=drained in the win_wait_for module"
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -ne $port) {
|
||||
if ($null -ne $regex) {
|
||||
Fail-Json $result "regex should by used when checking a string in a file in the win_wait_for module"
|
||||
}
|
||||
|
||||
if ($null -ne $exclude_hosts -and $state -ne "drained") {
|
||||
Fail-Json $result "exclude_hosts should be used when state=drained in the win_wait_for module"
|
||||
}
|
||||
}
|
||||
|
||||
Function Test-Port($hostname, $port) {
|
||||
$timeout = $connect_timeout * 1000
|
||||
$socket = New-Object -TypeName System.Net.Sockets.TcpClient
|
||||
$connect = $socket.BeginConnect($hostname, $port, $null, $null)
|
||||
$wait = $connect.AsyncWaitHandle.WaitOne($timeout, $false)
|
||||
|
||||
if ($wait) {
|
||||
try {
|
||||
$socket.EndConnect($connect) | Out-Null
|
||||
$valid = $true
|
||||
} catch {
|
||||
$valid = $false
|
||||
}
|
||||
} else {
|
||||
$valid = $false
|
||||
}
|
||||
|
||||
$socket.Close()
|
||||
$socket.Dispose()
|
||||
|
||||
$valid
|
||||
}
|
||||
|
||||
Function Get-PortConnections($hostname, $port) {
|
||||
$connections = @()
|
||||
|
||||
$conn_info = [Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties()
|
||||
if ($hostname -eq "0.0.0.0") {
|
||||
$active_connections = $conn_info.GetActiveTcpConnections() | Where-Object { $_.LocalEndPoint.Port -eq $port }
|
||||
} else {
|
||||
$active_connections = $conn_info.GetActiveTcpConnections() | Where-Object { $_.LocalEndPoint.Address -eq $hostname -and $_.LocalEndPoint.Port -eq $port }
|
||||
}
|
||||
|
||||
if ($null -ne $active_connections) {
|
||||
foreach ($active_connection in $active_connections) {
|
||||
$connections += $active_connection.RemoteEndPoint.Address
|
||||
}
|
||||
}
|
||||
|
||||
$connections
|
||||
}
|
||||
|
||||
$module_start = Get-Date
|
||||
|
||||
if ($null -ne $delay) {
|
||||
Start-Sleep -Seconds $delay
|
||||
}
|
||||
|
||||
$attempts = 0
|
||||
if ($null -eq $path -and $null -eq $port -and $state -ne "drained") {
|
||||
Start-Sleep -Seconds $timeout
|
||||
} elseif ($null -ne $path) {
|
||||
if ($state -in @("present", "started")) {
|
||||
# check if the file exists or string exists in file
|
||||
$start_time = Get-Date
|
||||
$complete = $false
|
||||
while (((Get-Date) - $start_time).TotalSeconds -lt $timeout) {
|
||||
$attempts += 1
|
||||
if (Test-AnsiblePath -Path $path) {
|
||||
if ($null -eq $regex) {
|
||||
$complete = $true
|
||||
break
|
||||
} else {
|
||||
$file_contents = Get-Content -Path $path -Raw
|
||||
if ($file_contents -match $regex) {
|
||||
$complete = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
Start-Sleep -Seconds $sleep
|
||||
}
|
||||
|
||||
if ($complete -eq $false) {
|
||||
$result.elapsed = ((Get-Date) - $module_start).TotalSeconds
|
||||
$result.wait_attempts = $attempts
|
||||
if ($null -eq $regex) {
|
||||
Fail-Json $result "timeout while waiting for file $path to be present"
|
||||
} else {
|
||||
Fail-Json $result "timeout while waiting for string regex $regex in file $path to match"
|
||||
}
|
||||
}
|
||||
} elseif ($state -in @("absent")) {
|
||||
# check if the file is deleted or string doesn't exist in file
|
||||
$start_time = Get-Date
|
||||
$complete = $false
|
||||
while (((Get-Date) - $start_time).TotalSeconds -lt $timeout) {
|
||||
$attempts += 1
|
||||
if (Test-AnsiblePath -Path $path) {
|
||||
if ($null -ne $regex) {
|
||||
$file_contents = Get-Content -Path $path -Raw
|
||||
if ($file_contents -notmatch $regex) {
|
||||
$complete = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$complete = $true
|
||||
break
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $sleep
|
||||
}
|
||||
|
||||
if ($complete -eq $false) {
|
||||
$result.elapsed = ((Get-Date) - $module_start).TotalSeconds
|
||||
$result.wait_attempts = $attempts
|
||||
if ($null -eq $regex) {
|
||||
Fail-Json $result "timeout while waiting for file $path to be absent"
|
||||
} else {
|
||||
Fail-Json $result "timeout while waiting for string regex $regex in file $path to not match"
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($null -ne $port) {
|
||||
if ($state -in @("started","present")) {
|
||||
# check that the port is online and is listening
|
||||
$start_time = Get-Date
|
||||
$complete = $false
|
||||
while (((Get-Date) - $start_time).TotalSeconds -lt $timeout) {
|
||||
$attempts += 1
|
||||
$port_result = Test-Port -hostname $hostname -port $port
|
||||
if ($port_result -eq $true) {
|
||||
$complete = $true
|
||||
break
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $sleep
|
||||
}
|
||||
|
||||
if ($complete -eq $false) {
|
||||
$result.elapsed = ((Get-Date) - $module_start).TotalSeconds
|
||||
$result.wait_attempts = $attempts
|
||||
Fail-Json $result "timeout while waiting for $($hostname):$port to start listening"
|
||||
}
|
||||
} elseif ($state -in @("stopped","absent")) {
|
||||
# check that the port is offline and is not listening
|
||||
$start_time = Get-Date
|
||||
$complete = $false
|
||||
while (((Get-Date) - $start_time).TotalSeconds -lt $timeout) {
|
||||
$attempts += 1
|
||||
$port_result = Test-Port -hostname $hostname -port $port
|
||||
if ($port_result -eq $false) {
|
||||
$complete = $true
|
||||
break
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $sleep
|
||||
}
|
||||
|
||||
if ($complete -eq $false) {
|
||||
$result.elapsed = ((Get-Date) - $module_start).TotalSeconds
|
||||
$result.wait_attempts = $attempts
|
||||
Fail-Json $result "timeout while waiting for $($hostname):$port to stop listening"
|
||||
}
|
||||
} elseif ($state -eq "drained") {
|
||||
# check that the local port is online but has no active connections
|
||||
$start_time = Get-Date
|
||||
$complete = $false
|
||||
while (((Get-Date) - $start_time).TotalSeconds -lt $timeout) {
|
||||
$attempts += 1
|
||||
$active_connections = Get-PortConnections -hostname $hostname -port $port
|
||||
if ($null -eq $active_connections) {
|
||||
$complete = $true
|
||||
break
|
||||
} elseif ($active_connections.Count -eq 0) {
|
||||
# no connections on port
|
||||
$complete = $true
|
||||
break
|
||||
} else {
|
||||
# there are listeners, check if we should ignore any hosts
|
||||
if ($null -ne $exclude_hosts) {
|
||||
$connection_info = $active_connections
|
||||
foreach ($exclude_host in $exclude_hosts) {
|
||||
try {
|
||||
$exclude_ips = [System.Net.Dns]::GetHostAddresses($exclude_host) | ForEach-Object { Write-Output $_.IPAddressToString }
|
||||
$connection_info = $connection_info | Where-Object { $_ -notin $exclude_ips }
|
||||
} catch { # ignore invalid hostnames
|
||||
Add-Warning -obj $result -message "Invalid hostname specified $exclude_host"
|
||||
}
|
||||
}
|
||||
|
||||
if ($connection_info.Count -eq 0) {
|
||||
$complete = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $sleep
|
||||
}
|
||||
|
||||
if ($complete -eq $false) {
|
||||
$result.elapsed = ((Get-Date) - $module_start).TotalSeconds
|
||||
$result.wait_attempts = $attempts
|
||||
Fail-Json $result "timeout while waiting for $($hostname):$port to drain"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$result.elapsed = ((Get-Date) - $module_start).TotalSeconds
|
||||
$result.wait_attempts = $attempts
|
||||
|
||||
Exit-Json $result
|
||||
@ -1,155 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
# this is a windows documentation stub, actual code lives in the .ps1
|
||||
# file of the same name
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_wait_for
|
||||
version_added: '2.4'
|
||||
short_description: Waits for a condition before continuing
|
||||
description:
|
||||
- You can wait for a set amount of time C(timeout), this is the default if
|
||||
nothing is specified.
|
||||
- Waiting for a port to become available is useful for when services are not
|
||||
immediately available after their init scripts return which is true of
|
||||
certain Java application servers.
|
||||
- You can wait for a file to exist or not exist on the filesystem.
|
||||
- This module can also be used to wait for a regex match string to be present
|
||||
in a file.
|
||||
- You can wait for active connections to be closed before continuing on a
|
||||
local port.
|
||||
options:
|
||||
connect_timeout:
|
||||
description:
|
||||
- The maximum number of seconds to wait for a connection to happen before
|
||||
closing and retrying.
|
||||
type: int
|
||||
default: 5
|
||||
delay:
|
||||
description:
|
||||
- The number of seconds to wait before starting to poll.
|
||||
type: int
|
||||
exclude_hosts:
|
||||
description:
|
||||
- The list of hosts or IPs to ignore when looking for active TCP
|
||||
connections when C(state=drained).
|
||||
type: list
|
||||
host:
|
||||
description:
|
||||
- A resolvable hostname or IP address to wait for.
|
||||
- If C(state=drained) then it will only check for connections on the IP
|
||||
specified, you can use '0.0.0.0' to use all host IPs.
|
||||
type: str
|
||||
default: '127.0.0.1'
|
||||
path:
|
||||
description:
|
||||
- The path to a file on the filesystem to check.
|
||||
- If C(state) is present or started then it will wait until the file
|
||||
exists.
|
||||
- If C(state) is absent then it will wait until the file does not exist.
|
||||
type: path
|
||||
port:
|
||||
description:
|
||||
- The port number to poll on C(host).
|
||||
type: int
|
||||
regex:
|
||||
description:
|
||||
- Can be used to match a string in a file.
|
||||
- If C(state) is present or started then it will wait until the regex
|
||||
matches.
|
||||
- If C(state) is absent then it will wait until the regex does not match.
|
||||
- Defaults to a multiline regex.
|
||||
type: str
|
||||
aliases: [ "search_regex", "regexp" ]
|
||||
sleep:
|
||||
description:
|
||||
- Number of seconds to sleep between checks.
|
||||
type: int
|
||||
default: 1
|
||||
state:
|
||||
description:
|
||||
- When checking a port, C(started) will ensure the port is open, C(stopped)
|
||||
will check that is it closed and C(drained) will check for active
|
||||
connections.
|
||||
- When checking for a file or a search string C(present) or C(started) will
|
||||
ensure that the file or string is present, C(absent) will check that the
|
||||
file or search string is absent or removed.
|
||||
type: str
|
||||
choices: [ absent, drained, present, started, stopped ]
|
||||
default: started
|
||||
timeout:
|
||||
description:
|
||||
- The maximum number of seconds to wait for.
|
||||
type: int
|
||||
default: 300
|
||||
seealso:
|
||||
- module: wait_for
|
||||
- module: win_wait_for_process
|
||||
author:
|
||||
- Jordan Borean (@jborean93)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Wait 300 seconds for port 8000 to become open on the host, don't start checking for 10 seconds
|
||||
win_wait_for:
|
||||
port: 8000
|
||||
delay: 10
|
||||
|
||||
- name: Wait 150 seconds for port 8000 of any IP to close active connections
|
||||
win_wait_for:
|
||||
host: 0.0.0.0
|
||||
port: 8000
|
||||
state: drained
|
||||
timeout: 150
|
||||
|
||||
- name: Wait for port 8000 of any IP to close active connection, ignoring certain hosts
|
||||
win_wait_for:
|
||||
host: 0.0.0.0
|
||||
port: 8000
|
||||
state: drained
|
||||
exclude_hosts: ['10.2.1.2', '10.2.1.3']
|
||||
|
||||
- name: Wait for file C:\temp\log.txt to exist before continuing
|
||||
win_wait_for:
|
||||
path: C:\temp\log.txt
|
||||
|
||||
- name: Wait until process complete is in the file before continuing
|
||||
win_wait_for:
|
||||
path: C:\temp\log.txt
|
||||
regex: process complete
|
||||
|
||||
- name: Wait until file is removed
|
||||
win_wait_for:
|
||||
path: C:\temp\log.txt
|
||||
state: absent
|
||||
|
||||
- name: Wait until port 1234 is offline but try every 10 seconds
|
||||
win_wait_for:
|
||||
port: 1234
|
||||
state: absent
|
||||
sleep: 10
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
wait_attempts:
|
||||
description: The number of attempts to poll the file or port before module
|
||||
finishes.
|
||||
returned: always
|
||||
type: int
|
||||
sample: 1
|
||||
elapsed:
|
||||
description: The elapsed seconds between the start of poll and the end of the
|
||||
module. This includes the delay if the option is set.
|
||||
returned: always
|
||||
type: float
|
||||
sample: 2.1406487
|
||||
'''
|
||||
@ -1,837 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
#Requires -Module Ansible.ModuleUtils.CamelConversion
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$params = Parse-Args $args -supports_check_mode $true
|
||||
$_remote_tmp = Get-AnsibleParam $params "_ansible_remote_tmp" -type "path" -default $env:TMP
|
||||
|
||||
$session_util = @'
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Principal;
|
||||
using System.Text;
|
||||
|
||||
namespace Ansible
|
||||
{
|
||||
public class SessionInfo
|
||||
{
|
||||
// SECURITY_LOGON_SESSION_DATA
|
||||
public UInt64 LogonId { get; internal set; }
|
||||
public Sid Account { get; internal set; }
|
||||
public string LoginDomain { get; internal set; }
|
||||
public string AuthenticationPackage { get; internal set; }
|
||||
public SECURITY_LOGON_TYPE LogonType { get; internal set; }
|
||||
public string LoginTime { get; internal set; }
|
||||
public string LogonServer { get; internal set; }
|
||||
public string DnsDomainName { get; internal set; }
|
||||
public string Upn { get; internal set; }
|
||||
public ArrayList UserFlags { get; internal set; }
|
||||
|
||||
// TOKEN_STATISTICS
|
||||
public SECURITY_IMPERSONATION_LEVEL ImpersonationLevel { get; internal set; }
|
||||
public TOKEN_TYPE TokenType { get; internal set; }
|
||||
|
||||
// TOKEN_GROUPS
|
||||
public ArrayList Groups { get; internal set; }
|
||||
public ArrayList Rights { get; internal set; }
|
||||
|
||||
// TOKEN_MANDATORY_LABEL
|
||||
public Sid Label { get; internal set; }
|
||||
|
||||
// TOKEN_PRIVILEGES
|
||||
public Hashtable Privileges { get; internal set; }
|
||||
}
|
||||
|
||||
public class Win32Exception : System.ComponentModel.Win32Exception
|
||||
{
|
||||
private string _msg;
|
||||
public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
|
||||
public Win32Exception(int errorCode, string message) : base(errorCode)
|
||||
{
|
||||
_msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode);
|
||||
}
|
||||
public override string Message { get { return _msg; } }
|
||||
public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
public struct LSA_UNICODE_STRING
|
||||
{
|
||||
public UInt16 Length;
|
||||
public UInt16 MaximumLength;
|
||||
public IntPtr buffer;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct LUID
|
||||
{
|
||||
public UInt32 LowPart;
|
||||
public Int32 HighPart;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct SECURITY_LOGON_SESSION_DATA
|
||||
{
|
||||
public UInt32 Size;
|
||||
public LUID LogonId;
|
||||
public LSA_UNICODE_STRING Username;
|
||||
public LSA_UNICODE_STRING LoginDomain;
|
||||
public LSA_UNICODE_STRING AuthenticationPackage;
|
||||
public SECURITY_LOGON_TYPE LogonType;
|
||||
public UInt32 Session;
|
||||
public IntPtr Sid;
|
||||
public UInt64 LoginTime;
|
||||
public LSA_UNICODE_STRING LogonServer;
|
||||
public LSA_UNICODE_STRING DnsDomainName;
|
||||
public LSA_UNICODE_STRING Upn;
|
||||
public UInt32 UserFlags;
|
||||
public LSA_LAST_INTER_LOGON_INFO LastLogonInfo;
|
||||
public LSA_UNICODE_STRING LogonScript;
|
||||
public LSA_UNICODE_STRING ProfilePath;
|
||||
public LSA_UNICODE_STRING HomeDirectory;
|
||||
public LSA_UNICODE_STRING HomeDirectoryDrive;
|
||||
public UInt64 LogoffTime;
|
||||
public UInt64 KickOffTime;
|
||||
public UInt64 PasswordLastSet;
|
||||
public UInt64 PasswordCanChange;
|
||||
public UInt64 PasswordMustChange;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct LSA_LAST_INTER_LOGON_INFO
|
||||
{
|
||||
public UInt64 LastSuccessfulLogon;
|
||||
public UInt64 LastFailedLogon;
|
||||
public UInt32 FailedAttemptCountSinceLastSuccessfulLogon;
|
||||
}
|
||||
|
||||
public enum TOKEN_TYPE
|
||||
{
|
||||
TokenPrimary = 1,
|
||||
TokenImpersonation
|
||||
}
|
||||
|
||||
public enum SECURITY_IMPERSONATION_LEVEL
|
||||
{
|
||||
SecurityAnonymous,
|
||||
SecurityIdentification,
|
||||
SecurityImpersonation,
|
||||
SecurityDelegation
|
||||
}
|
||||
|
||||
public enum SECURITY_LOGON_TYPE
|
||||
{
|
||||
System = 0, // Used only by the Sytem account
|
||||
Interactive = 2,
|
||||
Network,
|
||||
Batch,
|
||||
Service,
|
||||
Proxy,
|
||||
Unlock,
|
||||
NetworkCleartext,
|
||||
NewCredentials,
|
||||
RemoteInteractive,
|
||||
CachedInteractive,
|
||||
CachedRemoteInteractive,
|
||||
CachedUnlock
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum TokenGroupAttributes : uint
|
||||
{
|
||||
SE_GROUP_ENABLED = 0x00000004,
|
||||
SE_GROUP_ENABLED_BY_DEFAULT = 0x00000002,
|
||||
SE_GROUP_INTEGRITY = 0x00000020,
|
||||
SE_GROUP_INTEGRITY_ENABLED = 0x00000040,
|
||||
SE_GROUP_LOGON_ID = 0xC0000000,
|
||||
SE_GROUP_MANDATORY = 0x00000001,
|
||||
SE_GROUP_OWNER = 0x00000008,
|
||||
SE_GROUP_RESOURCE = 0x20000000,
|
||||
SE_GROUP_USE_FOR_DENY_ONLY = 0x00000010,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum UserFlags : uint
|
||||
{
|
||||
LOGON_OPTIMIZED = 0x4000,
|
||||
LOGON_WINLOGON = 0x8000,
|
||||
LOGON_PKINIT = 0x10000,
|
||||
LOGON_NOT_OPTMIZED = 0x20000,
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct SID_AND_ATTRIBUTES
|
||||
{
|
||||
public IntPtr Sid;
|
||||
public UInt32 Attributes;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct LUID_AND_ATTRIBUTES
|
||||
{
|
||||
public LUID Luid;
|
||||
public UInt32 Attributes;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct TOKEN_GROUPS
|
||||
{
|
||||
public UInt32 GroupCount;
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
|
||||
public SID_AND_ATTRIBUTES[] Groups;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct TOKEN_MANDATORY_LABEL
|
||||
{
|
||||
public SID_AND_ATTRIBUTES Label;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct TOKEN_STATISTICS
|
||||
{
|
||||
public LUID TokenId;
|
||||
public LUID AuthenticationId;
|
||||
public UInt64 ExpirationTime;
|
||||
public TOKEN_TYPE TokenType;
|
||||
public SECURITY_IMPERSONATION_LEVEL ImpersonationLevel;
|
||||
public UInt32 DynamicCharged;
|
||||
public UInt32 DynamicAvailable;
|
||||
public UInt32 GroupCount;
|
||||
public UInt32 PrivilegeCount;
|
||||
public LUID ModifiedId;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct TOKEN_PRIVILEGES
|
||||
{
|
||||
public UInt32 PrivilegeCount;
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
|
||||
public LUID_AND_ATTRIBUTES[] Privileges;
|
||||
}
|
||||
|
||||
public class AccessToken : IDisposable
|
||||
{
|
||||
public enum TOKEN_INFORMATION_CLASS
|
||||
{
|
||||
TokenUser = 1,
|
||||
TokenGroups,
|
||||
TokenPrivileges,
|
||||
TokenOwner,
|
||||
TokenPrimaryGroup,
|
||||
TokenDefaultDacl,
|
||||
TokenSource,
|
||||
TokenType,
|
||||
TokenImpersonationLevel,
|
||||
TokenStatistics,
|
||||
TokenRestrictedSids,
|
||||
TokenSessionId,
|
||||
TokenGroupsAndPrivileges,
|
||||
TokenSessionReference,
|
||||
TokenSandBoxInert,
|
||||
TokenAuditPolicy,
|
||||
TokenOrigin,
|
||||
TokenElevationType,
|
||||
TokenLinkedToken,
|
||||
TokenElevation,
|
||||
TokenHasRestrictions,
|
||||
TokenAccessInformation,
|
||||
TokenVirtualizationAllowed,
|
||||
TokenVirtualizationEnabled,
|
||||
TokenIntegrityLevel,
|
||||
TokenUIAccess,
|
||||
TokenMandatoryPolicy,
|
||||
TokenLogonSid,
|
||||
TokenIsAppContainer,
|
||||
TokenCapabilities,
|
||||
TokenAppContainerSid,
|
||||
TokenAppContainerNumber,
|
||||
TokenUserClaimAttributes,
|
||||
TokenDeviceClaimAttributes,
|
||||
TokenRestrictedUserClaimAttributes,
|
||||
TokenRestrictedDeviceClaimAttributes,
|
||||
TokenDeviceGroups,
|
||||
TokenRestrictedDeviceGroups,
|
||||
TokenSecurityAttributes,
|
||||
TokenIsRestricted,
|
||||
MaxTokenInfoClass
|
||||
}
|
||||
|
||||
public IntPtr hToken = IntPtr.Zero;
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern IntPtr GetCurrentProcess();
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
private static extern bool OpenProcessToken(
|
||||
IntPtr ProcessHandle,
|
||||
TokenAccessLevels DesiredAccess,
|
||||
out IntPtr TokenHandle);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
private static extern bool GetTokenInformation(
|
||||
IntPtr TokenHandle,
|
||||
TOKEN_INFORMATION_CLASS TokenInformationClass,
|
||||
IntPtr TokenInformation,
|
||||
UInt32 TokenInformationLength,
|
||||
out UInt32 ReturnLength);
|
||||
|
||||
public AccessToken(TokenAccessLevels tokenAccessLevels)
|
||||
{
|
||||
IntPtr currentProcess = GetCurrentProcess();
|
||||
if (!OpenProcessToken(currentProcess, tokenAccessLevels, out hToken))
|
||||
throw new Win32Exception("OpenProcessToken() for current process failed");
|
||||
}
|
||||
|
||||
public IntPtr GetTokenInformation<T>(out T tokenInformation, TOKEN_INFORMATION_CLASS tokenClass)
|
||||
{
|
||||
UInt32 tokenLength = 0;
|
||||
GetTokenInformation(hToken, tokenClass, IntPtr.Zero, 0, out tokenLength);
|
||||
|
||||
IntPtr infoPtr = Marshal.AllocHGlobal((int)tokenLength);
|
||||
|
||||
if (!GetTokenInformation(hToken, tokenClass, infoPtr, tokenLength, out tokenLength))
|
||||
throw new Win32Exception(String.Format("GetTokenInformation() data for {0} failed", tokenClass.ToString()));
|
||||
|
||||
tokenInformation = (T)Marshal.PtrToStructure(infoPtr, typeof(T));
|
||||
return infoPtr;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
~AccessToken() { Dispose(); }
|
||||
}
|
||||
|
||||
public class LsaHandle : IDisposable
|
||||
{
|
||||
[Flags]
|
||||
public enum DesiredAccess : uint
|
||||
{
|
||||
POLICY_VIEW_LOCAL_INFORMATION = 0x00000001,
|
||||
POLICY_VIEW_AUDIT_INFORMATION = 0x00000002,
|
||||
POLICY_GET_PRIVATE_INFORMATION = 0x00000004,
|
||||
POLICY_TRUST_ADMIN = 0x00000008,
|
||||
POLICY_CREATE_ACCOUNT = 0x00000010,
|
||||
POLICY_CREATE_SECRET = 0x00000020,
|
||||
POLICY_CREATE_PRIVILEGE = 0x00000040,
|
||||
POLICY_SET_DEFAULT_QUOTA_LIMITS = 0x00000080,
|
||||
POLICY_SET_AUDIT_REQUIREMENTS = 0x00000100,
|
||||
POLICY_AUDIT_LOG_ADMIN = 0x00000200,
|
||||
POLICY_SERVER_ADMIN = 0x00000400,
|
||||
POLICY_LOOKUP_NAMES = 0x00000800,
|
||||
POLICY_NOTIFICATION = 0x00001000
|
||||
}
|
||||
|
||||
public IntPtr handle = IntPtr.Zero;
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
private static extern uint LsaOpenPolicy(
|
||||
LSA_UNICODE_STRING[] SystemName,
|
||||
ref LSA_OBJECT_ATTRIBUTES ObjectAttributes,
|
||||
DesiredAccess AccessMask,
|
||||
out IntPtr PolicyHandle);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
private static extern uint LsaClose(
|
||||
IntPtr ObjectHandle);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = false)]
|
||||
private static extern int LsaNtStatusToWinError(
|
||||
uint Status);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct LSA_OBJECT_ATTRIBUTES
|
||||
{
|
||||
public int Length;
|
||||
public IntPtr RootDirectory;
|
||||
public IntPtr ObjectName;
|
||||
public int Attributes;
|
||||
public IntPtr SecurityDescriptor;
|
||||
public IntPtr SecurityQualityOfService;
|
||||
}
|
||||
|
||||
public LsaHandle(DesiredAccess desiredAccess)
|
||||
{
|
||||
LSA_OBJECT_ATTRIBUTES lsaAttr;
|
||||
lsaAttr.RootDirectory = IntPtr.Zero;
|
||||
lsaAttr.ObjectName = IntPtr.Zero;
|
||||
lsaAttr.Attributes = 0;
|
||||
lsaAttr.SecurityDescriptor = IntPtr.Zero;
|
||||
lsaAttr.SecurityQualityOfService = IntPtr.Zero;
|
||||
lsaAttr.Length = Marshal.SizeOf(typeof(LSA_OBJECT_ATTRIBUTES));
|
||||
LSA_UNICODE_STRING[] system = new LSA_UNICODE_STRING[1];
|
||||
system[0].buffer = IntPtr.Zero;
|
||||
|
||||
uint res = LsaOpenPolicy(system, ref lsaAttr, desiredAccess, out handle);
|
||||
if (res != 0)
|
||||
throw new Win32Exception(LsaNtStatusToWinError(res), "LsaOpenPolicy() failed");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (handle != IntPtr.Zero)
|
||||
{
|
||||
LsaClose(handle);
|
||||
handle = IntPtr.Zero;
|
||||
}
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
~LsaHandle() { Dispose(); }
|
||||
}
|
||||
|
||||
public class Sid
|
||||
{
|
||||
public string SidString { get; internal set; }
|
||||
public string DomainName { get; internal set; }
|
||||
public string AccountName { get; internal set; }
|
||||
public SID_NAME_USE SidType { get; internal set; }
|
||||
|
||||
public enum SID_NAME_USE
|
||||
{
|
||||
SidTypeUser = 1,
|
||||
SidTypeGroup,
|
||||
SidTypeDomain,
|
||||
SidTypeAlias,
|
||||
SidTypeWellKnownGroup,
|
||||
SidTypeDeletedAccount,
|
||||
SidTypeInvalid,
|
||||
SidTypeUnknown,
|
||||
SidTypeComputer,
|
||||
SidTypeLabel,
|
||||
SidTypeLogon,
|
||||
}
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern bool LookupAccountSid(
|
||||
string lpSystemName,
|
||||
[MarshalAs(UnmanagedType.LPArray)]
|
||||
byte[] Sid,
|
||||
StringBuilder lpName,
|
||||
ref UInt32 cchName,
|
||||
StringBuilder ReferencedDomainName,
|
||||
ref UInt32 cchReferencedDomainName,
|
||||
out SID_NAME_USE peUse);
|
||||
|
||||
public Sid(IntPtr sidPtr)
|
||||
{
|
||||
SecurityIdentifier sid;
|
||||
try
|
||||
{
|
||||
sid = new SecurityIdentifier(sidPtr);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new ArgumentException(String.Format("Failed to cast IntPtr to SecurityIdentifier: {0}", e));
|
||||
}
|
||||
|
||||
SetSidInfo(sid);
|
||||
}
|
||||
|
||||
public Sid(SecurityIdentifier sid)
|
||||
{
|
||||
SetSidInfo(sid);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return SidString;
|
||||
}
|
||||
|
||||
private void SetSidInfo(SecurityIdentifier sid)
|
||||
{
|
||||
byte[] sidBytes = new byte[sid.BinaryLength];
|
||||
sid.GetBinaryForm(sidBytes, 0);
|
||||
|
||||
StringBuilder lpName = new StringBuilder();
|
||||
UInt32 cchName = 0;
|
||||
StringBuilder referencedDomainName = new StringBuilder();
|
||||
UInt32 cchReferencedDomainName = 0;
|
||||
SID_NAME_USE peUse;
|
||||
LookupAccountSid(null, sidBytes, lpName, ref cchName, referencedDomainName, ref cchReferencedDomainName, out peUse);
|
||||
|
||||
lpName.EnsureCapacity((int)cchName);
|
||||
referencedDomainName.EnsureCapacity((int)cchReferencedDomainName);
|
||||
|
||||
SidString = sid.ToString();
|
||||
if (!LookupAccountSid(null, sidBytes, lpName, ref cchName, referencedDomainName, ref cchReferencedDomainName, out peUse))
|
||||
{
|
||||
int lastError = Marshal.GetLastWin32Error();
|
||||
|
||||
if (lastError != 1332 && lastError != 1789) // Fails to lookup Logon Sid
|
||||
{
|
||||
throw new Win32Exception(lastError, String.Format("LookupAccountSid() failed for SID: {0} {1}", sid.ToString(), lastError));
|
||||
}
|
||||
else if (SidString.StartsWith("S-1-5-5-"))
|
||||
{
|
||||
AccountName = String.Format("LogonSessionId_{0}", SidString.Substring(8));
|
||||
DomainName = "NT AUTHORITY";
|
||||
SidType = SID_NAME_USE.SidTypeLogon;
|
||||
}
|
||||
else
|
||||
{
|
||||
AccountName = null;
|
||||
DomainName = null;
|
||||
SidType = SID_NAME_USE.SidTypeUnknown;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AccountName = lpName.ToString();
|
||||
DomainName = referencedDomainName.ToString();
|
||||
SidType = peUse;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class SessionUtil
|
||||
{
|
||||
[DllImport("secur32.dll", SetLastError = false)]
|
||||
private static extern uint LsaFreeReturnBuffer(
|
||||
IntPtr Buffer);
|
||||
|
||||
[DllImport("secur32.dll", SetLastError = false)]
|
||||
private static extern uint LsaEnumerateLogonSessions(
|
||||
out UInt64 LogonSessionCount,
|
||||
out IntPtr LogonSessionList);
|
||||
|
||||
[DllImport("secur32.dll", SetLastError = false)]
|
||||
private static extern uint LsaGetLogonSessionData(
|
||||
IntPtr LogonId,
|
||||
out IntPtr ppLogonSessionData);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = false)]
|
||||
private static extern int LsaNtStatusToWinError(
|
||||
uint Status);
|
||||
|
||||
[DllImport("advapi32", SetLastError = true)]
|
||||
private static extern uint LsaEnumerateAccountRights(
|
||||
IntPtr PolicyHandle,
|
||||
IntPtr AccountSid,
|
||||
out IntPtr UserRights,
|
||||
out UInt64 CountOfRights);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern bool LookupPrivilegeName(
|
||||
string lpSystemName,
|
||||
ref LUID lpLuid,
|
||||
StringBuilder lpName,
|
||||
ref UInt32 cchName);
|
||||
|
||||
private const UInt32 SE_PRIVILEGE_ENABLED_BY_DEFAULT = 0x00000001;
|
||||
private const UInt32 SE_PRIVILEGE_ENABLED = 0x00000002;
|
||||
private const UInt32 STATUS_OBJECT_NAME_NOT_FOUND = 0xC0000034;
|
||||
private const UInt32 STATUS_ACCESS_DENIED = 0xC0000022;
|
||||
|
||||
public static SessionInfo GetSessionInfo()
|
||||
{
|
||||
AccessToken accessToken = new AccessToken(TokenAccessLevels.Query);
|
||||
|
||||
// Get Privileges
|
||||
Hashtable privilegeInfo = new Hashtable();
|
||||
TOKEN_PRIVILEGES privileges;
|
||||
IntPtr privilegesPtr = accessToken.GetTokenInformation(out privileges, AccessToken.TOKEN_INFORMATION_CLASS.TokenPrivileges);
|
||||
LUID_AND_ATTRIBUTES[] luidAndAttributes = new LUID_AND_ATTRIBUTES[privileges.PrivilegeCount];
|
||||
try
|
||||
{
|
||||
PtrToStructureArray(luidAndAttributes, privilegesPtr.ToInt64() + Marshal.SizeOf(privileges.PrivilegeCount));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(privilegesPtr);
|
||||
}
|
||||
foreach (LUID_AND_ATTRIBUTES luidAndAttribute in luidAndAttributes)
|
||||
{
|
||||
LUID privLuid = luidAndAttribute.Luid;
|
||||
UInt32 privNameLen = 0;
|
||||
StringBuilder privName = new StringBuilder();
|
||||
LookupPrivilegeName(null, ref privLuid, null, ref privNameLen);
|
||||
privName.EnsureCapacity((int)(privNameLen + 1));
|
||||
if (!LookupPrivilegeName(null, ref privLuid, privName, ref privNameLen))
|
||||
throw new Win32Exception("LookupPrivilegeName() failed");
|
||||
|
||||
string state = "disabled";
|
||||
if ((luidAndAttribute.Attributes & SE_PRIVILEGE_ENABLED) == SE_PRIVILEGE_ENABLED)
|
||||
state = "enabled";
|
||||
if ((luidAndAttribute.Attributes & SE_PRIVILEGE_ENABLED_BY_DEFAULT) == SE_PRIVILEGE_ENABLED_BY_DEFAULT)
|
||||
state = "enabled-by-default";
|
||||
privilegeInfo.Add(privName.ToString(), state);
|
||||
}
|
||||
|
||||
// Get Current Process LogonSID, User Rights and Groups
|
||||
ArrayList userRights = new ArrayList();
|
||||
ArrayList userGroups = new ArrayList();
|
||||
TOKEN_GROUPS groups;
|
||||
IntPtr groupsPtr = accessToken.GetTokenInformation(out groups, AccessToken.TOKEN_INFORMATION_CLASS.TokenGroups);
|
||||
SID_AND_ATTRIBUTES[] sidAndAttributes = new SID_AND_ATTRIBUTES[groups.GroupCount];
|
||||
LsaHandle lsaHandle = null;
|
||||
// We can only get rights if we are an admin
|
||||
if (new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator))
|
||||
lsaHandle = new LsaHandle(LsaHandle.DesiredAccess.POLICY_LOOKUP_NAMES);
|
||||
try
|
||||
{
|
||||
PtrToStructureArray(sidAndAttributes, groupsPtr.ToInt64() + IntPtr.Size);
|
||||
foreach (SID_AND_ATTRIBUTES sidAndAttribute in sidAndAttributes)
|
||||
{
|
||||
TokenGroupAttributes attributes = (TokenGroupAttributes)sidAndAttribute.Attributes;
|
||||
if (attributes.HasFlag(TokenGroupAttributes.SE_GROUP_ENABLED) && lsaHandle != null)
|
||||
{
|
||||
ArrayList rights = GetAccountRights(lsaHandle.handle, sidAndAttribute.Sid);
|
||||
foreach (string right in rights)
|
||||
{
|
||||
// Includes both Privileges and Account Rights, only add the ones with Logon in the name
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/bb545671(v=vs.85).aspx
|
||||
if (!userRights.Contains(right) && right.Contains("Logon"))
|
||||
userRights.Add(right);
|
||||
}
|
||||
}
|
||||
// Do not include the Logon SID in the groups category
|
||||
if (!attributes.HasFlag(TokenGroupAttributes.SE_GROUP_LOGON_ID))
|
||||
{
|
||||
Hashtable groupInfo = new Hashtable();
|
||||
Sid group = new Sid(sidAndAttribute.Sid);
|
||||
ArrayList groupAttributes = new ArrayList();
|
||||
foreach (TokenGroupAttributes attribute in Enum.GetValues(typeof(TokenGroupAttributes)))
|
||||
{
|
||||
if (attributes.HasFlag(attribute))
|
||||
{
|
||||
string attributeName = attribute.ToString().Substring(9);
|
||||
attributeName = attributeName.Replace('_', ' ');
|
||||
attributeName = attributeName.First().ToString().ToUpper() + attributeName.Substring(1).ToLower();
|
||||
groupAttributes.Add(attributeName);
|
||||
}
|
||||
}
|
||||
// Using snake_case here as I can't generically convert all dict keys in PS (see Privileges)
|
||||
groupInfo.Add("sid", group.SidString);
|
||||
groupInfo.Add("domain_name", group.DomainName);
|
||||
groupInfo.Add("account_name", group.AccountName);
|
||||
groupInfo.Add("type", group.SidType);
|
||||
groupInfo.Add("attributes", groupAttributes);
|
||||
userGroups.Add(groupInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(groupsPtr);
|
||||
if (lsaHandle != null)
|
||||
lsaHandle.Dispose();
|
||||
}
|
||||
|
||||
// Get Integrity Level
|
||||
Sid integritySid = null;
|
||||
TOKEN_MANDATORY_LABEL mandatoryLabel;
|
||||
IntPtr mandatoryLabelPtr = accessToken.GetTokenInformation(out mandatoryLabel, AccessToken.TOKEN_INFORMATION_CLASS.TokenIntegrityLevel);
|
||||
Marshal.FreeHGlobal(mandatoryLabelPtr);
|
||||
integritySid = new Sid(mandatoryLabel.Label.Sid);
|
||||
|
||||
// Get Token Statistics
|
||||
TOKEN_STATISTICS tokenStats;
|
||||
IntPtr tokenStatsPtr = accessToken.GetTokenInformation(out tokenStats, AccessToken.TOKEN_INFORMATION_CLASS.TokenStatistics);
|
||||
Marshal.FreeHGlobal(tokenStatsPtr);
|
||||
|
||||
SessionInfo sessionInfo = GetSessionDataForLogonSession(tokenStats.AuthenticationId);
|
||||
sessionInfo.Groups = userGroups;
|
||||
sessionInfo.Label = integritySid;
|
||||
sessionInfo.ImpersonationLevel = tokenStats.ImpersonationLevel;
|
||||
sessionInfo.TokenType = tokenStats.TokenType;
|
||||
sessionInfo.Privileges = privilegeInfo;
|
||||
sessionInfo.Rights = userRights;
|
||||
return sessionInfo;
|
||||
}
|
||||
|
||||
private static ArrayList GetAccountRights(IntPtr lsaHandle, IntPtr sid)
|
||||
{
|
||||
UInt32 res;
|
||||
ArrayList rights = new ArrayList();
|
||||
IntPtr userRightsPointer = IntPtr.Zero;
|
||||
UInt64 countOfRights = 0;
|
||||
|
||||
res = LsaEnumerateAccountRights(lsaHandle, sid, out userRightsPointer, out countOfRights);
|
||||
if (res != 0 && res != STATUS_OBJECT_NAME_NOT_FOUND)
|
||||
throw new Win32Exception(LsaNtStatusToWinError(res), "LsaEnumerateAccountRights() failed");
|
||||
else if (res != STATUS_OBJECT_NAME_NOT_FOUND)
|
||||
{
|
||||
LSA_UNICODE_STRING[] userRights = new LSA_UNICODE_STRING[countOfRights];
|
||||
PtrToStructureArray(userRights, userRightsPointer.ToInt64());
|
||||
rights = new ArrayList();
|
||||
foreach (LSA_UNICODE_STRING right in userRights)
|
||||
rights.Add(Marshal.PtrToStringUni(right.buffer));
|
||||
}
|
||||
|
||||
return rights;
|
||||
}
|
||||
|
||||
private static SessionInfo GetSessionDataForLogonSession(LUID logonSession)
|
||||
{
|
||||
uint res;
|
||||
UInt64 count = 0;
|
||||
IntPtr luidPtr = IntPtr.Zero;
|
||||
SessionInfo sessionInfo = null;
|
||||
UInt64 processDataId = ConvertLuidToUint(logonSession);
|
||||
|
||||
res = LsaEnumerateLogonSessions(out count, out luidPtr);
|
||||
if (res != 0)
|
||||
throw new Win32Exception(LsaNtStatusToWinError(res), "LsaEnumerateLogonSessions() failed");
|
||||
Int64 luidAddr = luidPtr.ToInt64();
|
||||
|
||||
try
|
||||
{
|
||||
for (UInt64 i = 0; i < count; i++)
|
||||
{
|
||||
IntPtr dataPointer = IntPtr.Zero;
|
||||
res = LsaGetLogonSessionData(luidPtr, out dataPointer);
|
||||
if (res == STATUS_ACCESS_DENIED) // Non admins won't be able to get info for session's that are not their own
|
||||
{
|
||||
luidPtr = new IntPtr(luidPtr.ToInt64() + Marshal.SizeOf(typeof(LUID)));
|
||||
continue;
|
||||
}
|
||||
else if (res != 0)
|
||||
throw new Win32Exception(LsaNtStatusToWinError(res), String.Format("LsaGetLogonSessionData() failed {0}", res));
|
||||
|
||||
SECURITY_LOGON_SESSION_DATA sessionData = (SECURITY_LOGON_SESSION_DATA)Marshal.PtrToStructure(dataPointer, typeof(SECURITY_LOGON_SESSION_DATA));
|
||||
UInt64 sessionDataid = ConvertLuidToUint(sessionData.LogonId);
|
||||
|
||||
if (sessionDataid == processDataId)
|
||||
{
|
||||
ArrayList userFlags = new ArrayList();
|
||||
UserFlags flags = (UserFlags)sessionData.UserFlags;
|
||||
foreach (UserFlags flag in Enum.GetValues(typeof(UserFlags)))
|
||||
{
|
||||
if (flags.HasFlag(flag))
|
||||
{
|
||||
string flagName = flag.ToString().Substring(6);
|
||||
flagName = flagName.Replace('_', ' ');
|
||||
flagName = flagName.First().ToString().ToUpper() + flagName.Substring(1).ToLower();
|
||||
userFlags.Add(flagName);
|
||||
}
|
||||
}
|
||||
|
||||
sessionInfo = new SessionInfo()
|
||||
{
|
||||
AuthenticationPackage = Marshal.PtrToStringUni(sessionData.AuthenticationPackage.buffer),
|
||||
DnsDomainName = Marshal.PtrToStringUni(sessionData.DnsDomainName.buffer),
|
||||
LoginDomain = Marshal.PtrToStringUni(sessionData.LoginDomain.buffer),
|
||||
LoginTime = ConvertIntegerToDateString(sessionData.LoginTime),
|
||||
LogonId = ConvertLuidToUint(sessionData.LogonId),
|
||||
LogonServer = Marshal.PtrToStringUni(sessionData.LogonServer.buffer),
|
||||
LogonType = sessionData.LogonType,
|
||||
Upn = Marshal.PtrToStringUni(sessionData.Upn.buffer),
|
||||
UserFlags = userFlags,
|
||||
Account = new Sid(sessionData.Sid)
|
||||
};
|
||||
break;
|
||||
}
|
||||
luidPtr = new IntPtr(luidPtr.ToInt64() + Marshal.SizeOf(typeof(LUID)));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
LsaFreeReturnBuffer(new IntPtr(luidAddr));
|
||||
}
|
||||
|
||||
if (sessionInfo == null)
|
||||
throw new Exception(String.Format("Could not find the data for logon session {0}", processDataId));
|
||||
return sessionInfo;
|
||||
}
|
||||
|
||||
private static string ConvertIntegerToDateString(UInt64 time)
|
||||
{
|
||||
if (time == 0)
|
||||
return null;
|
||||
if (time > (UInt64)DateTime.MaxValue.ToFileTime())
|
||||
return null;
|
||||
|
||||
DateTime dateTime = DateTime.FromFileTime((long)time);
|
||||
return dateTime.ToString("o");
|
||||
}
|
||||
|
||||
private static UInt64 ConvertLuidToUint(LUID luid)
|
||||
{
|
||||
UInt32 low = luid.LowPart;
|
||||
UInt64 high = (UInt64)luid.HighPart;
|
||||
high = high << 32;
|
||||
UInt64 uintValue = (high | (UInt64)low);
|
||||
return uintValue;
|
||||
}
|
||||
|
||||
private static void PtrToStructureArray<T>(T[] array, Int64 pointerAddress)
|
||||
{
|
||||
Int64 pointerOffset = pointerAddress;
|
||||
for (int i = 0; i < array.Length; i++, pointerOffset += Marshal.SizeOf(typeof(T)))
|
||||
array[i] = (T)Marshal.PtrToStructure(new IntPtr(pointerOffset), typeof(T));
|
||||
}
|
||||
|
||||
public static IEnumerable<T> GetValues<T>()
|
||||
{
|
||||
return Enum.GetValues(typeof(T)).Cast<T>();
|
||||
}
|
||||
}
|
||||
}
|
||||
'@
|
||||
|
||||
$original_tmp = $env:TMP
|
||||
$env:TMP = $_remote_tmp
|
||||
Add-Type -TypeDefinition $session_util
|
||||
$env:TMP = $original_tmp
|
||||
|
||||
$session_info = [Ansible.SessionUtil]::GetSessionInfo()
|
||||
|
||||
Function Convert-Value($value) {
|
||||
$new_value = $value
|
||||
if ($value -is [System.Collections.ArrayList]) {
|
||||
$new_value = [System.Collections.ArrayList]@()
|
||||
foreach ($list_value in $value) {
|
||||
$new_list_value = Convert-Value -value $list_value
|
||||
[void]$new_value.Add($new_list_value)
|
||||
}
|
||||
} elseif ($value -is [Hashtable]) {
|
||||
$new_value = @{}
|
||||
foreach ($entry in $value.GetEnumerator()) {
|
||||
$entry_value = Convert-Value -value $entry.Value
|
||||
# manually convert Sid type entry to remove the SidType prefix
|
||||
if ($entry.Name -eq "type") {
|
||||
$entry_value = $entry_value.Replace("SidType", "")
|
||||
}
|
||||
$new_value[$entry.Name] = $entry_value
|
||||
}
|
||||
} elseif ($value -is [Ansible.Sid]) {
|
||||
$new_value = @{
|
||||
sid = $value.SidString
|
||||
account_name = $value.AccountName
|
||||
domain_name = $value.DomainName
|
||||
type = $value.SidType.ToString().Replace("SidType", "")
|
||||
}
|
||||
} elseif ($value -is [Enum]) {
|
||||
$new_value = $value.ToString()
|
||||
}
|
||||
|
||||
return ,$new_value
|
||||
}
|
||||
|
||||
$result = @{
|
||||
changed = $false
|
||||
}
|
||||
|
||||
$properties = [type][Ansible.SessionInfo]
|
||||
foreach ($property in $properties.DeclaredProperties) {
|
||||
$property_name = $property.Name
|
||||
$property_value = $session_info.$property_name
|
||||
$snake_name = Convert-StringToSnakeCase -string $property_name
|
||||
|
||||
$result.$snake_name = Convert-Value -value $property_value
|
||||
}
|
||||
|
||||
Exit-Json -obj $result
|
||||
@ -1,203 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
# this is a windows documentation stub. actual code lives in the .ps1
|
||||
# file of the same name
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_whoami
|
||||
version_added: "2.5"
|
||||
short_description: Get information about the current user and process
|
||||
description:
|
||||
- Designed to return the same information as the C(whoami /all) command.
|
||||
- Also includes information missing from C(whoami) such as logon metadata like
|
||||
logon rights, id, type.
|
||||
notes:
|
||||
- If running this module with a non admin user, the logon rights will be an
|
||||
empty list as Administrator rights are required to query LSA for the
|
||||
information.
|
||||
seealso:
|
||||
- module: win_credential
|
||||
- module: win_group_membership
|
||||
- module: win_user_right
|
||||
author:
|
||||
- Jordan Borean (@jborean93)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Get whoami information
|
||||
win_whoami:
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
authentication_package:
|
||||
description: The name of the authentication package used to authenticate the
|
||||
user in the session.
|
||||
returned: success
|
||||
type: str
|
||||
sample: Negotiate
|
||||
user_flags:
|
||||
description: The user flags for the logon session, see UserFlags in
|
||||
U(https://msdn.microsoft.com/en-us/library/windows/desktop/aa380128).
|
||||
returned: success
|
||||
type: str
|
||||
sample: Winlogon
|
||||
upn:
|
||||
description: The user principal name of the current user.
|
||||
returned: success
|
||||
type: str
|
||||
sample: Administrator@DOMAIN.COM
|
||||
logon_type:
|
||||
description: The logon type that identifies the logon method, see
|
||||
U(https://msdn.microsoft.com/en-us/library/windows/desktop/aa380129.aspx).
|
||||
returned: success
|
||||
type: str
|
||||
sample: Network
|
||||
privileges:
|
||||
description: A dictionary of privileges and their state on the logon token.
|
||||
returned: success
|
||||
type: dict
|
||||
sample: {
|
||||
"SeChangeNotifyPrivileges": "enabled-by-default",
|
||||
"SeRemoteShutdownPrivilege": "disabled",
|
||||
"SeDebugPrivilege": "enabled"
|
||||
}
|
||||
label:
|
||||
description: The mandatory label set to the logon session.
|
||||
returned: success
|
||||
type: complex
|
||||
contains:
|
||||
domain_name:
|
||||
description: The domain name of the label SID.
|
||||
returned: success
|
||||
type: str
|
||||
sample: Mandatory Label
|
||||
sid:
|
||||
description: The SID in string form.
|
||||
returned: success
|
||||
type: str
|
||||
sample: S-1-16-12288
|
||||
account_name:
|
||||
description: The account name of the label SID.
|
||||
returned: success
|
||||
type: str
|
||||
sample: High Mandatory Level
|
||||
type:
|
||||
description: The type of SID.
|
||||
returned: success
|
||||
type: str
|
||||
sample: Label
|
||||
impersonation_level:
|
||||
description: The impersonation level of the token, only valid if
|
||||
C(token_type) is C(TokenImpersonation), see
|
||||
U(https://msdn.microsoft.com/en-us/library/windows/desktop/aa379572.aspx).
|
||||
returned: success
|
||||
type: str
|
||||
sample: SecurityAnonymous
|
||||
login_time:
|
||||
description: The logon time in ISO 8601 format
|
||||
returned: success
|
||||
type: str
|
||||
sample: '2017-11-27T06:24:14.3321665+10:00'
|
||||
groups:
|
||||
description: A list of groups and attributes that the user is a member of.
|
||||
returned: success
|
||||
type: list
|
||||
sample: [
|
||||
{
|
||||
"account_name": "Domain Users",
|
||||
"domain_name": "DOMAIN",
|
||||
"attributes": [
|
||||
"Mandatory",
|
||||
"Enabled by default",
|
||||
"Enabled"
|
||||
],
|
||||
"sid": "S-1-5-21-1654078763-769949647-2968445802-513",
|
||||
"type": "Group"
|
||||
},
|
||||
{
|
||||
"account_name": "Administrators",
|
||||
"domain_name": "BUILTIN",
|
||||
"attributes": [
|
||||
"Mandatory",
|
||||
"Enabled by default",
|
||||
"Enabled",
|
||||
"Owner"
|
||||
],
|
||||
"sid": "S-1-5-32-544",
|
||||
"type": "Alias"
|
||||
}
|
||||
]
|
||||
account:
|
||||
description: The running account SID details.
|
||||
returned: success
|
||||
type: complex
|
||||
contains:
|
||||
domain_name:
|
||||
description: The domain name of the account SID.
|
||||
returned: success
|
||||
type: str
|
||||
sample: DOMAIN
|
||||
sid:
|
||||
description: The SID in string form.
|
||||
returned: success
|
||||
type: str
|
||||
sample: S-1-5-21-1654078763-769949647-2968445802-500
|
||||
account_name:
|
||||
description: The account name of the account SID.
|
||||
returned: success
|
||||
type: str
|
||||
sample: Administrator
|
||||
type:
|
||||
description: The type of SID.
|
||||
returned: success
|
||||
type: str
|
||||
sample: User
|
||||
login_domain:
|
||||
description: The name of the domain used to authenticate the owner of the
|
||||
session.
|
||||
returned: success
|
||||
type: str
|
||||
sample: DOMAIN
|
||||
rights:
|
||||
description: A list of logon rights assigned to the logon.
|
||||
returned: success and running user is a member of the local Administrators group
|
||||
type: list
|
||||
sample: [
|
||||
"SeNetworkLogonRight",
|
||||
"SeInteractiveLogonRight",
|
||||
"SeBatchLogonRight",
|
||||
"SeRemoteInteractiveLogonRight"
|
||||
]
|
||||
logon_server:
|
||||
description: The name of the server used to authenticate the owner of the
|
||||
logon session.
|
||||
returned: success
|
||||
type: str
|
||||
sample: DC01
|
||||
logon_id:
|
||||
description: The unique identifier of the logon session.
|
||||
returned: success
|
||||
type: int
|
||||
sample: 20470143
|
||||
dns_domain_name:
|
||||
description: The DNS name of the logon session, this is an empty string if
|
||||
this is not set.
|
||||
returned: success
|
||||
type: str
|
||||
sample: DOMAIN.COM
|
||||
token_type:
|
||||
description: The token type to indicate whether it is a primary or
|
||||
impersonation token.
|
||||
returned: success
|
||||
type: str
|
||||
sample: TokenPrimary
|
||||
'''
|
||||
@ -1,522 +0,0 @@
|
||||
# This file is part of Ansible
|
||||
|
||||
# Copyright (c) 2017 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
import shutil
|
||||
import tempfile
|
||||
import traceback
|
||||
import zipfile
|
||||
|
||||
from ansible import constants as C
|
||||
from ansible.errors import AnsibleError, AnsibleFileNotFound
|
||||
from ansible.module_utils._text import to_bytes, to_native, to_text
|
||||
from ansible.module_utils.parsing.convert_bool import boolean
|
||||
from ansible.plugins.action import ActionBase
|
||||
from ansible.utils.hashing import checksum
|
||||
|
||||
|
||||
def _walk_dirs(topdir, loader, decrypt=True, base_path=None, local_follow=False, trailing_slash_detector=None, checksum_check=False):
|
||||
"""
|
||||
Walk a filesystem tree returning enough information to copy the files.
|
||||
This is similar to the _walk_dirs function in ``copy.py`` but returns
|
||||
a dict instead of a tuple for each entry and includes the checksum of
|
||||
a local file if wanted.
|
||||
|
||||
:arg topdir: The directory that the filesystem tree is rooted at
|
||||
:arg loader: The self._loader object from ActionBase
|
||||
:kwarg decrypt: Whether to decrypt a file encrypted with ansible-vault
|
||||
:kwarg base_path: The initial directory structure to strip off of the
|
||||
files for the destination directory. If this is None (the default),
|
||||
the base_path is set to ``top_dir``.
|
||||
:kwarg local_follow: Whether to follow symlinks on the source. When set
|
||||
to False, no symlinks are dereferenced. When set to True (the
|
||||
default), the code will dereference most symlinks. However, symlinks
|
||||
can still be present if needed to break a circular link.
|
||||
:kwarg trailing_slash_detector: Function to determine if a path has
|
||||
a trailing directory separator. Only needed when dealing with paths on
|
||||
a remote machine (in which case, pass in a function that is aware of the
|
||||
directory separator conventions on the remote machine).
|
||||
:kawrg whether to get the checksum of the local file and add to the dict
|
||||
:returns: dictionary of dictionaries. All of the path elements in the structure are text string.
|
||||
This separates all the files, directories, and symlinks along with
|
||||
import information about each::
|
||||
|
||||
{
|
||||
'files'; [{
|
||||
src: '/absolute/path/to/copy/from',
|
||||
dest: 'relative/path/to/copy/to',
|
||||
checksum: 'b54ba7f5621240d403f06815f7246006ef8c7d43'
|
||||
}, ...],
|
||||
'directories'; [{
|
||||
src: '/absolute/path/to/copy/from',
|
||||
dest: 'relative/path/to/copy/to'
|
||||
}, ...],
|
||||
'symlinks'; [{
|
||||
src: '/symlink/target/path',
|
||||
dest: 'relative/path/to/copy/to'
|
||||
}, ...],
|
||||
|
||||
}
|
||||
|
||||
The ``symlinks`` field is only populated if ``local_follow`` is set to False
|
||||
*or* a circular symlink cannot be dereferenced. The ``checksum`` entry is set
|
||||
to None if checksum_check=False.
|
||||
|
||||
"""
|
||||
# Convert the path segments into byte strings
|
||||
|
||||
r_files = {'files': [], 'directories': [], 'symlinks': []}
|
||||
|
||||
def _recurse(topdir, rel_offset, parent_dirs, rel_base=u'', checksum_check=False):
|
||||
"""
|
||||
This is a closure (function utilizing variables from it's parent
|
||||
function's scope) so that we only need one copy of all the containers.
|
||||
Note that this function uses side effects (See the Variables used from
|
||||
outer scope).
|
||||
|
||||
:arg topdir: The directory we are walking for files
|
||||
:arg rel_offset: Integer defining how many characters to strip off of
|
||||
the beginning of a path
|
||||
:arg parent_dirs: Directories that we're copying that this directory is in.
|
||||
:kwarg rel_base: String to prepend to the path after ``rel_offset`` is
|
||||
applied to form the relative path.
|
||||
|
||||
Variables used from the outer scope
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
:r_files: Dictionary of files in the hierarchy. See the return value
|
||||
for :func:`walk` for the structure of this dictionary.
|
||||
:local_follow: Read-only inside of :func:`_recurse`. Whether to follow symlinks
|
||||
"""
|
||||
for base_path, sub_folders, files in os.walk(topdir):
|
||||
for filename in files:
|
||||
filepath = os.path.join(base_path, filename)
|
||||
dest_filepath = os.path.join(rel_base, filepath[rel_offset:])
|
||||
|
||||
if os.path.islink(filepath):
|
||||
# Dereference the symlnk
|
||||
real_file = loader.get_real_file(os.path.realpath(filepath), decrypt=decrypt)
|
||||
if local_follow and os.path.isfile(real_file):
|
||||
# Add the file pointed to by the symlink
|
||||
r_files['files'].append(
|
||||
{
|
||||
"src": real_file,
|
||||
"dest": dest_filepath,
|
||||
"checksum": _get_local_checksum(checksum_check, real_file)
|
||||
}
|
||||
)
|
||||
else:
|
||||
# Mark this file as a symlink to copy
|
||||
r_files['symlinks'].append({"src": os.readlink(filepath), "dest": dest_filepath})
|
||||
else:
|
||||
# Just a normal file
|
||||
real_file = loader.get_real_file(filepath, decrypt=decrypt)
|
||||
r_files['files'].append(
|
||||
{
|
||||
"src": real_file,
|
||||
"dest": dest_filepath,
|
||||
"checksum": _get_local_checksum(checksum_check, real_file)
|
||||
}
|
||||
)
|
||||
|
||||
for dirname in sub_folders:
|
||||
dirpath = os.path.join(base_path, dirname)
|
||||
dest_dirpath = os.path.join(rel_base, dirpath[rel_offset:])
|
||||
real_dir = os.path.realpath(dirpath)
|
||||
dir_stats = os.stat(real_dir)
|
||||
|
||||
if os.path.islink(dirpath):
|
||||
if local_follow:
|
||||
if (dir_stats.st_dev, dir_stats.st_ino) in parent_dirs:
|
||||
# Just insert the symlink if the target directory
|
||||
# exists inside of the copy already
|
||||
r_files['symlinks'].append({"src": os.readlink(dirpath), "dest": dest_dirpath})
|
||||
else:
|
||||
# Walk the dirpath to find all parent directories.
|
||||
new_parents = set()
|
||||
parent_dir_list = os.path.dirname(dirpath).split(os.path.sep)
|
||||
for parent in range(len(parent_dir_list), 0, -1):
|
||||
parent_stat = os.stat(u'/'.join(parent_dir_list[:parent]))
|
||||
if (parent_stat.st_dev, parent_stat.st_ino) in parent_dirs:
|
||||
# Reached the point at which the directory
|
||||
# tree is already known. Don't add any
|
||||
# more or we might go to an ancestor that
|
||||
# isn't being copied.
|
||||
break
|
||||
new_parents.add((parent_stat.st_dev, parent_stat.st_ino))
|
||||
|
||||
if (dir_stats.st_dev, dir_stats.st_ino) in new_parents:
|
||||
# This was a a circular symlink. So add it as
|
||||
# a symlink
|
||||
r_files['symlinks'].append({"src": os.readlink(dirpath), "dest": dest_dirpath})
|
||||
else:
|
||||
# Walk the directory pointed to by the symlink
|
||||
r_files['directories'].append({"src": real_dir, "dest": dest_dirpath})
|
||||
offset = len(real_dir) + 1
|
||||
_recurse(real_dir, offset, parent_dirs.union(new_parents),
|
||||
rel_base=dest_dirpath,
|
||||
checksum_check=checksum_check)
|
||||
else:
|
||||
# Add the symlink to the destination
|
||||
r_files['symlinks'].append({"src": os.readlink(dirpath), "dest": dest_dirpath})
|
||||
else:
|
||||
# Just a normal directory
|
||||
r_files['directories'].append({"src": dirpath, "dest": dest_dirpath})
|
||||
|
||||
# Check if the source ends with a "/" so that we know which directory
|
||||
# level to work at (similar to rsync)
|
||||
source_trailing_slash = False
|
||||
if trailing_slash_detector:
|
||||
source_trailing_slash = trailing_slash_detector(topdir)
|
||||
else:
|
||||
source_trailing_slash = topdir.endswith(os.path.sep)
|
||||
|
||||
# Calculate the offset needed to strip the base_path to make relative
|
||||
# paths
|
||||
if base_path is None:
|
||||
base_path = topdir
|
||||
if not source_trailing_slash:
|
||||
base_path = os.path.dirname(base_path)
|
||||
if topdir.startswith(base_path):
|
||||
offset = len(base_path)
|
||||
|
||||
# Make sure we're making the new paths relative
|
||||
if trailing_slash_detector and not trailing_slash_detector(base_path):
|
||||
offset += 1
|
||||
elif not base_path.endswith(os.path.sep):
|
||||
offset += 1
|
||||
|
||||
if os.path.islink(topdir) and not local_follow:
|
||||
r_files['symlinks'] = {"src": os.readlink(topdir), "dest": os.path.basename(topdir)}
|
||||
return r_files
|
||||
|
||||
dir_stats = os.stat(topdir)
|
||||
parents = frozenset(((dir_stats.st_dev, dir_stats.st_ino),))
|
||||
# Actually walk the directory hierarchy
|
||||
_recurse(topdir, offset, parents, checksum_check=checksum_check)
|
||||
|
||||
return r_files
|
||||
|
||||
|
||||
def _get_local_checksum(get_checksum, local_path):
|
||||
if get_checksum:
|
||||
return checksum(local_path)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
|
||||
WIN_PATH_SEPARATOR = "\\"
|
||||
|
||||
def _create_content_tempfile(self, content):
|
||||
''' Create a tempfile containing defined content '''
|
||||
fd, content_tempfile = tempfile.mkstemp(dir=C.DEFAULT_LOCAL_TMP)
|
||||
f = os.fdopen(fd, 'wb')
|
||||
content = to_bytes(content)
|
||||
try:
|
||||
f.write(content)
|
||||
except Exception as err:
|
||||
os.remove(content_tempfile)
|
||||
raise Exception(err)
|
||||
finally:
|
||||
f.close()
|
||||
return content_tempfile
|
||||
|
||||
def _create_zip_tempfile(self, files, directories):
|
||||
tmpdir = tempfile.mkdtemp(dir=C.DEFAULT_LOCAL_TMP)
|
||||
zip_file_path = os.path.join(tmpdir, "win_copy.zip")
|
||||
zip_file = zipfile.ZipFile(zip_file_path, "w", zipfile.ZIP_STORED, True)
|
||||
|
||||
# encoding the file/dir name with base64 so Windows can unzip a unicode
|
||||
# filename and get the right name, Windows doesn't handle unicode names
|
||||
# very well
|
||||
for directory in directories:
|
||||
directory_path = to_bytes(directory['src'], errors='surrogate_or_strict')
|
||||
archive_path = to_bytes(directory['dest'], errors='surrogate_or_strict')
|
||||
|
||||
encoded_path = to_text(base64.b64encode(archive_path), errors='surrogate_or_strict')
|
||||
zip_file.write(directory_path, encoded_path, zipfile.ZIP_DEFLATED)
|
||||
|
||||
for file in files:
|
||||
file_path = to_bytes(file['src'], errors='surrogate_or_strict')
|
||||
archive_path = to_bytes(file['dest'], errors='surrogate_or_strict')
|
||||
|
||||
encoded_path = to_text(base64.b64encode(archive_path), errors='surrogate_or_strict')
|
||||
zip_file.write(file_path, encoded_path, zipfile.ZIP_DEFLATED)
|
||||
|
||||
return zip_file_path
|
||||
|
||||
def _remove_tempfile_if_content_defined(self, content, content_tempfile):
|
||||
if content is not None:
|
||||
os.remove(content_tempfile)
|
||||
|
||||
def _copy_single_file(self, local_file, dest, source_rel, task_vars, tmp, backup):
|
||||
if self._play_context.check_mode:
|
||||
module_return = dict(changed=True)
|
||||
return module_return
|
||||
|
||||
# copy the file across to the server
|
||||
tmp_src = self._connection._shell.join_path(tmp, 'source')
|
||||
self._transfer_file(local_file, tmp_src)
|
||||
|
||||
copy_args = self._task.args.copy()
|
||||
copy_args.update(
|
||||
dict(
|
||||
dest=dest,
|
||||
src=tmp_src,
|
||||
_original_basename=source_rel,
|
||||
_copy_mode="single",
|
||||
backup=backup,
|
||||
)
|
||||
)
|
||||
copy_args.pop('content', None)
|
||||
|
||||
copy_result = self._execute_module(module_name="copy",
|
||||
module_args=copy_args,
|
||||
task_vars=task_vars)
|
||||
|
||||
return copy_result
|
||||
|
||||
def _copy_zip_file(self, dest, files, directories, task_vars, tmp, backup):
|
||||
# create local zip file containing all the files and directories that
|
||||
# need to be copied to the server
|
||||
if self._play_context.check_mode:
|
||||
module_return = dict(changed=True)
|
||||
return module_return
|
||||
|
||||
try:
|
||||
zip_file = self._create_zip_tempfile(files, directories)
|
||||
except Exception as e:
|
||||
module_return = dict(
|
||||
changed=False,
|
||||
failed=True,
|
||||
msg="failed to create tmp zip file: %s" % to_text(e),
|
||||
exception=traceback.format_exc()
|
||||
)
|
||||
return module_return
|
||||
|
||||
zip_path = self._loader.get_real_file(zip_file)
|
||||
|
||||
# send zip file to remote, file must end in .zip so
|
||||
# Com Shell.Application works
|
||||
tmp_src = self._connection._shell.join_path(tmp, 'source.zip')
|
||||
self._transfer_file(zip_path, tmp_src)
|
||||
|
||||
# run the explode operation of win_copy on remote
|
||||
copy_args = self._task.args.copy()
|
||||
copy_args.update(
|
||||
dict(
|
||||
src=tmp_src,
|
||||
dest=dest,
|
||||
_copy_mode="explode",
|
||||
backup=backup,
|
||||
)
|
||||
)
|
||||
copy_args.pop('content', None)
|
||||
module_return = self._execute_module(module_name='copy',
|
||||
module_args=copy_args,
|
||||
task_vars=task_vars)
|
||||
shutil.rmtree(os.path.dirname(zip_path))
|
||||
return module_return
|
||||
|
||||
def run(self, tmp=None, task_vars=None):
|
||||
''' handler for file transfer operations '''
|
||||
if task_vars is None:
|
||||
task_vars = dict()
|
||||
|
||||
result = super(ActionModule, self).run(tmp, task_vars)
|
||||
del tmp # tmp no longer has any effect
|
||||
|
||||
source = self._task.args.get('src', None)
|
||||
content = self._task.args.get('content', None)
|
||||
dest = self._task.args.get('dest', None)
|
||||
remote_src = boolean(self._task.args.get('remote_src', False), strict=False)
|
||||
local_follow = boolean(self._task.args.get('local_follow', False), strict=False)
|
||||
force = boolean(self._task.args.get('force', True), strict=False)
|
||||
decrypt = boolean(self._task.args.get('decrypt', True), strict=False)
|
||||
backup = boolean(self._task.args.get('backup', False), strict=False)
|
||||
|
||||
result['src'] = source
|
||||
result['dest'] = dest
|
||||
|
||||
result['failed'] = True
|
||||
if (source is None and content is None) or dest is None:
|
||||
result['msg'] = "src (or content) and dest are required"
|
||||
elif source is not None and content is not None:
|
||||
result['msg'] = "src and content are mutually exclusive"
|
||||
elif content is not None and dest is not None and (
|
||||
dest.endswith(os.path.sep) or dest.endswith(self.WIN_PATH_SEPARATOR)):
|
||||
result['msg'] = "dest must be a file if content is defined"
|
||||
else:
|
||||
del result['failed']
|
||||
|
||||
if result.get('failed'):
|
||||
return result
|
||||
|
||||
# If content is defined make a temp file and write the content into it
|
||||
content_tempfile = None
|
||||
if content is not None:
|
||||
try:
|
||||
# if content comes to us as a dict it should be decoded json.
|
||||
# We need to encode it back into a string and write it out
|
||||
if isinstance(content, dict) or isinstance(content, list):
|
||||
content_tempfile = self._create_content_tempfile(json.dumps(content))
|
||||
else:
|
||||
content_tempfile = self._create_content_tempfile(content)
|
||||
source = content_tempfile
|
||||
except Exception as err:
|
||||
result['failed'] = True
|
||||
result['msg'] = "could not write content tmp file: %s" % to_native(err)
|
||||
return result
|
||||
# all actions should occur on the remote server, run win_copy module
|
||||
elif remote_src:
|
||||
new_module_args = self._task.args.copy()
|
||||
new_module_args.update(
|
||||
dict(
|
||||
_copy_mode="remote",
|
||||
dest=dest,
|
||||
src=source,
|
||||
force=force,
|
||||
backup=backup,
|
||||
)
|
||||
)
|
||||
new_module_args.pop('content', None)
|
||||
result.update(self._execute_module(module_args=new_module_args, task_vars=task_vars))
|
||||
return result
|
||||
# find_needle returns a path that may not have a trailing slash on a
|
||||
# directory so we need to find that out first and append at the end
|
||||
else:
|
||||
trailing_slash = source.endswith(os.path.sep)
|
||||
try:
|
||||
# find in expected paths
|
||||
source = self._find_needle('files', source)
|
||||
except AnsibleError as e:
|
||||
result['failed'] = True
|
||||
result['msg'] = to_text(e)
|
||||
result['exception'] = traceback.format_exc()
|
||||
return result
|
||||
|
||||
if trailing_slash != source.endswith(os.path.sep):
|
||||
if source[-1] == os.path.sep:
|
||||
source = source[:-1]
|
||||
else:
|
||||
source = source + os.path.sep
|
||||
|
||||
# A list of source file tuples (full_path, relative_path) which will try to copy to the destination
|
||||
source_files = {'files': [], 'directories': [], 'symlinks': []}
|
||||
|
||||
# If source is a directory populate our list else source is a file and translate it to a tuple.
|
||||
if os.path.isdir(to_bytes(source, errors='surrogate_or_strict')):
|
||||
result['operation'] = 'folder_copy'
|
||||
|
||||
# Get a list of the files we want to replicate on the remote side
|
||||
source_files = _walk_dirs(source, self._loader, decrypt=decrypt, local_follow=local_follow,
|
||||
trailing_slash_detector=self._connection._shell.path_has_trailing_slash,
|
||||
checksum_check=force)
|
||||
|
||||
# If it's recursive copy, destination is always a dir,
|
||||
# explicitly mark it so (note - win_copy module relies on this).
|
||||
if not self._connection._shell.path_has_trailing_slash(dest):
|
||||
dest = "%s%s" % (dest, self.WIN_PATH_SEPARATOR)
|
||||
|
||||
check_dest = dest
|
||||
# Source is a file, add details to source_files dict
|
||||
else:
|
||||
result['operation'] = 'file_copy'
|
||||
|
||||
# If the local file does not exist, get_real_file() raises AnsibleFileNotFound
|
||||
try:
|
||||
source_full = self._loader.get_real_file(source, decrypt=decrypt)
|
||||
except AnsibleFileNotFound as e:
|
||||
result['failed'] = True
|
||||
result['msg'] = "could not find src=%s, %s" % (source_full, to_text(e))
|
||||
return result
|
||||
|
||||
original_basename = os.path.basename(source)
|
||||
result['original_basename'] = original_basename
|
||||
|
||||
# check if dest ends with / or \ and append source filename to dest
|
||||
if self._connection._shell.path_has_trailing_slash(dest):
|
||||
check_dest = dest
|
||||
filename = original_basename
|
||||
result['dest'] = self._connection._shell.join_path(dest, filename)
|
||||
else:
|
||||
# replace \\ with / so we can use os.path to get the filename or dirname
|
||||
unix_path = dest.replace(self.WIN_PATH_SEPARATOR, os.path.sep)
|
||||
filename = os.path.basename(unix_path)
|
||||
check_dest = os.path.dirname(unix_path)
|
||||
|
||||
file_checksum = _get_local_checksum(force, source_full)
|
||||
source_files['files'].append(
|
||||
dict(
|
||||
src=source_full,
|
||||
dest=filename,
|
||||
checksum=file_checksum
|
||||
)
|
||||
)
|
||||
result['checksum'] = file_checksum
|
||||
result['size'] = os.path.getsize(to_bytes(source_full, errors='surrogate_or_strict'))
|
||||
|
||||
# find out the files/directories/symlinks that we need to copy to the server
|
||||
query_args = self._task.args.copy()
|
||||
query_args.update(
|
||||
dict(
|
||||
_copy_mode="query",
|
||||
dest=check_dest,
|
||||
force=force,
|
||||
files=source_files['files'],
|
||||
directories=source_files['directories'],
|
||||
symlinks=source_files['symlinks'],
|
||||
)
|
||||
)
|
||||
# src is not required for query, will fail path validation is src has unix allowed chars
|
||||
query_args.pop('src', None)
|
||||
|
||||
query_args.pop('content', None)
|
||||
query_return = self._execute_module(module_args=query_args,
|
||||
task_vars=task_vars)
|
||||
|
||||
if query_return.get('failed') is True:
|
||||
result.update(query_return)
|
||||
return result
|
||||
|
||||
if len(query_return['files']) > 0 or len(query_return['directories']) > 0 and self._connection._shell.tmpdir is None:
|
||||
self._connection._shell.tmpdir = self._make_tmp_path()
|
||||
|
||||
if len(query_return['files']) == 1 and len(query_return['directories']) == 0:
|
||||
# we only need to copy 1 file, don't mess around with zips
|
||||
file_src = query_return['files'][0]['src']
|
||||
file_dest = query_return['files'][0]['dest']
|
||||
result.update(self._copy_single_file(file_src, dest, file_dest,
|
||||
task_vars, self._connection._shell.tmpdir, backup))
|
||||
if result.get('failed') is True:
|
||||
result['msg'] = "failed to copy file %s: %s" % (file_src, result['msg'])
|
||||
result['changed'] = True
|
||||
|
||||
elif len(query_return['files']) > 0 or len(query_return['directories']) > 0:
|
||||
# either multiple files or directories need to be copied, compress
|
||||
# to a zip and 'explode' the zip on the server
|
||||
# TODO: handle symlinks
|
||||
result.update(self._copy_zip_file(dest, source_files['files'],
|
||||
source_files['directories'],
|
||||
task_vars, self._connection._shell.tmpdir, backup))
|
||||
result['changed'] = True
|
||||
else:
|
||||
# no operations need to occur
|
||||
result['failed'] = False
|
||||
result['changed'] = False
|
||||
|
||||
# remove the content tmp file and remote tmp file if it was created
|
||||
self._remove_tempfile_if_content_defined(content, content_tempfile)
|
||||
self._remove_tmp_path(self._connection._shell.tmpdir)
|
||||
return result
|
||||
@ -1,96 +0,0 @@
|
||||
# Copyright: (c) 2018, Matt Davis <mdavis@ansible.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
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.module_utils._text import to_native
|
||||
from ansible.plugins.action import ActionBase
|
||||
from ansible.plugins.action.reboot import ActionModule as RebootActionModule
|
||||
from ansible.utils.display import Display
|
||||
|
||||
display = Display()
|
||||
|
||||
|
||||
class TimedOutException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ActionModule(RebootActionModule, ActionBase):
|
||||
TRANSFERS_FILES = False
|
||||
_VALID_ARGS = frozenset((
|
||||
'connect_timeout', 'connect_timeout_sec', 'msg', 'post_reboot_delay', 'post_reboot_delay_sec', 'pre_reboot_delay', 'pre_reboot_delay_sec',
|
||||
'reboot_timeout', 'reboot_timeout_sec', 'shutdown_timeout', 'shutdown_timeout_sec', 'test_command',
|
||||
))
|
||||
|
||||
DEFAULT_BOOT_TIME_COMMAND = "(Get-WmiObject -ClassName Win32_OperatingSystem).LastBootUpTime"
|
||||
DEFAULT_CONNECT_TIMEOUT = 5
|
||||
DEFAULT_PRE_REBOOT_DELAY = 2
|
||||
DEFAULT_SUDOABLE = False
|
||||
DEFAULT_SHUTDOWN_COMMAND_ARGS = '/r /t {delay_sec} /c "{message}"'
|
||||
|
||||
DEPRECATED_ARGS = {
|
||||
'shutdown_timeout': '2.5',
|
||||
'shutdown_timeout_sec': '2.5',
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ActionModule, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_distribution(self, task_vars):
|
||||
return {'name': 'windows', 'version': '', 'family': ''}
|
||||
|
||||
def get_shutdown_command(self, task_vars, distribution):
|
||||
return self.DEFAULT_SHUTDOWN_COMMAND
|
||||
|
||||
def run_test_command(self, distribution, **kwargs):
|
||||
# Need to wrap the test_command in our PowerShell encoded wrapper. This is done to align the command input to a
|
||||
# common shell and to allow the psrp connection plugin to report the correct exit code without manually setting
|
||||
# $LASTEXITCODE for just that plugin.
|
||||
test_command = self._task.args.get('test_command', self.DEFAULT_TEST_COMMAND)
|
||||
kwargs['test_command'] = self._connection._shell._encode_script(test_command)
|
||||
super(ActionModule, self).run_test_command(distribution, **kwargs)
|
||||
|
||||
def perform_reboot(self, task_vars, distribution):
|
||||
shutdown_command = self.get_shutdown_command(task_vars, distribution)
|
||||
shutdown_command_args = self.get_shutdown_command_args(distribution)
|
||||
reboot_command = self._connection._shell._encode_script('{0} {1}'.format(shutdown_command, shutdown_command_args))
|
||||
|
||||
display.vvv("{action}: rebooting server...".format(action=self._task.action))
|
||||
display.debug("{action}: distribution: {dist}".format(action=self._task.action, dist=distribution))
|
||||
display.debug("{action}: rebooting server with command '{command}'".format(action=self._task.action, command=reboot_command))
|
||||
|
||||
result = {}
|
||||
reboot_result = self._low_level_execute_command(reboot_command, sudoable=self.DEFAULT_SUDOABLE)
|
||||
result['start'] = datetime.utcnow()
|
||||
|
||||
# Test for "A system shutdown has already been scheduled. (1190)" and handle it gracefully
|
||||
stdout = reboot_result['stdout']
|
||||
stderr = reboot_result['stderr']
|
||||
if reboot_result['rc'] == 1190 or (reboot_result['rc'] != 0 and "(1190)" in reboot_result['stderr']):
|
||||
display.warning('A scheduled reboot was pre-empted by Ansible.')
|
||||
|
||||
# Try to abort (this may fail if it was already aborted)
|
||||
result1 = self._low_level_execute_command(self._connection._shell._encode_script('shutdown /a'),
|
||||
sudoable=self.DEFAULT_SUDOABLE)
|
||||
|
||||
# Initiate reboot again
|
||||
result2 = self._low_level_execute_command(reboot_command, sudoable=self.DEFAULT_SUDOABLE)
|
||||
|
||||
reboot_result['rc'] = result2['rc']
|
||||
stdout += result1['stdout'] + result2['stdout']
|
||||
stderr += result1['stderr'] + result2['stderr']
|
||||
|
||||
if reboot_result['rc'] != 0:
|
||||
result['failed'] = True
|
||||
result['rebooted'] = False
|
||||
result['msg'] = "Reboot command failed, error was: {stdout} {stderr}".format(
|
||||
stdout=to_native(stdout.strip()),
|
||||
stderr=to_native(stderr.strip()))
|
||||
return result
|
||||
|
||||
result['failed'] = False
|
||||
return result
|
||||
@ -1,29 +0,0 @@
|
||||
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Make coding more python3-ish
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from ansible.plugins.action import ActionBase
|
||||
from ansible.plugins.action.template import ActionModule as TemplateActionModule
|
||||
|
||||
|
||||
# Even though TemplateActionModule inherits from ActionBase, we still need to
|
||||
# directly inherit from ActionBase to appease the plugin loader.
|
||||
class ActionModule(TemplateActionModule, ActionBase):
|
||||
DEFAULT_NEWLINE_SEQUENCE = '\r\n'
|
||||
@ -1,247 +0,0 @@
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import json
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.module_utils._text import to_text
|
||||
from ansible.module_utils.parsing.convert_bool import boolean
|
||||
from ansible.parsing.yaml.objects import AnsibleUnicode
|
||||
from ansible.plugins.action import ActionBase
|
||||
from ansible.plugins.loader import become_loader
|
||||
from ansible.utils.display import Display
|
||||
|
||||
display = Display()
|
||||
|
||||
|
||||
class ActionModule(ActionBase):
|
||||
|
||||
DEFAULT_REBOOT_TIMEOUT = 1200
|
||||
|
||||
def _run_win_updates(self, module_args, task_vars, use_task):
|
||||
display.vvv("win_updates: running win_updates module")
|
||||
wrap_async = self._task.async_val
|
||||
result = self._execute_module_with_become(module_name='win_updates',
|
||||
module_args=module_args,
|
||||
task_vars=task_vars,
|
||||
wrap_async=wrap_async,
|
||||
use_task=use_task)
|
||||
return result
|
||||
|
||||
def _reboot_server(self, task_vars, reboot_timeout, use_task):
|
||||
display.vvv("win_updates: rebooting remote host after update install")
|
||||
reboot_args = {
|
||||
'reboot_timeout': reboot_timeout
|
||||
}
|
||||
reboot_result = self._run_action_plugin('win_reboot', task_vars,
|
||||
module_args=reboot_args)
|
||||
if reboot_result.get('failed', False):
|
||||
raise AnsibleError(reboot_result['msg'])
|
||||
|
||||
# only run this if the user has specified we can only use scheduled
|
||||
# tasks, the win_shell command requires become and will be skipped if
|
||||
# become isn't available to use
|
||||
if use_task:
|
||||
display.vvv("win_updates: skipping WUA is not busy check as "
|
||||
"use_scheduled_task=True is set")
|
||||
else:
|
||||
display.vvv("win_updates: checking WUA is not busy with win_shell "
|
||||
"command")
|
||||
# While this always returns False after a reboot it doesn't return
|
||||
# a value until Windows is actually ready and finished installing
|
||||
# updates. This needs to run with become as WUA doesn't work over
|
||||
# WinRM, ignore connection errors as another reboot can happen
|
||||
command = "(New-Object -ComObject Microsoft.Update.Session)." \
|
||||
"CreateUpdateInstaller().IsBusy"
|
||||
shell_module_args = {
|
||||
'_raw_params': command
|
||||
}
|
||||
|
||||
try:
|
||||
shell_result = self._execute_module_with_become(
|
||||
module_name='win_shell', module_args=shell_module_args,
|
||||
task_vars=task_vars, wrap_async=False, use_task=use_task
|
||||
)
|
||||
display.vvv("win_updates: shell wait results: %s"
|
||||
% json.dumps(shell_result))
|
||||
except Exception as exc:
|
||||
display.debug("win_updates: Fatal error when running shell "
|
||||
"command, attempting to recover: %s" % to_text(exc))
|
||||
|
||||
display.vvv("win_updates: ensure the connection is up and running")
|
||||
# in case Windows needs to reboot again after the updates, we wait for
|
||||
# the connection to be stable again
|
||||
wait_for_result = self._run_action_plugin('wait_for_connection',
|
||||
task_vars)
|
||||
if wait_for_result.get('failed', False):
|
||||
raise AnsibleError(wait_for_result['msg'])
|
||||
|
||||
def _run_action_plugin(self, plugin_name, task_vars, module_args=None):
|
||||
# Create new task object and reset the args
|
||||
new_task = self._task.copy()
|
||||
new_task.args = {}
|
||||
|
||||
if module_args is not None:
|
||||
for key, value in module_args.items():
|
||||
new_task.args[key] = value
|
||||
|
||||
# run the action plugin and return the results
|
||||
action = self._shared_loader_obj.action_loader.get(
|
||||
plugin_name,
|
||||
task=new_task,
|
||||
connection=self._connection,
|
||||
play_context=self._play_context,
|
||||
loader=self._loader,
|
||||
templar=self._templar,
|
||||
shared_loader_obj=self._shared_loader_obj
|
||||
)
|
||||
return action.run(task_vars=task_vars)
|
||||
|
||||
def _merge_dict(self, original, new):
|
||||
dict_var = original.copy()
|
||||
dict_var.update(new)
|
||||
return dict_var
|
||||
|
||||
def _execute_module_with_become(self, module_name, module_args, task_vars,
|
||||
wrap_async, use_task):
|
||||
orig_become = self._connection.become
|
||||
try:
|
||||
if not use_task and orig_become is None:
|
||||
become = become_loader.get('runas')
|
||||
become.set_options(direct={'become_user': 'SYSTEM', 'become_pass': None})
|
||||
self._connection.set_become_plugin(become)
|
||||
|
||||
module_res = self._execute_module(module_name=module_name,
|
||||
module_args=module_args,
|
||||
task_vars=task_vars,
|
||||
wrap_async=wrap_async)
|
||||
finally:
|
||||
self._connection.set_become_plugin(orig_become)
|
||||
|
||||
return module_res
|
||||
|
||||
def run(self, tmp=None, task_vars=None):
|
||||
self._supports_check_mode = True
|
||||
self._supports_async = True
|
||||
|
||||
result = super(ActionModule, self).run(tmp, task_vars)
|
||||
del tmp # tmp no longer has any effect
|
||||
|
||||
state = self._task.args.get('state', 'installed')
|
||||
reboot = self._task.args.get('reboot', False)
|
||||
reboot_timeout = self._task.args.get('reboot_timeout',
|
||||
self.DEFAULT_REBOOT_TIMEOUT)
|
||||
use_task = boolean(self._task.args.get('use_scheduled_task', False),
|
||||
strict=False)
|
||||
|
||||
if state not in ['installed', 'searched', 'downloaded']:
|
||||
result['failed'] = True
|
||||
result['msg'] = "state must be either installed, searched or downloaded"
|
||||
return result
|
||||
|
||||
try:
|
||||
reboot = boolean(reboot)
|
||||
except TypeError as exc:
|
||||
result['failed'] = True
|
||||
result['msg'] = "cannot parse reboot as a boolean: %s" % to_text(exc)
|
||||
return result
|
||||
|
||||
if not isinstance(reboot_timeout, int):
|
||||
result['failed'] = True
|
||||
result['msg'] = "reboot_timeout must be an integer"
|
||||
return result
|
||||
|
||||
if reboot and self._task.async_val > 0:
|
||||
result['failed'] = True
|
||||
result['msg'] = "async is not supported for this task when " \
|
||||
"reboot=yes"
|
||||
return result
|
||||
|
||||
# Run the module
|
||||
new_module_args = self._task.args.copy()
|
||||
new_module_args.pop('reboot', None)
|
||||
new_module_args.pop('reboot_timeout', None)
|
||||
result = self._run_win_updates(new_module_args, task_vars, use_task)
|
||||
|
||||
# if the module failed to run at all then changed won't be populated
|
||||
# so we just return the result as is
|
||||
# https://github.com/ansible/ansible/issues/38232
|
||||
failed = result.get('failed', False)
|
||||
if ("updates" not in result.keys() and self._task.async_val == 0) or failed:
|
||||
result['failed'] = True
|
||||
return result
|
||||
|
||||
changed = result.get('changed', False)
|
||||
updates = result.get('updates', dict())
|
||||
filtered_updates = result.get('filtered_updates', dict())
|
||||
found_update_count = result.get('found_update_count', 0)
|
||||
installed_update_count = result.get('installed_update_count', 0)
|
||||
|
||||
# Handle automatic reboots if the reboot flag is set
|
||||
if reboot and state == 'installed' and not \
|
||||
self._play_context.check_mode:
|
||||
previously_errored = False
|
||||
while result['installed_update_count'] > 0 or \
|
||||
result['found_update_count'] > 0 or \
|
||||
result['reboot_required'] is True:
|
||||
display.vvv("win_updates: check win_updates results for "
|
||||
"automatic reboot: %s" % json.dumps(result))
|
||||
|
||||
# check if the module failed, break from the loop if it
|
||||
# previously failed and return error to the user
|
||||
if result.get('failed', False):
|
||||
if previously_errored:
|
||||
break
|
||||
previously_errored = True
|
||||
else:
|
||||
previously_errored = False
|
||||
|
||||
reboot_error = None
|
||||
# check if a reboot was required before installing the updates
|
||||
if result.get('msg', '') == "A reboot is required before " \
|
||||
"more updates can be installed":
|
||||
reboot_error = "reboot was required before more updates " \
|
||||
"can be installed"
|
||||
|
||||
if result.get('reboot_required', False):
|
||||
if reboot_error is None:
|
||||
reboot_error = "reboot was required to finalise " \
|
||||
"update install"
|
||||
try:
|
||||
changed = True
|
||||
self._reboot_server(task_vars, reboot_timeout,
|
||||
use_task)
|
||||
except AnsibleError as exc:
|
||||
result['failed'] = True
|
||||
result['msg'] = "Failed to reboot remote host when " \
|
||||
"%s: %s" \
|
||||
% (reboot_error, to_text(exc))
|
||||
break
|
||||
|
||||
result.pop('msg', None)
|
||||
# rerun the win_updates module after the reboot is complete
|
||||
result = self._run_win_updates(new_module_args, task_vars,
|
||||
use_task)
|
||||
if result.get('failed', False):
|
||||
return result
|
||||
|
||||
result_updates = result.get('updates', dict())
|
||||
result_filtered_updates = result.get('filtered_updates', dict())
|
||||
updates = self._merge_dict(updates, result_updates)
|
||||
filtered_updates = self._merge_dict(filtered_updates,
|
||||
result_filtered_updates)
|
||||
found_update_count += result.get('found_update_count', 0)
|
||||
installed_update_count += result.get('installed_update_count', 0)
|
||||
if result['changed']:
|
||||
changed = True
|
||||
|
||||
# finally create the return dict based on the aggregated execution
|
||||
# values if we are not in async
|
||||
if self._task.async_val == 0:
|
||||
result['changed'] = changed
|
||||
result['updates'] = updates
|
||||
result['filtered_updates'] = filtered_updates
|
||||
result['found_update_count'] = found_update_count
|
||||
result['installed_update_count'] = installed_update_count
|
||||
|
||||
return result
|
||||
@ -1,154 +0,0 @@
|
||||
# -*- 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)
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class ModuleDocFragment(object):
|
||||
|
||||
# Standard files documentation fragment
|
||||
DOCUMENTATION = r'''
|
||||
options:
|
||||
method:
|
||||
description:
|
||||
- The HTTP Method of the request.
|
||||
type: str
|
||||
follow_redirects:
|
||||
description:
|
||||
- Whether or the module should follow redirects.
|
||||
- C(all) will follow all redirect.
|
||||
- C(none) will not follow any redirect.
|
||||
- C(safe) will follow only "safe" redirects, where "safe" means that the
|
||||
client is only doing a C(GET) or C(HEAD) on the URI to which it is being
|
||||
redirected.
|
||||
choices:
|
||||
- all
|
||||
- none
|
||||
- safe
|
||||
default: safe
|
||||
type: str
|
||||
headers:
|
||||
description:
|
||||
- Extra headers to set on the request.
|
||||
- This should be a dictionary where the key is the header name and the
|
||||
value is the value for that header.
|
||||
type: dict
|
||||
http_agent:
|
||||
description:
|
||||
- Header to identify as, generally appears in web server logs.
|
||||
- This is set to the C(User-Agent) header on a HTTP request.
|
||||
default: ansible-httpget
|
||||
type: str
|
||||
version_added: "2.9"
|
||||
maximum_redirection:
|
||||
description:
|
||||
- Specify how many times the module will redirect a connection to an
|
||||
alternative URI before the connection fails.
|
||||
- If set to C(0) or I(follow_redirects) is set to C(none), or C(safe) when
|
||||
not doing a C(GET) or C(HEAD) it prevents all redirection.
|
||||
default: 50
|
||||
type: int
|
||||
timeout:
|
||||
description:
|
||||
- Specifies how long the request can be pending before it times out (in
|
||||
seconds).
|
||||
- Set to C(0) to specify an infinite timeout.
|
||||
default: 30
|
||||
type: int
|
||||
version_added: "2.4"
|
||||
validate_certs:
|
||||
description:
|
||||
- If C(no), SSL certificates will not be validated.
|
||||
- This should only be used on personally controlled sites using self-signed
|
||||
certificates.
|
||||
default: yes
|
||||
type: bool
|
||||
version_added: "2.4"
|
||||
client_cert:
|
||||
description:
|
||||
- The path to the client certificate (.pfx) that is used for X509
|
||||
authentication. This path can either be the path to the C(pfx) on the
|
||||
filesystem or the PowerShell certificate path
|
||||
C(Cert:\CurrentUser\My\<thumbprint>).
|
||||
- The WinRM connection must be authenticated with C(CredSSP) or C(become)
|
||||
is used on the task if the certificate file is not password protected.
|
||||
- Other authentication types can set I(client_cert_password) when the cert
|
||||
is password protected.
|
||||
type: str
|
||||
client_cert_password:
|
||||
description:
|
||||
- The password for I(client_cert) if the cert is password protected.
|
||||
type: str
|
||||
force_basic_auth:
|
||||
description:
|
||||
- By default the authentication header is only sent when a webservice
|
||||
responses to an initial request with a 401 status. Since some basic auth
|
||||
services do not properly send a 401, logins will fail.
|
||||
- This option forces the sending of the Basic authentication header upon
|
||||
the original request.
|
||||
default: no
|
||||
type: bool
|
||||
version_added: "2.5"
|
||||
url_username:
|
||||
description:
|
||||
- The username to use for authentication.
|
||||
type: str
|
||||
url_password:
|
||||
description:
|
||||
- The password for I(url_username).
|
||||
type: str
|
||||
use_default_credential:
|
||||
description:
|
||||
- Uses the current user's credentials when authenticating with a server
|
||||
protected with C(NTLM), C(Kerberos), or C(Negotiate) authentication.
|
||||
- Sites that use C(Basic) auth will still require explicit credentials
|
||||
through the I(url_username) and I(url_password) options.
|
||||
- The module will only have access to the user's credentials if using
|
||||
C(become) with a password, you are connecting with SSH using a password,
|
||||
or connecting with WinRM using C(CredSSP) or C(Kerberos with delegation).
|
||||
- If not using C(become) or a different auth method to the ones stated
|
||||
above, there will be no default credentials available and no
|
||||
authentication will occur.
|
||||
default: no
|
||||
type: bool
|
||||
version_added: "2.9"
|
||||
use_proxy:
|
||||
description:
|
||||
- If C(no), it will not use the proxy defined in IE for the current user.
|
||||
default: yes
|
||||
type: bool
|
||||
proxy_url:
|
||||
description:
|
||||
- An explicit proxy to use for the request.
|
||||
- By default, the request will use the IE defined proxy unless I(use_proxy)
|
||||
is set to C(no).
|
||||
type: str
|
||||
proxy_username:
|
||||
description:
|
||||
- The username to use for proxy authentication.
|
||||
type: str
|
||||
proxy_password:
|
||||
description:
|
||||
- The password for I(proxy_username).
|
||||
type: str
|
||||
proxy_use_default_credential:
|
||||
description:
|
||||
- Uses the current user's credentials when authenticating with a proxy host
|
||||
protected with C(NTLM), C(Kerberos), or C(Negotiate) authentication.
|
||||
- Proxies that use C(Basic) auth will still require explicit credentials
|
||||
through the I(proxy_username) and I(proxy_password) options.
|
||||
- The module will only have access to the user's credentials if using
|
||||
C(become) with a password, you are connecting with SSH using a password,
|
||||
or connecting with WinRM using C(CredSSP) or C(Kerberos with delegation).
|
||||
- If not using C(become) or a different auth method to the ones stated
|
||||
above, there will be no default credentials available and no proxy
|
||||
authentication will occur.
|
||||
default: no
|
||||
type: bool
|
||||
version_added: "2.9"
|
||||
seealso:
|
||||
- module: win_inet_proxy
|
||||
'''
|
||||
@ -1 +0,0 @@
|
||||
shippable/windows/group3
|
||||
@ -1,5 +0,0 @@
|
||||
---
|
||||
test_acl_path: '{{ win_output_dir }}\win_acl .ÅÑŚÌβŁÈ [$!@^&test(;)]'
|
||||
test_acl_network_path: \\localhost\{{ test_acl_path[0:1] }}$\{{ test_acl_path[3:] }}
|
||||
# Use HKU as that path is not automatically loaded in the PSProvider making our test more complex
|
||||
test_acl_reg_path: HKU:\.DEFAULT\Ansible Test .ÅÑŚÌβŁÈ [$!@^&test(;)]
|
||||
@ -1,33 +0,0 @@
|
||||
---
|
||||
- name: ensure we start with a clean dir
|
||||
win_file:
|
||||
path: '{{ test_acl_path }}'
|
||||
state: '{{ item }}'
|
||||
with_items:
|
||||
- absent
|
||||
- directory
|
||||
|
||||
- name: ensure we start with a clean reg path
|
||||
win_regedit:
|
||||
path: '{{ test_acl_reg_path }}'
|
||||
delete_key: yes
|
||||
state: '{{ item }}'
|
||||
with_items:
|
||||
- absent
|
||||
- present
|
||||
|
||||
- block:
|
||||
- name: run tests
|
||||
include_tasks: tests.yml
|
||||
|
||||
always:
|
||||
- name: cleanup testing dir
|
||||
win_file:
|
||||
path: '{{ test_acl_path }}'
|
||||
state: absent
|
||||
|
||||
- name: cleanup testing reg path
|
||||
win_regedit:
|
||||
path: '{{ test_acl_reg_path }}'
|
||||
delete_key: yes
|
||||
state: absent
|
||||
@ -1,362 +0,0 @@
|
||||
# these are very basic tests, they should be expanded greatly as this is a core module
|
||||
---
|
||||
- name: get register cmd that will get ace info
|
||||
set_fact:
|
||||
test_ace_cmd: |
|
||||
# Overcome bug in Set-Acl/Get-Acl for registry paths and -LiteralPath
|
||||
New-PSDrive -Name HKU -PSProvider Registry -Root HKEY_USERS > $null
|
||||
Push-Location -LiteralPath (Split-Path -Path $path -Qualifier)
|
||||
$rights_key = if ((Get-Item -LiteralPath $path -Force).PSProvider.Name -eq "Registry") {
|
||||
"RegistryRights"
|
||||
} else {
|
||||
"FileSystemRights"
|
||||
}
|
||||
$ace_list = (Get-Acl -LiteralPath $path).Access | Where-Object { $_.IsInherited -eq $false } | ForEach-Object {
|
||||
@{
|
||||
rights = $_."$rights_key".ToString()
|
||||
type = $_.AccessControlType.ToString()
|
||||
identity = $_.IdentityReference.Value.ToString()
|
||||
inheritance_flags = $_.InheritanceFlags.ToString()
|
||||
propagation_flags = $_.PropagationFlags.ToString()
|
||||
}
|
||||
}
|
||||
Pop-Location
|
||||
ConvertTo-Json -InputObject @($ace_list)
|
||||
|
||||
- name: add write rights to Guest
|
||||
win_acl:
|
||||
path: '{{ test_acl_path }}'
|
||||
type: allow
|
||||
user: Guests
|
||||
rights: Write
|
||||
register: allow_right
|
||||
|
||||
- name: get result of add write rights to Guest
|
||||
win_shell: '$path = ''{{ test_acl_path }}''; {{ test_ace_cmd }}'
|
||||
register: allow_right_actual
|
||||
|
||||
- name: assert add write rights to Guest
|
||||
assert:
|
||||
that:
|
||||
- allow_right is changed
|
||||
- (allow_right_actual.stdout|from_json)|count == 1
|
||||
- (allow_right_actual.stdout|from_json)[0].identity == 'BUILTIN\Guests'
|
||||
- (allow_right_actual.stdout|from_json)[0].inheritance_flags == 'ContainerInherit, ObjectInherit'
|
||||
- (allow_right_actual.stdout|from_json)[0].propagation_flags == 'None'
|
||||
- (allow_right_actual.stdout|from_json)[0].rights == 'Write, Synchronize'
|
||||
- (allow_right_actual.stdout|from_json)[0].type == 'Allow'
|
||||
|
||||
- name: add write rights to Guest (idempotent)
|
||||
win_acl:
|
||||
path: '{{ test_acl_path }}'
|
||||
type: allow
|
||||
user: Guests
|
||||
rights: Write
|
||||
register: allow_right_again
|
||||
|
||||
- name: assert add write rights to Guest (idempotent)
|
||||
assert:
|
||||
that:
|
||||
- not allow_right_again is changed
|
||||
|
||||
- name: remove write rights from Guest
|
||||
win_acl:
|
||||
path: '{{ test_acl_path }}'
|
||||
type: allow
|
||||
user: Guests
|
||||
rights: Write
|
||||
state: absent
|
||||
register: remove_right
|
||||
|
||||
- name: get result of remove write rights from Guest
|
||||
win_shell: '$path = ''{{ test_acl_path }}''; {{ test_ace_cmd }}'
|
||||
register: remove_right_actual
|
||||
|
||||
- name: assert remove write rights from Guest
|
||||
assert:
|
||||
that:
|
||||
- remove_right is changed
|
||||
- remove_right_actual.stdout_lines == ["[", "", "]"]
|
||||
|
||||
- name: remove write rights from Guest (idempotent)
|
||||
win_acl:
|
||||
path: '{{ test_acl_path }}'
|
||||
type: allow
|
||||
user: Guests
|
||||
rights: Write
|
||||
state: absent
|
||||
register: remove_right_again
|
||||
|
||||
- name: assert remote write rights from Guest (idempotent)
|
||||
assert:
|
||||
that:
|
||||
- not remove_right_again is changed
|
||||
|
||||
- name: add deny write rights to Guest
|
||||
win_acl:
|
||||
path: '{{ test_acl_path }}'
|
||||
type: deny
|
||||
user: Guests
|
||||
rights: Write
|
||||
inherit: ContainerInherit
|
||||
propagation: NoPropagateInherit
|
||||
state: present
|
||||
register: add_deny_right
|
||||
|
||||
- name: get result of add deny write rights to Guest
|
||||
win_shell: '$path = ''{{ test_acl_path }}''; {{ test_ace_cmd }}'
|
||||
register: add_deny_right_actual
|
||||
|
||||
- name: assert add deny write rights to Guest
|
||||
assert:
|
||||
that:
|
||||
- add_deny_right is changed
|
||||
- (add_deny_right_actual.stdout|from_json)|count == 1
|
||||
- (add_deny_right_actual.stdout|from_json)[0].identity == 'BUILTIN\Guests'
|
||||
- (add_deny_right_actual.stdout|from_json)[0].inheritance_flags == 'ContainerInherit'
|
||||
- (add_deny_right_actual.stdout|from_json)[0].propagation_flags == 'NoPropagateInherit'
|
||||
- (add_deny_right_actual.stdout|from_json)[0].rights == 'Write'
|
||||
- (add_deny_right_actual.stdout|from_json)[0].type == 'Deny'
|
||||
|
||||
- name: add deny write rights to Guest (idempotent)
|
||||
win_acl:
|
||||
path: '{{ test_acl_path }}'
|
||||
type: deny
|
||||
user: Guests
|
||||
rights: Write
|
||||
inherit: ContainerInherit
|
||||
propagation: NoPropagateInherit
|
||||
state: present
|
||||
register: add_deny_right_again
|
||||
|
||||
- name: assert add deny write rights to Guest (idempotent)
|
||||
assert:
|
||||
that:
|
||||
- not add_deny_right_again is changed
|
||||
|
||||
- name: remove deny write rights from Guest
|
||||
win_acl:
|
||||
path: '{{ test_acl_path }}'
|
||||
type: deny
|
||||
user: Guests
|
||||
rights: Write
|
||||
inherit: ContainerInherit
|
||||
propagation: NoPropagateInherit
|
||||
state: absent
|
||||
register: remove_deny_right
|
||||
|
||||
- name: get result of remove deny write rights from Guest
|
||||
win_shell: '$path = ''{{ test_acl_path }}''; {{ test_ace_cmd }}'
|
||||
register: remove_deny_right_actual
|
||||
|
||||
- name: assert remove deny write rights from Guest
|
||||
assert:
|
||||
that:
|
||||
- remove_deny_right is changed
|
||||
- remove_deny_right_actual.stdout_lines == ["[", "", "]"]
|
||||
|
||||
- name: remove deny write rights from Guest (idempotent)
|
||||
win_acl:
|
||||
path: '{{ test_acl_path }}'
|
||||
type: deny
|
||||
user: Guests
|
||||
rights: Write
|
||||
inherit: ContainerInherit
|
||||
propagation: NoPropagateInherit
|
||||
state: absent
|
||||
register: remove_deny_right_again
|
||||
|
||||
- name: assert remove deny write rights from Guest (idempotent)
|
||||
assert:
|
||||
that:
|
||||
- not remove_deny_right_again is changed
|
||||
|
||||
- name: add write rights to Guest - network
|
||||
win_acl:
|
||||
path: '{{ test_acl_network_path }}'
|
||||
type: allow
|
||||
user: Guests
|
||||
rights: Write
|
||||
register: allow_right
|
||||
|
||||
- name: get result of add write rights to Guest - network
|
||||
win_shell: '$path = ''{{ test_acl_path }}''; {{ test_ace_cmd }}'
|
||||
register: allow_right_actual
|
||||
|
||||
- name: assert add write rights to Guest - network
|
||||
assert:
|
||||
that:
|
||||
- allow_right is changed
|
||||
- (allow_right_actual.stdout|from_json)|count == 1
|
||||
- (allow_right_actual.stdout|from_json)[0].identity == 'BUILTIN\Guests'
|
||||
- (allow_right_actual.stdout|from_json)[0].inheritance_flags == 'ContainerInherit, ObjectInherit'
|
||||
- (allow_right_actual.stdout|from_json)[0].propagation_flags == 'None'
|
||||
- (allow_right_actual.stdout|from_json)[0].rights == 'Write, Synchronize'
|
||||
- (allow_right_actual.stdout|from_json)[0].type == 'Allow'
|
||||
|
||||
- name: remove write rights from Guest - network
|
||||
win_acl:
|
||||
path: '{{ test_acl_network_path }}'
|
||||
type: allow
|
||||
user: Guests
|
||||
rights: Write
|
||||
state: absent
|
||||
register: remove_right
|
||||
|
||||
- name: get result of remove write rights from Guest - network
|
||||
win_shell: '$path = ''{{ test_acl_path }}''; {{ test_ace_cmd }}'
|
||||
register: remove_right_actual
|
||||
|
||||
- name: assert remove write rights from Guest
|
||||
assert:
|
||||
that:
|
||||
- remove_right is changed
|
||||
- remove_right_actual.stdout_lines == ["[", "", "]"]
|
||||
|
||||
- name: add write rights to Guest - registry
|
||||
win_acl:
|
||||
path: '{{ test_acl_reg_path }}'
|
||||
type: allow
|
||||
user: Guests
|
||||
rights: WriteKey
|
||||
register: allow_right_reg
|
||||
|
||||
- name: get result of add write rights to Guest - registry
|
||||
win_shell: '$path = ''{{ test_acl_reg_path }}''; {{ test_ace_cmd }}'
|
||||
register: allow_right_reg_actual
|
||||
|
||||
- name: assert add write rights to Guest - registry
|
||||
assert:
|
||||
that:
|
||||
- allow_right_reg is changed
|
||||
- (allow_right_reg_actual.stdout|from_json)|count == 1
|
||||
- (allow_right_reg_actual.stdout|from_json)[0].identity == 'BUILTIN\Guests'
|
||||
- (allow_right_reg_actual.stdout|from_json)[0].inheritance_flags == 'ContainerInherit, ObjectInherit'
|
||||
- (allow_right_reg_actual.stdout|from_json)[0].propagation_flags == 'None'
|
||||
- (allow_right_reg_actual.stdout|from_json)[0].rights == 'WriteKey'
|
||||
- (allow_right_reg_actual.stdout|from_json)[0].type == 'Allow'
|
||||
|
||||
- name: add write rights to Guest (idempotent) - registry
|
||||
win_acl:
|
||||
path: '{{ test_acl_reg_path }}'
|
||||
type: allow
|
||||
user: Guests
|
||||
rights: WriteKey
|
||||
register: allow_right_reg_again
|
||||
|
||||
- name: assert add write rights to Guest (idempotent) - registry
|
||||
assert:
|
||||
that:
|
||||
- not allow_right_reg_again is changed
|
||||
|
||||
- name: remove write rights from Guest - registry
|
||||
win_acl:
|
||||
path: '{{ test_acl_reg_path }}'
|
||||
type: allow
|
||||
user: Guests
|
||||
rights: WriteKey
|
||||
state: absent
|
||||
register: remove_right_reg
|
||||
|
||||
- name: get result of remove write rights from Guest - registry
|
||||
win_shell: '$path = ''{{ test_acl_reg_path }}''; {{ test_ace_cmd }}'
|
||||
register: remove_right_reg_actual
|
||||
|
||||
- name: assert remove write rights from Guest - registry
|
||||
assert:
|
||||
that:
|
||||
- remove_right_reg is changed
|
||||
- remove_right_reg_actual.stdout_lines == ["[", "", "]"]
|
||||
|
||||
- name: remove write rights from Guest (idempotent) - registry
|
||||
win_acl:
|
||||
path: '{{ test_acl_reg_path }}'
|
||||
type: allow
|
||||
user: Guests
|
||||
rights: WriteKey
|
||||
state: absent
|
||||
register: remove_right_reg_again
|
||||
|
||||
- name: assert remote write rights from Guest (idempotent) - registry
|
||||
assert:
|
||||
that:
|
||||
- not remove_right_reg_again is changed
|
||||
|
||||
- name: add deny write rights to Guest - registry
|
||||
win_acl:
|
||||
path: '{{ test_acl_reg_path }}'
|
||||
type: deny
|
||||
user: Guests
|
||||
rights: WriteKey
|
||||
inherit: ContainerInherit
|
||||
propagation: NoPropagateInherit
|
||||
state: present
|
||||
register: add_deny_right_reg
|
||||
|
||||
- name: get result of add deny write rights to Guest - registry
|
||||
win_shell: '$path = ''{{ test_acl_reg_path }}''; {{ test_ace_cmd }}'
|
||||
register: add_deny_right_reg_actual
|
||||
|
||||
- name: assert add deny write rights to Guest - registry
|
||||
assert:
|
||||
that:
|
||||
- add_deny_right_reg is changed
|
||||
- (add_deny_right_reg_actual.stdout|from_json)|count == 1
|
||||
- (add_deny_right_reg_actual.stdout|from_json)[0].identity == 'BUILTIN\Guests'
|
||||
- (add_deny_right_reg_actual.stdout|from_json)[0].inheritance_flags == 'ContainerInherit'
|
||||
- (add_deny_right_reg_actual.stdout|from_json)[0].propagation_flags == 'NoPropagateInherit'
|
||||
- (add_deny_right_reg_actual.stdout|from_json)[0].rights == 'WriteKey'
|
||||
- (add_deny_right_reg_actual.stdout|from_json)[0].type == 'Deny'
|
||||
|
||||
- name: add deny write rights to Guest (idempotent) - registry
|
||||
win_acl:
|
||||
path: '{{ test_acl_reg_path }}'
|
||||
type: deny
|
||||
user: Guests
|
||||
rights: WriteKey
|
||||
inherit: ContainerInherit
|
||||
propagation: NoPropagateInherit
|
||||
state: present
|
||||
register: add_deny_right_reg_again
|
||||
|
||||
- name: assert add deny write rights to Guest (idempotent) - registry
|
||||
assert:
|
||||
that:
|
||||
- not add_deny_right_reg_again is changed
|
||||
|
||||
- name: remove deny write rights from Guest - registry
|
||||
win_acl:
|
||||
path: '{{ test_acl_reg_path }}'
|
||||
type: deny
|
||||
user: Guests
|
||||
rights: WriteKey
|
||||
inherit: ContainerInherit
|
||||
propagation: NoPropagateInherit
|
||||
state: absent
|
||||
register: remove_deny_right_reg
|
||||
|
||||
- name: get result of remove deny write rights from Guest - registry
|
||||
win_shell: '$path = ''{{ test_acl_reg_path }}''; {{ test_ace_cmd }}'
|
||||
register: remove_deny_right_reg_actual
|
||||
|
||||
- name: assert remove deny write rights from Guest - registry
|
||||
assert:
|
||||
that:
|
||||
- remove_deny_right_reg is changed
|
||||
- remove_deny_right_reg_actual.stdout_lines == ["[", "", "]"]
|
||||
|
||||
- name: remove deny write rights from Guest (idempotent) - registry
|
||||
win_acl:
|
||||
path: '{{ test_acl_reg_path }}'
|
||||
type: deny
|
||||
user: Guests
|
||||
rights: WriteKey
|
||||
inherit: ContainerInherit
|
||||
propagation: NoPropagateInherit
|
||||
state: absent
|
||||
register: remove_deny_right_reg_again
|
||||
|
||||
- name: assert remove deny write rights from Guest (idempotent) - registry
|
||||
assert:
|
||||
that:
|
||||
- not remove_deny_right_reg_again is changed
|
||||
@ -1 +0,0 @@
|
||||
shippable/windows/group3
|
||||
@ -1 +0,0 @@
|
||||
test_win_acl_inheritance_path: C:\ansible\win_acl_inheritance .ÅÑŚÌβŁÈ [$!@^&test(;)]
|
||||
@ -1,36 +0,0 @@
|
||||
#!powershell
|
||||
|
||||
# WANT_JSON
|
||||
# POWERSHELL_COMMON
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version 2.0
|
||||
|
||||
$params = Parse-Args $args -supports_check_mode $false
|
||||
$path = Get-AnsibleParam -obj $params "path" -type "path" -failifempty $true
|
||||
|
||||
$result = @{
|
||||
changed = $false
|
||||
}
|
||||
|
||||
$acl = Get-Acl -LiteralPath $path
|
||||
|
||||
$result.inherited = $acl.AreAccessRulesProtected -eq $false
|
||||
|
||||
$user_details = @{}
|
||||
$acl.Access | ForEach-Object {
|
||||
$user = $_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value
|
||||
if ($user_details.ContainsKey($user)) {
|
||||
$details = $user_details.$user
|
||||
} else {
|
||||
$details = @{
|
||||
isinherited = $false
|
||||
}
|
||||
}
|
||||
$details.isinherited = $_.IsInherited
|
||||
$user_details.$user = $details
|
||||
}
|
||||
|
||||
$result.user_details = $user_details
|
||||
|
||||
Exit-Json $result
|
||||
@ -1,174 +0,0 @@
|
||||
---
|
||||
# Test setup
|
||||
# Use single task to save in CI runtime
|
||||
- name: create test folders
|
||||
win_shell: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$tmp_dir = '{{ test_win_acl_inheritance_path }}'
|
||||
if (Test-Path -LiteralPath $tmp_dir) {
|
||||
Remove-Item -LiteralPath $tmp_dir -Force -Recurse
|
||||
}
|
||||
New-Item -Path $tmp_dir -ItemType Directory > $null
|
||||
|
||||
Add-Type -AssemblyName System.DirectoryServices.AccountManagement
|
||||
$current_sid = ([System.DirectoryServices.AccountManagement.UserPrincipal]::Current).Sid
|
||||
$system_sid = New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList @([System.Security.Principal.WellKnownSidType]::LocalSystemSid, $null)
|
||||
$everyone_sid = New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList @([System.Security.Principal.WellKnownSidType]::WorldSid, $null)
|
||||
|
||||
$sd = New-Object -TypeName System.Security.AccessControl.DirectorySecurity
|
||||
$sd.SetAccessRuleProtection($true, $false)
|
||||
$sd.AddAccessRule(
|
||||
(New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList @(
|
||||
$system_sid,
|
||||
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
||||
[System.Security.AccessControl.InheritanceFlags]"ContainerInherit, ObjectInherit",
|
||||
[System.Security.AccessControl.PropagationFlags]::None,
|
||||
[System.Security.AccessControl.AccessControlType]::Allow
|
||||
))
|
||||
)
|
||||
$sd.AddAccessRule(
|
||||
(New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList @(
|
||||
$current_sid,
|
||||
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
||||
[System.Security.AccessControl.InheritanceFlags]"ContainerInherit, ObjectInherit",
|
||||
[System.Security.AccessControl.PropagationFlags]::None,
|
||||
[System.Security.AccessControl.AccessControlType]::Allow
|
||||
))
|
||||
)
|
||||
$sd.AddAccessRule(
|
||||
(New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList @(
|
||||
$everyone_sid,
|
||||
[System.Security.AccessControl.FileSystemRights]::Read,
|
||||
[System.Security.AccessControl.InheritanceFlags]"ContainerInherit, ObjectInherit",
|
||||
[System.Security.AccessControl.PropagationFlags]::None,
|
||||
[System.Security.AccessControl.AccessControlType]::Allow
|
||||
))
|
||||
)
|
||||
|
||||
Set-Acl -LiteralPath $tmp_dir -AclObject $sd
|
||||
|
||||
New-Item -Path "$tmp_dir\folder" -ItemType Directory > $null
|
||||
Set-Content -LiteralPath "$tmp_dir\folder\file.txt" -Value 'a'
|
||||
|
||||
$system_sid.Value
|
||||
$current_sid.Value
|
||||
$everyone_sid.Value
|
||||
register: test_sids # register the output SID values used for comparison tests below
|
||||
|
||||
# Run tests
|
||||
- name: remove inheritance check
|
||||
win_acl_inheritance:
|
||||
path: '{{ test_win_acl_inheritance_path }}\folder'
|
||||
reorganize: True
|
||||
state: absent
|
||||
register: remove_check
|
||||
check_mode: True
|
||||
|
||||
- name: get actual remove inheritance check
|
||||
test_get_acl:
|
||||
path: '{{ test_win_acl_inheritance_path }}\folder'
|
||||
register: actual_remove_check
|
||||
|
||||
- name: assert remove inheritance check
|
||||
assert:
|
||||
that:
|
||||
- remove_check is changed
|
||||
- actual_remove_check.inherited == True
|
||||
- actual_remove_check.user_details[test_sids.stdout_lines[0]].isinherited == True
|
||||
- actual_remove_check.user_details[test_sids.stdout_lines[1]].isinherited == True
|
||||
- actual_remove_check.user_details[test_sids.stdout_lines[2]].isinherited == True
|
||||
|
||||
- name: remove inheritance
|
||||
win_acl_inheritance:
|
||||
path: '{{ test_win_acl_inheritance_path }}\folder'
|
||||
reorganize: True
|
||||
state: absent
|
||||
register: remove
|
||||
|
||||
- name: get actual remove inheritance
|
||||
test_get_acl:
|
||||
path: '{{ test_win_acl_inheritance_path }}\folder'
|
||||
register: actual_remove
|
||||
|
||||
- name: assert remove inheritance
|
||||
assert:
|
||||
that:
|
||||
- remove is changed
|
||||
- actual_remove.inherited == False
|
||||
- actual_remove.user_details[test_sids.stdout_lines[0]].isinherited == False
|
||||
- actual_remove.user_details[test_sids.stdout_lines[1]].isinherited == False
|
||||
- actual_remove.user_details[test_sids.stdout_lines[2]].isinherited == False
|
||||
|
||||
- name: remove inheritance again
|
||||
win_acl_inheritance:
|
||||
path: '{{ test_win_acl_inheritance_path }}\folder'
|
||||
reorganize: True
|
||||
state: absent
|
||||
register: remove_again
|
||||
|
||||
- name: assert remove inheritance again
|
||||
assert:
|
||||
that:
|
||||
- remove_again is not changed
|
||||
|
||||
- name: add inheritance check
|
||||
win_acl_inheritance:
|
||||
path: '{{ test_win_acl_inheritance_path }}\folder'
|
||||
reorganize: True
|
||||
state: present
|
||||
register: add_check
|
||||
check_mode: True
|
||||
|
||||
- name: get actual add inheritance check
|
||||
test_get_acl:
|
||||
path: '{{ test_win_acl_inheritance_path }}\folder'
|
||||
register: actual_add_check
|
||||
|
||||
- name: assert add inheritance check
|
||||
assert:
|
||||
that:
|
||||
- add_check is changed
|
||||
- actual_add_check.inherited == False
|
||||
- actual_add_check.user_details[test_sids.stdout_lines[0]].isinherited == False
|
||||
- actual_add_check.user_details[test_sids.stdout_lines[1]].isinherited == False
|
||||
- actual_add_check.user_details[test_sids.stdout_lines[2]].isinherited == False
|
||||
|
||||
- name: add inheritance
|
||||
win_acl_inheritance:
|
||||
path: '{{ test_win_acl_inheritance_path }}\folder'
|
||||
reorganize: True
|
||||
state: present
|
||||
register: add
|
||||
|
||||
- name: get actual add inheritance
|
||||
test_get_acl:
|
||||
path: '{{ test_win_acl_inheritance_path }}\folder'
|
||||
register: actual_add
|
||||
|
||||
- name: assert add inheritance
|
||||
assert:
|
||||
that:
|
||||
- add is changed
|
||||
- actual_add.inherited == True
|
||||
- actual_add.user_details[test_sids.stdout_lines[0]].isinherited == True
|
||||
- actual_add.user_details[test_sids.stdout_lines[1]].isinherited == True
|
||||
- actual_add.user_details[test_sids.stdout_lines[2]].isinherited == True
|
||||
|
||||
- name: add inheritance again
|
||||
win_acl_inheritance:
|
||||
path: '{{ test_win_acl_inheritance_path }}\folder'
|
||||
reorganize: True
|
||||
state: present
|
||||
register: add_again
|
||||
|
||||
- name: assert add inheritance again
|
||||
assert:
|
||||
that:
|
||||
- add_again is not changed
|
||||
|
||||
# Test cleanup
|
||||
- name: remove test folder
|
||||
win_file:
|
||||
path: '{{ test_win_acl_inheritance_path }}'
|
||||
state: absent
|
||||
@ -1,2 +0,0 @@
|
||||
shippable/windows/group7
|
||||
skip/windows/2016 # Host takes a while to run and module isn't OS dependent
|
||||
@ -1,4 +0,0 @@
|
||||
win_cert_dir: '{{win_output_dir}}\win_certificate .ÅÑŚÌβŁÈ [$!@^&test(;)]'
|
||||
key_password: password
|
||||
subj_thumbprint: 'BD7AF104CF1872BDB518D95C9534EA941665FD27'
|
||||
root_thumbprint: 'BC05633694E675449136679A658281F17A191087'
|
||||
@ -1,20 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDKDCCAhCgAwIBAgIJAP1vIdGgMJv/MA0GCSqGSIb3DQEBCwUAMCgxGTAXBgNV
|
||||
BAMMEHJvb3QuYW5zaWJsZS5jb20xCzAJBgNVBAYTAlVTMCAXDTE3MTIxNTA4Mzkz
|
||||
MloYDzIwODYwMTAyMDgzOTMyWjAoMRkwFwYDVQQDDBByb290LmFuc2libGUuY29t
|
||||
MQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMmq
|
||||
YT8eZY6rFQKnmScUGnnUH1tLQ+3WQpfKiWygCUSb1CNqO3J1u3pGMEqYM58LK4Kr
|
||||
Mpskv7K1tCV/EMZqGTqXAIfSLy9umlb/9C3AhL9thBPn5I9dam/EmrIZktI9/w5Y
|
||||
wBXn4toe+OopA3QkMQh9BUjUCPb9fdOI+ir7OGFZMmxXmiM64+BEeywM2oSGsdZ9
|
||||
5hU378UBu2IX4+OAV8Fbr2l6VW+Fxg/tKIOo6Bs46Pa4EZgtemOqs3kxYBOltBTb
|
||||
vFcLsLa4KYVu5Ge5YfB0Axfaem7PoP8IlMs8gxyojZ/r0o5hzxUcYlL/h8GeeoLW
|
||||
PFFdiAS+UgxWINOqNXMCAwEAAaNTMFEwHQYDVR0OBBYEFLp9k4LmOnAR4ROrqhb+
|
||||
CFdbk2+oMB8GA1UdIwQYMBaAFLp9k4LmOnAR4ROrqhb+CFdbk2+oMA8GA1UdEwEB
|
||||
/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGksycHsjGbXfWfuhQh+CvXk/A2v
|
||||
MoNgiHtNMTGliVNgoVp1B1rj4x9xyZ8YrO8GAmv8jaCwCShd0B5Ul4aZVk1wglVv
|
||||
lFAwb4IAZN9jv9+fw5BRzQ2tLhkVWIEwx6pZkhGhhjBvMaplLN5JwBtsdZorFbm7
|
||||
wuKiUKcFAM28acoOhCmOhgyNNBZpZn5wXaQDY43AthJOhitAV7vph4MPUkwIJnOh
|
||||
MA5GJXEqS58TE9z9pkhQnn9598G8tmOXyA2erAoM9JAXM3EYHxVpoHBb9QRj6WAw
|
||||
XVBo6qRXkwjNEM5CbnD4hVIBsdkOGsDrgd4Q5izQZ3x+jFNkdL/zPsXjJFw=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAyaphPx5ljqsVAqeZJxQaedQfW0tD7dZCl8qJbKAJRJvUI2o7
|
||||
cnW7ekYwSpgznwsrgqsymyS/srW0JX8QxmoZOpcAh9IvL26aVv/0LcCEv22EE+fk
|
||||
j11qb8SashmS0j3/DljAFefi2h746ikDdCQxCH0FSNQI9v1904j6Kvs4YVkybFea
|
||||
Izrj4ER7LAzahIax1n3mFTfvxQG7Yhfj44BXwVuvaXpVb4XGD+0og6joGzjo9rgR
|
||||
mC16Y6qzeTFgE6W0FNu8VwuwtrgphW7kZ7lh8HQDF9p6bs+g/wiUyzyDHKiNn+vS
|
||||
jmHPFRxiUv+HwZ56gtY8UV2IBL5SDFYg06o1cwIDAQABAoIBAFRpZNsutgPJyLmb
|
||||
vZeF6q8kAxwLnRtom+c9d9hoBHkbYOiSBuAaN6cuyffvTWw9GLFRR5V5BGSheg5X
|
||||
6YWj03uayTYQ3H9WJHRWHrcn5mjaRnaukhUQXQT7nmT+H16xZJl0vLJupZ33aOla
|
||||
0X9DxuJusk+RsU7xPEHXDCABl8/m7v3cFttUBughGBG5oDuzKlFbhXPwA8/yeJ1v
|
||||
qdXKxENi9HO4X5fH1l0vFNIhEqvUVKjw/AzapYtr+bv1wssoNAzvhT7CFa2GjPQ0
|
||||
Ibcq3+RxyAN4iQVITy86Yl4LW1jLx63wbg9q1WG/ca9K/OEAuT7ebJNeMYmM+kf7
|
||||
sf6A8wECgYEA+nnLJ4QtANtAs6nmDC106DTx1cOf3Yq9JOAvmGLomF/8SrUzZbXM
|
||||
F+JcZcttXuuFIFcZD0K7fFP9sx2ITH//BS5V0B0x7Z2olWexVjR6/5pOVFPu19ow
|
||||
tyDCNi5BlTPbvSr/fAxjmO9SgVTb8oG66i4mi0Xn5bp1E441KdvNsHECgYEAzhz/
|
||||
+SjFJlJcGNvMmgfAbfv6McUv7TKrPIvVkA++Gi5QdqJjkuzL1uTfgWIY/9iDByMd
|
||||
W36rFTkYrw6LTMF2dkMjul72Kkco3UExSzOmF4lFmCt3DZW6a6CExKpwk4kF2RnX
|
||||
GRD0FoZZown3RbPHi9rsWxjyVy/yKGwnvXYndiMCgYEA6rnIUDfllK/jansFQtQ2
|
||||
goVbPGAfKJYjurL852mJX4JUBA7bI63CnX9b52lEDXfZQf1dVpfK6zAqx/gdCtPI
|
||||
QSqy8FzrtSnSGnEaFxcHTRFl5lDhuxaWIIdqeSvP+eqnOhdZZP6XN3LPdrP3isNY
|
||||
Tq0BIfNY5khd/v19hMSfdYECgYBQ8h6tMY/LrwiwUpIV4/l0uELYDQL3erC5RImI
|
||||
3EXiblH3ZWsJpqmfKZ+FZos+3z8GLIo5BpQV76h8B5A5grkNVOzRIr42eF/aFOJR
|
||||
EGWoVKbaTiehVC40WoQJ4I35wxRi4L0TAQ97USQe3akY3LP/fujYFgIGr7PAoEkz
|
||||
JRX2VQKBgQDir8/a3FZVo6nYI8zIhBz8xqZJIgvlYQqiQFFwADu5eNPMvNIaVy+6
|
||||
7HKibGM2jPkuS2KHdc8WUp8IrRRMui04qE7kRxVu41QXEBfPiDvrvAQf8SfJe631
|
||||
XvYeZr7HKY4NI5J0ENcb54d7DLQ8a1/wL/GeLVrfUWG35Ra5MW57Og==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC0TCCAbkCCQC/MtOBa1UDpzANBgkqhkiG9w0BAQsFADAoMRkwFwYDVQQDDBBy
|
||||
b290LmFuc2libGUuY29tMQswCQYDVQQGEwJVUzAgFw0xNzEyMTUwODU2MzBaGA8y
|
||||
MDg2MDEwMjA4NTYzMFowKzEcMBoGA1UEAwwTc3ViamVjdC5hbnNpYmxlLmNvbTEL
|
||||
MAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDszqdF
|
||||
So3GlVP1xUnN4bSPrFRFiOl/Mqup0Zn5UJJUR9wLnRD+OLcq7kKin6hYqozSu7cC
|
||||
+BnWQoq7vGSSNVqv7BqFMwzGJt9IBUQv0UqIQkA/duUdKdAiMn2PQRsNDnkWEbTj
|
||||
4xsitItVNv84cDG0lkZBYyTgfyZlZLZWplkpUQkrZhoFCekZRJ+ODrqNW3W560rr
|
||||
OUIh+HiQeBqocat6OdxgICBqpUh8EVo1iha3DXjGN08q5utg6gmbIl2VBaVJjfyd
|
||||
wnUSqHylJwh6WCIEh+HXsn4ndfNWSN/fDqvi5I10V1j6Zos7yqQf8qAezUAm6eSq
|
||||
hLgZz0odq9DsO4HHAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFK5mVIJ2D+kI0kk
|
||||
sxnW4ibWFjzlYFYPYrZg+2JFIVTbKBg1YzyhuIKm0uztqRxQq5iLn/C/uponHoqF
|
||||
7KDQI37KAJIQdgSva+mEuO9bZAXg/eegail2hN6np7HjOKlPu23s40dAbFrbcOWP
|
||||
VbsBEPDP0HLv6OgbQWzNlE9HO1b7pX6ozk3q4ULO7IR85P6OHYsBBThL+qsOTzg/
|
||||
gVknuB9+n9hgNqZcAcXBLDetOM9aEmYJCGk0enYP5UGLYpseE+rTXFbRuHTPr1o6
|
||||
e8BetiSWS/wcrV4ZF5qr9NiYt5eD6JzTB5Rn5awxxj0FwMtrBu003lLQUWxsuTzz
|
||||
35/RLY4=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEA7M6nRUqNxpVT9cVJzeG0j6xURYjpfzKrqdGZ+VCSVEfcC50Q
|
||||
/ji3Ku5Cop+oWKqM0ru3AvgZ1kKKu7xkkjVar+wahTMMxibfSAVEL9FKiEJAP3bl
|
||||
HSnQIjJ9j0EbDQ55FhG04+MbIrSLVTb/OHAxtJZGQWMk4H8mZWS2VqZZKVEJK2Ya
|
||||
BQnpGUSfjg66jVt1uetK6zlCIfh4kHgaqHGrejncYCAgaqVIfBFaNYoWtw14xjdP
|
||||
KubrYOoJmyJdlQWlSY38ncJ1Eqh8pScIelgiBIfh17J+J3XzVkjf3w6r4uSNdFdY
|
||||
+maLO8qkH/KgHs1AJunkqoS4Gc9KHavQ7DuBxwIDAQABAoIBAQDfjqBfS+jYZrUi
|
||||
uqPYV5IMaNYN5xj4Wi+xXA0OT0A1jLlxxU/7kDNrtg72U9+sBSZ483nstag+nAc5
|
||||
ALu5Q+FfX3gR84XFs4DrDv22XtEMHe9leqsFgynYfu4GRaJyCw3JBeJNmWNOuj8n
|
||||
rYn4EAL8xzmAFUcFIURwSEnTN6vI0cS09nQukz+9CIBuGr7TPMET8YlATDJcH+Ua
|
||||
EGZ9MAFXdKF6adC2nrCVBDNr8mUEpK1XdQcPH2bvcTuZ3Jj5AF2rOrcHq4FZUm97
|
||||
8PaMH6Sarxhwl+ycwrKbU5aEzUYTk67k0V6m9lyvH9z3O3Y84Tr3cZZ5WxdnG6Ri
|
||||
72MFlfgRAoGBAP8wA+KWJ/ttmEXAoSX4J2fPl7X1RhR+1zPNdLY7iX0uNstL8IFH
|
||||
vUN9JHi1Tr7llav+2bUTOu2EMDVmDWZH0s/qKOn+GmqIQLp0441fVAiamTcgwGKE
|
||||
Wwsu4dg10IJ9akHIIbrILT0CvRcIRf67EYLBj3ZwfR+wF1ncefbsxWA9AoGBAO2P
|
||||
qGMn+yrIi5DZF23x6iD2Y7bIdlUmqIqwb99XhW+3YJmRuh1EuN6XP2bIveRa9xvm
|
||||
Q7bbcQM0Yv2c7eTyxpzz2I4bmnccVbs6M1VhtkyQEy5+X5yOl9wnitaaUrbWFy/w
|
||||
kDPuISjLl3xDlxd6dbjf70fkG5oogx5c5toEyWZTAoGAK1CHGErMdozfr9dGgx9f
|
||||
8Or3oVcEki4FcTGKgfQRHkJd4pv9MrRul6oCKsr7lsN5aDxVz7p34iDx3d54n8fJ
|
||||
LKleUHllGngOJJf6l+B6bwtuvkC85vv4SCmpA/3+amfHRWsm7oFTzGtOlT4+Q0KV
|
||||
clBQfZYSZvKIxCP8P8ForzECgYEAjDOad1qjOy68X7Ifx71cJjQDyV4pqDt2gNN8
|
||||
Ut1+XN5m3ntI0fk6+fNdcbXLjDe7WvXcxNBhtDh4q6CwLcyyNvMavVPBJ8bLOgIx
|
||||
RZSzWCA3kdr3ZpgpO78Ci4DsjAdyC9L36A4D9+Wf87CYPT0CuSdAOrd/Ks36BDNj
|
||||
8wucKQ0CgYAaRwQ18nkemrpQ/+EQgEWnWfqgB+6T4ygZ4ZTym0FAtG7CdLxvCi8V
|
||||
toyn+zi+yFTRFXHDmvg9HLIIMK/hRQjgc8Ns5nDwgQlGwCZTvjVbD4anCr1IWuky
|
||||
owvxKWsHseNilKrnAk2maQxrrrpSk8QWrp2CFw04LsWGTxtFvstBmg==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
dependencies:
|
||||
- prepare_win_tests
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue