diff --git a/lib/ansible/module_utils/powershell.ps1 b/lib/ansible/module_utils/powershell.ps1 index 16cc726acd0..e22fad9d0fa 100644 --- a/lib/ansible/module_utils/powershell.ps1 +++ b/lib/ansible/module_utils/powershell.ps1 @@ -110,57 +110,60 @@ Function Expand-Environment($value) #Get-AnsibleParam also supports Parameter validation to save you from coding that manually: #Example: Get-AnsibleParam -obj $params -name "State" -default "Present" -ValidateSet "Present","Absent" -resultobj $resultobj -failifempty $true #Note that if you use the failifempty option, you do need to specify resultobject as well. -Function Get-AnsibleParam($obj, $name, $default = $null, $resultobj, $failifempty=$false, $emptyattributefailmessage, $ValidateSet, $ValidateSetErrorMessage, $type=$null) +Function Get-AnsibleParam($obj, $name, $default = $null, $resultobj = @{}, $failifempty = $false, $emptyattributefailmessage, $ValidateSet, $ValidateSetErrorMessage, $type = $null, $aliases = @()) { - # Check if the provided Member $name exists in $obj and return it or the default. - Try - { - If (-not $obj.$name.GetType) - { + # Check if the provided Member $name or aliases exist in $obj and return it or the default. + try { + + $found = $null + # First try to find preferred parameter $name + $aliases = @($name) + $aliases + + # Iterate over aliases to find acceptable Member $name + foreach ($alias in $aliases) { + if (Get-Member -InputObject $obj -Name $alias) { + $found = $alias + break + } + } + + if ($found -eq $null) { throw } + $name = $found - if ($ValidateSet) - { - if ($ValidateSet -contains ($obj.$name)) - { + if ($ValidateSet) { + + if ($ValidateSet -contains ($obj.$name)) { $value = $obj.$name - } - Else - { - if ($ValidateSetErrorMessage -eq $null) - { + } else { + if ($ValidateSetErrorMessage -eq $null) { #Auto-generated error should be sufficient in most use cases $ValidateSetErrorMessage = "Argument $name needs to be one of $($ValidateSet -join ",") but was $($obj.$name)." } Fail-Json -obj $resultobj -message $ValidateSetErrorMessage } - } - Else - { + + } else { $value = $obj.$name } - } - Catch - { - If ($failifempty -eq $false) - { + + } catch { + if ($failifempty -eq $false) { $value = $default - } - Else - { - If (!$emptyattributefailmessage) - { + } else { + if (!$emptyattributefailmessage) { $emptyattributefailmessage = "Missing required argument: $name" } Fail-Json -obj $resultobj -message $emptyattributefailmessage } + } - If ($value -ne $null -and $type -eq "path") { + if ($value -ne $null -and $type -eq "path") { # Expand environment variables on path-type (Beware: turns $null into "") $value = Expand-Environment($value) - } ElseIf ($type -eq "bool") { + } elseif ($type -eq "bool") { # Convert boolean types to real Powershell booleans $value = $value | ConvertTo-Bool } @@ -221,7 +224,7 @@ Function Parse-Args($arguments, $supports_check_mode = $false) $parameters } -# Helper function to calculate a hash of a file in a way which powershell 3 +# Helper function to calculate a hash of a file in a way which powershell 3 # and above can handle: Function Get-FileChecksum($path) { @@ -255,7 +258,7 @@ Function Get-PendingRebootStatus { return $True } - else + else { return $False } diff --git a/lib/ansible/modules/windows/win_regedit.ps1 b/lib/ansible/modules/windows/win_regedit.ps1 index 78497891b9f..f82c69f83d6 100644 --- a/lib/ansible/modules/windows/win_regedit.ps1 +++ b/lib/ansible/modules/windows/win_regedit.ps1 @@ -16,221 +16,283 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -$ErrorActionPreference = "Stop" - # WANT_JSON # POWERSHELL_COMMON -New-PSDrive -PSProvider registry -Root HKEY_CLASSES_ROOT -Name HKCR -ErrorAction SilentlyContinue | Out-Null -New-PSDrive -PSProvider registry -Root HKEY_USERS -Name HKU -ErrorAction SilentlyContinue | Out-Null -New-PSDrive -PSProvider registry -Root HKEY_CURRENT_CONFIG -Name HCCC -ErrorAction SilentlyContinue | Out-Null +# TODO: Add missing REG_NONE support + +$ErrorActionPreference = "Stop" + +$params = Parse-Args $args -supports_check_mode $true +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false -$params = Parse-Args $args; -$result = New-Object PSObject; -Set-Attr $result "changed" $false; -Set-Attr $result "data_changed" $false; -Set-Attr $result "data_type_changed" $false; +$path = Get-AnsibleParam -obj $params -name "path" -type "string" -failifempty $true -aliases "key" +$name = Get-AnsibleParam -obj $params -name "name" -type "string" -aliases "entry","value" +$data = Get-AnsibleParam -obj $params -name "data" +$type = Get-AnsibleParam -obj $params -name "type" -type "string" -validateSet "binary","dword","expandstring","multistring","string","qword" -aliases "datatype" -default "string" +$state = Get-AnsibleParam -obj $params -name "state" -type "string" -validateSet "present","absent" -default "present" -$registryKey = Get-Attr -obj $params -name "key" -failifempty $true -$registryValue = Get-Attr -obj $params -name "value" -default $null -$state = Get-Attr -obj $params -name "state" -validateSet "present","absent" -default "present" -$registryData = Get-Attr -obj $params -name "data" -default $null -$registryDataType = Get-Attr -obj $params -name "datatype" -validateSet "binary","dword","expandstring","multistring","string","qword" -default "string" +$result = @{ + changed = $false + data_changed = $false + data_type_changed = $false + diff = @{ + prepared = "" + } + warnings = @() +} -If ($state -eq "present" -and $registryData -eq $null -and $registryValue -ne $null) -{ +if ($state -eq "present" -and $data -eq $null -and $name -ne $null) { Fail-Json $result "missing required argument: data" } -# check the registry key is in powershell ps-drive format: HKLM, HKCU, HKU, HKCR, HCCC -If (-not ($registryKey -match "^H[KC][CLU][MURC]{0,1}:\\")) -{ - Fail-Json $result "key: $registryKey is not a valid powershell path, see module documentation for examples." +# Fix HCCC:\ PSDrive for pre-2.3 compatibility +if ($path -match "^HCCC:\\") { + $result.warnings += "Please use path: HKCC:\... instead of path: $path\n" + $path = $path -replace "HCCC:\\","HKCC:\\" } +# Check that the registry path is in PSDrive format: HKCC, HKCR, HKCU, HKLM, HKU +if (-not ($path -match "^HK(CC|CR|CU|LM|U):\\")) { + Fail-Json $result "path: $path is not a valid powershell path, see module documentation for examples." +} -Function Test-RegistryValueData { +# Allow empty values as the "(default)" value +if ($name -eq "") { + $registryValue = "(default)" +} + +Function Test-ValueData { Param ( - [parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()]$Path, - [parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()]$Value + [parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] $Path, + [parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] $Name ) - Try { - Get-ItemProperty -Path $Path -Name $Value - Return $true - } - Catch { - Return $false + + try { + Get-ItemProperty -Path $Path -Name $Name + return $true + } catch { + return $false } } # Returns true if registry data matches. # Handles binary, integer(dword) and string registry data -Function Compare-RegistryData { +Function Compare-Data { Param ( - [parameter(Mandatory=$true)] - [AllowEmptyString()]$ReferenceData, - [parameter(Mandatory=$true)] - [AllowEmptyString()]$DifferenceData - ) - - if ($ReferenceData -is [String] -or $ReferenceData -is [int]) { - if ($ReferenceData -eq $DifferenceData) { - return $true - } else { - return $false - } - } elseif ($ReferenceData -is [Object[]]) { - if (@(Compare-Object $ReferenceData $DifferenceData -SyncWindow 0).Length -eq 0) { - return $true - } else { - return $false - } + [parameter(Mandatory=$true)] [AllowEmptyString()] $ReferenceData, + [parameter(Mandatory=$true)] [AllowEmptyString()] $DifferenceData + ) + + if ($ReferenceData -is [String] -or $ReferenceData -is [int]) { + if ($ReferenceData -eq $DifferenceData) { + return $true + } else { + return $false } + } elseif ($ReferenceData -is [Object[]]) { + if (@(Compare-Object $ReferenceData $DifferenceData -SyncWindow 0).Length -eq 0) { + return $true + } else { + return $false + } + } } # 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 -function Convert-RegExportHexStringToByteArray -{ +function Convert-RegExportHexStringToByteArray { Param ( - [parameter(Mandatory=$true)] [String] $String + [parameter(Mandatory=$true)] [String] $String ) -# remove 'hex:' from the front of the string if present -$String = $String.ToLower() -replace '^hex\:', '' + # 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.ToLower() -replace '[^a-f0-9\\,x\-\:]','' + # Remove whitespace and any other non-hex crud. + $String = $String.ToLower() -replace '[^a-f0-9\\,x\-\:]','' -# turn commas into colons -$String = $String -replace ',',':' + # Turn commas into colons + $String = $String -replace ',',':' -#Maybe there's nothing left over to convert... -if ($String.Length -eq 0) { ,@() ; return } + # 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) -{ ,@([System.Convert]::ToByte($String,16)) } -elseif (($String.Length % 2 -eq 0) -and ($String.IndexOf(":") -eq -1)) -{ ,@($String -split '([a-f0-9]{2})' | foreach-object { if ($_) {[System.Convert]::ToByte($_,16)}}) } -elseif ($String.IndexOf(":") -ne -1) -{ ,@($String -split ':+' | foreach-object {[System.Convert]::ToByte($_,16)}) } -else -{ ,@() } + # 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 ,@() + } +} +# Create the required PSDrives if missing +if (-not (Test-Path HKCR:\)) { + New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT +} +if (-not (Test-Path HKU:\)) { + New-PSDrive -Name HKU -PSProvider Registry -Root HKEY_USERS +} +if (-not (Test-Path HKCC:\)) { + New-PSDrive -Name HKCC -PSProvider Registry -Root HKEY_CURRENT_CONFIG } -if($registryDataType -eq "binary" -and $registryData -ne $null -and $registryData -is [String]) { - $registryData = Convert-RegExportHexStringToByteArray($registryData) +# Convert HEX string to binary if type binary +if ($type -eq "binary" -and $data -ne $null -and $data -is [String]) { + $data = Convert-RegExportHexStringToByteArray($data) } -if($state -eq "present") { - if ((Test-Path $registryKey) -and $registryValue -ne $null) - { - if (Test-RegistryValueData -Path $registryKey -Value $registryValue) - { - # handle binary data - $currentRegistryData =(Get-ItemProperty -Path $registryKey | Select-Object -ExpandProperty $registryValue) - - if ($registryValue.ToLower() -eq "(default)") { - # Special case handling for the key's default property. Because .GetValueKind() doesn't work for the (default) key property - $oldRegistryDataType = "String" - } - else { - $oldRegistryDataType = (Get-Item $registryKey).GetValueKind($registryValue) +# Expand string if type expandstring +if ($type -eq "expandstring" -and $data -ne $null -and $data -is [String]) { + $data = Expand-Environment($data) + $datatype = "string" +} + +if ($state -eq "present") { + + if ((Test-Path $path) -and $name -ne $null) { + + if (Test-ValueData -Path $path -Name $name) { + + # Handle binary data + $old_data =(Get-ItemProperty -Path $path | Select-Object -ExpandProperty $name) + + if ($name.ToLower() -eq "(default)") { + # Special case handling for the path's default property. + # Because .GetValueKind() doesn't work for the (default) path property + $old_type = "String" + } else { + $old_type = (Get-Item $path).GetValueKind($name) } - # Changes Data and DataType - if ($registryDataType -ne $oldRegistryDataType) - { - Try - { - Remove-ItemProperty -Path $registryKey -Name $registryValue - New-ItemProperty -Path $registryKey -Name $registryValue -Value $registryData -PropertyType $registryDataType - $result.changed = $true - $result.data_changed = $true - $result.data_type_changed = $true + if ($type -ne $old_type) { + # Changes Data and DataType + if (-not $check_mode) { + try { + Remove-ItemProperty -Path $path -Name $name + New-ItemProperty -Path $path -Name $name -Value $data -PropertyType $type -Force + } catch { + Fail-Json $result $_.Exception.Message + } } - Catch - { - Fail-Json $result $_.Exception.Message + $result.changed = $true + $result.data_changed = $true + $result.data_type_changed = $true + $result.diff.prepared += @" + [$path] +-"$name" = "$old_type`:$data" ++"$name" = "$type`:$data" +"@ + + } elseif (-not (Compare-Data -ReferenceData $old_data -DifferenceData $data)) { + # Changes Only Data + if (-not $check_mode) { + try { + Set-ItemProperty -Path $path -Name $name -Value $data + } catch { + Fail-Json $result $_.Exception.Message + } } + $result.changed = $true + $result.data_changed = $true + $result.diff.prepared += @" + [$path] +-"$name" = "$type`:$old_data" ++"$name" = "$type`:$data" +"@ + + } else { + # Nothing to do, everything is already as requested } - # Changes Only Data - elseif (-Not (Compare-RegistryData -ReferenceData $currentRegistryData -DifferenceData $registryData)) - { - Try { - Set-ItemProperty -Path $registryKey -Name $registryValue -Value $registryData - $result.changed = $true - $result.data_changed = $true - } - Catch - { + + } else { + + if (-not $check_mode) { + try { + New-ItemProperty -Path $path -Name $name -Value $data -PropertyType $type + } Catch { Fail-Json $result $_.Exception.Message } } + $result.changed = $true + $result.diff.prepared += @" + [$path] ++"$name" = "$type`:$data" +"@ } - else - { - Try - { - New-ItemProperty -Path $registryKey -Name $registryValue -Value $registryData -PropertyType $registryDataType - $result.changed = $true - } - Catch - { + + } elseif (-not (Test-Path $path)) { + + if (-not $check_mode) { + try { + $new_path = New-Item $path -Type directory -Force + if ($name -ne $null) { + $new_path | New-ItemProperty -Name $name -Value $data -PropertyType $type -Force + } + } catch { + throw Fail-Json $result $_.Exception.Message } } - } - elseif(-not (Test-Path $registryKey)) - { - Try - { - $newRegistryKey = New-Item $registryKey -Force - $result.changed = $true + $result.changed = $true + $result.diff.prepared += @" ++[$path"] - if($registryValue -ne $null) { - $newRegistryKey | New-ItemProperty -Name $registryValue -Value $registryData -Force -PropertyType $registryDataType - $result.changed = $true - } - } - Catch - { - Fail-Json $result $_.Exception.Message +"@ + if ($name -ne $null) { + $result.diff.prepared += @" ++"$name" = "$type`:$data" + +"@ } + + } else { + # FIXME: Value is null, should we silently ignore this and do nothing ? } -} -else -{ - if (Test-Path $registryKey) - { - if ($registryValue -eq $null) { - Try - { - Remove-Item -Path $registryKey -Recurse - $result.changed = $true - } - Catch - { - Fail-Json $result $_.Exception.Message - } - } - elseif (Test-RegistryValueData -Path $registryKey -Value $registryValue) { - Try - { - Remove-ItemProperty -Path $registryKey -Name $registryValue - $result.changed = $true + +} elseif ($state -eq "absent") { + + if (Test-Path $path) { + if ($name -eq $null) { + + if (-not $check_mode) { + try { + Remove-Item -Path $path -Recurse + } catch { + Fail-Json $result $_.Exception.Message + } } - Catch - { - Fail-Json $result $_.Exception.Message + $result.changed = $true + $result.diff.prepared += @" +-[$path] +-"$name" = "$type`:$data" +"@ + + } elseif (Test-ValueData -Path $path -Value $name) { + + if (-not $check_mode) { + try { + Remove-ItemProperty -Path $path -Name $name + } catch { + Fail-Json $result $_.Exception.Message + } } + $result.changed = $true + $result.diff.prepared += @" + [$path] +-"$name" = "$type`:$data" +"@ } + } else { + # Nothing to do, everything is already as requested } } diff --git a/lib/ansible/modules/windows/win_regedit.py b/lib/ansible/modules/windows/win_regedit.py index 9b6919d3047..a32d40aa50d 100644 --- a/lib/ansible/modules/windows/win_regedit.py +++ b/lib/ansible/modules/windows/win_regedit.py @@ -29,32 +29,30 @@ DOCUMENTATION = r''' --- module: win_regedit version_added: "2.0" -short_description: Add, Edit, or Remove Registry Keys and Values +short_description: Add, change, or remove registry keys and values description: - - Add, Edit, or Remove Registry Keys and Values using ItemProperties Cmdlets + - Add, modify or remove registry keys and values. + - More information about the windows registry from Wikipedia (https://en.wikipedia.org/wiki/Windows_Registry). options: - key: + path: description: - - Name of Registry Key + - Name of registry path. + - Should be in one of the following registry hives: HKCC, HKCR, HKCU, HKLM, HKU. required: true - default: null - aliases: [] - value: + aliases: [ key ] + name: description: - - Name of Registry Value - required: true - default: null - aliases: [] + - Name of registry entry in C(path). + - This is an entry in the above C(key) parameter. + - If not provided, or empty we use the default name '(default)' + aliases: [ entry ] data: description: - - Registry Value Data. Binary data should be expressed 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 values will look like C(hex:be,ef,be,ef). The C(hex:) prefix is optional. - required: false - default: null - aliases: [] - datatype: + - Value of the registry entry C(name) in C(path). + - Binary data should be expressed 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 values will look like C(hex:be,ef,be,ef). The C(hex:) prefix is optional. + type: description: - - Registry Value Data Type - required: false + - Registry value data type. choices: - binary - dword @@ -63,65 +61,77 @@ options: - string - qword default: string - aliases: [] + aliases: [ datatype ] state: description: - - State of Registry Value - required: false + - State of registry entry. choices: - present - absent default: present - aliases: [] +notes: +- Check-mode C(-C/--check) and diff output (-D/--diff) are supported, so that you can test every change against the active configuration before applying changes. +- At the moment REG_NONE support is missing because it is lacking from the Powershell API. Workarounds are possible but currently lacking. +- Beware that some registry hives (HKEY_USERS in particular) do not allow to create new registry paths. author: "Adam Keech (@smadam813), Josh Ludwig (@joshludwig)" ''' EXAMPLES = r''' -- name: Create Registry Key called MyCompany +- name: Create registry path MyCompany win_regedit: - key: HKCU:\Software\MyCompany + path: HKCU:\Software\MyCompany -- name: Create Registry Key called MyCompany, a value within MyCompany Key called "hello", and data for the value "hello" containing "world". +- name: Add or update registry path MyCompany, with entry 'hello', and containing 'world' win_regedit: - key: HKCU:\Software\MyCompany - value: hello + path: HKCU:\Software\MyCompany + name: hello data: world -- name: Create Registry Key called MyCompany, a value within MyCompany Key called "hello", and data for the value "hello" containing "1337" as type "dword". +- name: Add or update registry path MyCompany, with entry 'hello', and containing 1337 win_regedit: - key: HKCU:\Software\MyCompany - value: hello + path: HKCU:\Software\MyCompany + name: hello data: 1337 - datatype: dword + type: dword -- name: Create Registry Key called MyCompany, a value within MyCompany Key called "hello", and binary data for the value "hello" as type "binary" data expressed as comma separated list +- name: Add or update registry path MyCompany, with entry 'hello', and containing binary data in hex-string format win_regedit: - key: HKCU:\Software\MyCompany - value: hello + path: HKCU:\Software\MyCompany + name: hello data: hex:be,ef,be,ef,be,ef,be,ef,be,ef - datatype: binary + type: binary -- name: Create Registry Key called MyCompany, a value within MyCompany Key called "hello", and binary data for the value "hello" as type "binary" data expressed as yaml array of bytes +- name: Add or update registry path MyCompany, with entry 'hello', and containing binary data in yaml format win_regedit: - key: HKCU:\Software\MyCompany - value: hello + path: HKCU:\Software\MyCompany + name: hello data: [0xbe,0xef,0xbe,0xef,0xbe,0xef,0xbe,0xef,0xbe,0xef] - datatype: binary + type: binary -- name: Delete Registry Key MyCompany. Not specifying a value will delete the root key which means all values will be deleted +- name: Disable keyboard layout hotkey for all users (changes existing) win_regedit: - key: HKCU:\Software\MyCompany - state: absent + path: HKU:\.DEFAULT\Keyboard Layout\Toggle + name: Layout Hotkey + data: 3 + type: dword -- name: Delete Registry Value "hello" from MyCompany Key +- name: Disable language hotkey for current users (adds new) win_regedit: - key: HKCU:\Software\MyCompany - value: hello + 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 -- name: Creates Registry Key called 'My Company' +- name: Remove entry 'hello' from registry path MyCompany win_regedit: - key: HKCU:\Software\My Company + path: HKCU:\Software\MyCompany + name: hello + state: absent ''' RETURN = r'''