From cb78262c1864a3e24adab03eee663e3205185511 Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Fri, 24 Feb 2017 08:29:11 +0100 Subject: [PATCH] win_lineinfile: Clean up and check-mode support (#21503) * win_lineinfile: Clean up and check-mode and diff support Changes include: - Use Get-AnsibleParam with -type support - Replace $result PSObject with normal hash - Remove trailing semi-colons - Fix indentation (majority is tabs, few lines using spaces) - Add check-mode support - Support `r and `n for CR and LF - Add diff support * Implement -WhatIf:$check_mode support * Keep original formatting as requested --- .../modules/windows/win_lineinfile.ps1 | 327 +++++++++--------- 1 file changed, 155 insertions(+), 172 deletions(-) diff --git a/lib/ansible/modules/windows/win_lineinfile.ps1 b/lib/ansible/modules/windows/win_lineinfile.ps1 index d133a828084..a155acc4e61 100644 --- a/lib/ansible/modules/windows/win_lineinfile.ps1 +++ b/lib/ansible/modules/windows/win_lineinfile.ps1 @@ -18,68 +18,22 @@ # POWERSHELL_COMMON -# Parse the parameters file dropped by the Ansible machinery - -$params = Parse-Args $args; - - -# Initialize defaults for input parameters. - -$path= Get-Attr $params "path" $FALSE; -$regexp = Get-Attr $params "regexp" $FALSE; -$state = Get-Attr $params "state" "present"; -$line = Get-Attr $params "line" $FALSE; -$backrefs = Get-Attr -obj $params -name "backrefs" -default "no" -type "bool" -$insertafter = Get-Attr $params "insertafter" $FALSE; -$insertbefore = Get-Attr $params "insertbefore" $FALSE; -$create = Get-Attr $params -name "create" -default "no" -type "bool"; -$backup = Get-Attr $params -name "backup" -default "no" -type "bool"; -$validate = Get-Attr $params "validate" $FALSE; -$encoding = Get-Attr $params "encoding" "auto"; -$newline = Get-Attr $params "newline" "windows"; - - -# Parse path / dest / destfile / name param aliases for compatibility with lineinfile -# and fail if at least one spelling of the parameter is not provided. - -If ($path -eq $FALSE) { - $path = Get-Attr $params "dest" $FALSE; - If ($path -eq $FALSE) { - $path = Get-Attr $params "destfile" $FALSE; - If ($path -eq $FALSE) { - $path = Get-Attr $params "name" $FALSE; - If ($path -eq $FALSE) { - Fail-Json (New-Object psobject) "missing required argument: path"; - } - } - } -} - - -# Fail if the path is not a file - -If (Test-Path $path -pathType container) { - Fail-Json (New-Object psobject) "Path $path is a directory"; -} - - # Write lines to a file using the specified line separator and encoding, # performing validation if a validation command was specified. - -function WriteLines($outlines, $path, $linesep, $encodingobj, $validate) { +function WriteLines($outlines, $path, $linesep, $encodingobj, $validate, $check_mode) { Try { $temppath = [System.IO.Path]::GetTempFileName(); } Catch { - Fail-Json ("Cannot create temporary file! (" + $_.Exception.Message + ")") + Fail-Json @{} "Cannot create temporary file! ($($_.Exception.Message))"; } $joined = $outlines -join $linesep; [System.IO.File]::WriteAllText($temppath, $joined, $encodingobj); - If ($validate -ne $FALSE) { + If ($validate) { - If (!($validate -like "*%s*")) { - Fail-Json (New-Object psobject) "validate must contain %s: $validate"; + If (-not ($validate -like "*%s*")) { + Fail-Json @{} "validate must contain %s: $validate"; } $validate = $validate.Replace("%s", $temppath); @@ -96,7 +50,7 @@ function WriteLines($outlines, $path, $linesep, $encodingobj, $validate) { [string] $output = $process.StandardOutput.ReadToEnd(); [string] $error = $process.StandardError.ReadToEnd(); Remove-Item $temppath -force; - Fail-Json (New-Object psobject) "failed to validate $cmdname $cmdargs with error: $output $error"; + Fail-Json @{} "failed to validate $cmdname $cmdargs with error: $output $error"; } } @@ -104,35 +58,39 @@ function WriteLines($outlines, $path, $linesep, $encodingobj, $validate) { # Commit changes to the path $cleanpath = $path.Replace("/", "\"); Try { - Copy-Item $temppath $cleanpath -force -ErrorAction Stop; + Copy-Item $temppath $cleanpath -force -ErrorAction Stop -WhatIf:$check_mode; } Catch { - Fail-Json ("Cannot write to: $cleanpath (" + $_.Exception.Message + ")") + Fail-Json @{} "Cannot write to: $cleanpath ($($_.Exception.Message))"; } Try { - Remove-Item $temppath -force -ErrorAction Stop; + Remove-Item $temppath -force -ErrorAction Stop -WhatIf:$check_mode; } Catch { - Fail-Json ("Cannot remove temporary file: $temppath (" + $_.Exception.Message + ")") + Fail-Json @{} "Cannot remove temporary file: $temppath ($($_.Exception.Message))"; } + return $joined; + } # Backup the file specified with a date/time filename - -function BackupFile($path) { +function BackupFile($path, $check_mode) { $backuppath = $path + "." + [DateTime]::Now.ToString("yyyyMMdd-HHmmss"); - Copy-Item $path $backuppath; + Try { + Copy-Item $path $backuppath -WhatIf:$check_mode; + } + Catch { + Fail-Json @{} "Cannot copy backup file! ($($_.Exception.Message))"; + } return $backuppath; } - # Implement the functionality for state == 'present' - -function Present($path, $regexp, $line, $insertafter, $insertbefore, $create, $backup, $backrefs, $validate, $encodingobj, $linesep) { +function Present($path, $regexp, $line, $insertafter, $insertbefore, $create, $backup, $backrefs, $validate, $encodingobj, $linesep, $check_mode, $diff_support) { # Note that we have to clean up the path because ansible wants to treat / and \ as # interchangeable in windows pathnames, but .NET framework internals do not support that. @@ -140,51 +98,62 @@ function Present($path, $regexp, $line, $insertafter, $insertbefore, $create, $b # Check if path exists. If it does not exist, either create it if create == "yes" # was specified or fail with a reasonable error message. - If (!(Test-Path $path)) { + If (-not (Test-Path -Path $path)) { If (-not $create) { - Fail-Json (New-Object psobject) "Path $path does not exist !"; + Fail-Json @{} "Path $path does not exist !"; } # Create new empty file, using the specified encoding to write correct BOM [System.IO.File]::WriteAllLines($cleanpath, "", $encodingobj); } + # Initialize result information + $result = @{ + backup = ""; + changed = $false; + msg = ""; + } + # Read the dest file lines using the indicated encoding into a mutable ArrayList. - $content = [System.IO.File]::ReadAllLines($cleanpath, $encodingobj); - If ($content -eq $null) { + $before = [System.IO.File]::ReadAllLines($cleanpath, $encodingobj) + If ($before -eq $null) { $lines = New-Object System.Collections.ArrayList; } Else { - $lines = [System.Collections.ArrayList] $content; + $lines = [System.Collections.ArrayList] $before; + } + + if ($diff_support) { + $result.diff = @{ + before = $before -join $linesep; + } } # Compile the regex specified, if provided - $mre = $FALSE; - If ($regexp -ne $FALSE) { + $mre = $null; + If ($regexp) { $mre = New-Object Regex $regexp, 'Compiled'; } # Compile the regex for insertafter or insertbefore, if provided - $insre = $FALSE; - - If ($insertafter -ne $FALSE -and $insertafter -ne "BOF" -and $insertafter -ne "EOF") { + $insre = $null; + If ($insertafter -and $insertafter -ne "BOF" -and $insertafter -ne "EOF") { $insre = New-Object Regex $insertafter, 'Compiled'; } - ElseIf ($insertbefore -ne $FALSE -and $insertbefore -ne "BOF") { + ElseIf ($insertbefore -and $insertbefore -ne "BOF") { $insre = New-Object Regex $insertbefore, 'Compiled'; } - # index[0] is the line num where regexp has been found - # index[1] is the line num where insertafter/inserbefore has been found + # index[0] is the line num where regexp has been found + # index[1] is the line num where insertafter/inserbefore has been found $index = -1, -1; $lineno = 0; # The latest match object and matched line $matched_line = ""; - $m = $FALSE; # Iterate through the lines in the file looking for matches Foreach ($cur_line in $lines) { - If ($regexp -ne $FALSE) { + If ($regexp) { $m = $mre.Match($cur_line); $match_found = $m.Success; If ($match_found) { @@ -197,20 +166,17 @@ function Present($path, $regexp, $line, $insertafter, $insertbefore, $create, $b If ($match_found) { $index[0] = $lineno; } - ElseIf ($insre -ne $FALSE -and $insre.Match($cur_line).Success) { - If ($insertafter -ne $FALSE) { + ElseIf ($insre -and $insre.Match($cur_line).Success) { + If ($insertafter) { $index[1] = $lineno + 1; } - If ($insertbefore -ne $FALSE) { + If ($insertbefore) { $index[1] = $lineno; } } $lineno = $lineno + 1; } - $changed = $FALSE; - $msg = ""; - If ($index[0] -ne -1) { If ($backrefs) { $new_line = [regex]::Replace($matched_line, $regexp, $line); @@ -220,8 +186,8 @@ function Present($path, $regexp, $line, $insertafter, $insertbefore, $create, $b } If ($lines[$index[0]] -cne $new_line) { $lines[$index[0]] = $new_line; - $msg = "line replaced"; - $changed = $TRUE; + $result.changed = $true; + $result.msg = "line replaced"; } } ElseIf ($backrefs) { @@ -229,83 +195,85 @@ function Present($path, $regexp, $line, $insertafter, $insertbefore, $create, $b } ElseIf ($insertbefore -eq "BOF" -or $insertafter -eq "BOF") { $lines.Insert(0, $line); - $msg = "line added"; - $changed = $TRUE; + $result.changed = $true; + $result.msg = "line added"; } ElseIf ($insertafter -eq "EOF" -or $index[1] -eq -1) { $lines.Add($line); - $msg = "line added"; - $changed = $TRUE; + $result.changed = $true; + $result.msg = "line added"; } Else { $lines.Insert($index[1], $line); - $msg = "line added"; - $changed = $TRUE; - } - - # Write backup file if backup == "yes" - $backuppath = ""; - - If ($changed -eq $TRUE -and $backup -eq $TRUE) { - $backuppath = BackupFile $path; + $result.changed = $true; + $result.msg = "line added"; } # Write changes to the path if changes were made - If ($changed) { - WriteLines $lines $path $linesep $encodingobj $validate; + If ($result.changed) { + + # Write backup file if backup == "yes" + If ($backup) { + $result.backup = BackupFile $path $check_mode; + } + + $after = WriteLines $lines $path $linesep $encodingobj $validate $check_mode; + + if ($diff_support) { + $result.diff.after = $after; + } } - $encodingstr = $encodingobj.WebName; - - # Return result information - $result = New-Object psobject @{ - changed = $changed - msg = $msg - backup = $backuppath - encoding = $encodingstr - } + $result.encoding = $encodingobj.WebName; Exit-Json $result; } # Implement the functionality for state == 'absent' - -function Absent($path, $regexp, $line, $backup, $validate, $encodingobj, $linesep) { +function Absent($path, $regexp, $line, $backup, $validate, $encodingobj, $linesep, $check_mode, $diff_support) { # Check if path exists. If it does not exist, fail with a reasonable error message. - If (!(Test-Path $path)) { - Fail-Json (New-Object psobject) "Path $path does not exist !"; + If (-not (Test-Path -Path $path)) { + Fail-Json @{} "Path $path does not exist !"; + } + + # Initialize result information + $result = @{ + backup = ""; + changed = $false; + msg = ""; } # Read the dest file lines using the indicated encoding into a mutable ArrayList. Note # that we have to clean up the path because ansible wants to treat / and \ as # interchangeable in windows pathnames, but .NET framework internals do not support that. - $cleanpath = $path.Replace("/", "\"); - $content = [System.IO.File]::ReadAllLines($cleanpath, $encodingobj); - If ($content -eq $null) { + $before = [System.IO.File]::ReadAllLines($cleanpath, $encodingobj); + If ($before -eq $null) { $lines = New-Object System.Collections.ArrayList; } Else { - $lines = [System.Collections.ArrayList] $content; + $lines = [System.Collections.ArrayList] $before; } - # Initialize message to be returned on success - $msg = ""; + if ($diff_support) { + $result.diff = @{ + before = $before -join $linesep; + } + } # Compile the regex specified, if provided - $cre = $FALSE; - If ($regexp -ne $FALSE) { + $cre = $null; + If ($regexp) { $cre = New-Object Regex $regexp, 'Compiled'; } $found = New-Object System.Collections.ArrayList; $left = New-Object System.Collections.ArrayList; - $changed = $FALSE; Foreach ($cur_line in $lines) { - If ($cre -ne $FALSE) { + If ($regexp) { $m = $cre.Match($cur_line); $match_found = $m.Success; } @@ -314,64 +282,79 @@ function Absent($path, $regexp, $line, $backup, $validate, $encodingobj, $linese } If ($match_found) { $found.Add($cur_line); - $changed = $TRUE; + $result.changed = $true; } Else { $left.Add($cur_line); } } - # Write backup file if backup == "yes" - $backuppath = ""; - - If ($changed -eq $TRUE -and $backup -eq $TRUE) { - $backuppath = BackupFile $path; - } - # Write changes to the path if changes were made - If ($changed) { - WriteLines $left $path $linesep $encodingobj $validate; + If ($result.changed) { + + # Write backup file if backup == "yes" + If ($backup) { + $result.backup = BackupFile $path $check_mode; + } + + $after = WriteLines $left $path $linesep $encodingobj $validate $check_mode; + + if ($diff_support) { + $result.diff.after = $after; + } } - # Return result information - $fcount = $found.Count; - $msg = "$fcount line(s) removed"; - $encodingstr = $encodingobj.WebName; - - $result = New-Object psobject @{ - changed = $changed - msg = $msg - backup = $backuppath - found = $fcount - encoding = $encodingstr - } + $result.encoding = $encodingobj.WebName; + $result.found = $found.Count; + $result.msg = "$($found.Count) line(s) removed"; Exit-Json $result; } +# Parse the parameters file dropped by the Ansible machinery +$params = Parse-Args $args -supports_check_mode $true; +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false; +$diff_support = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false; + +# Initialize defaults for input parameters. +$path = Get-AnsibleParam -obj $params -name "path" -type "path" -failifempty $true -aliases "dest","destfile","name"; +$regexp = Get-AnsibleParam -obj $params -name "regexp" -type "str"; +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present","absent"; +$line = Get-AnsibleParam -obj $params -name "line" -type "str"; +$backrefs = Get-AnsibleParam -obj $params -name "backrefs" -type "bool" -default $false; +$insertafter = Get-AnsibleParam -obj $params -name "insertafter" -type "str"; +$insertbefore = Get-AnsibleParam -obj $params -name "insertbefore" -type "str"; +$create = Get-AnsibleParam -obj $params -name "create" -type "bool" -default $false; +$backup = Get-AnsibleParam -obj $params -name "backup" -type "bool" -default $false; +$validate = Get-AnsibleParam -obj $params -name "validate" -type "str"; +$encoding = Get-AnsibleParam -obj $params -name "encoding" -type "str" -default "auto"; +$newline = Get-AnsibleParam -obj $params -name "newline" -type "str" -default "windows" -validateset "unix","windows"; + +# Fail if the path is not a file +If (Test-Path -Path $path -PathType "container") { + Fail-Json @{} "Path $path is a directory"; +} + # Default to windows line separator - probably most common - -$linesep = "`r`n"; - -If ($newline -ne "windows") { +$linesep = "`r`n" +If ($newline -eq "unix") { $linesep = "`n"; } - # Fix any CR/LF literals in the line argument. PS will not recognize either backslash # or backtick literals in the incoming string argument without this bit of black magic. - -If ($line -ne $FALSE) { +If ($line) { $line = $line.Replace("\r", "`r"); $line = $line.Replace("\n", "`n"); + $line = $line.Replace("``r", "`r"); + $line = $line.Replace("``n", "`n"); } - # Figure out the proper encoding to use for reading / writing the target file. # The default encoding is UTF-8 without BOM -$encodingobj = [System.Text.UTF8Encoding] $FALSE; +$encodingobj = [System.Text.UTF8Encoding] $false; # If an explicit encoding is specified, use that instead If ($encoding -ne "auto") { @@ -381,10 +364,9 @@ If ($encoding -ne "auto") { # Otherwise see if we can determine the current encoding of the target file. # If the file doesn't exist yet (create == 'yes') we use the default or # explicitly specified encoding set above. -Elseif (Test-Path $path) { +ElseIf (Test-Path -Path $path) { # Get a sorted list of encodings with preambles, longest first - $max_preamble_len = 0; $sortedlist = New-Object System.Collections.SortedList; Foreach ($encodinginfo in [System.Text.Encoding]::GetEncodings()) { @@ -399,12 +381,10 @@ Elseif (Test-Path $path) { } # Get the first N bytes from the file, where N is the max preamble length we saw - [Byte[]]$bom = Get-Content -Encoding Byte -ReadCount $max_preamble_len -TotalCount $max_preamble_len -Path $path; # Iterate through the sorted encodings, looking for a full match. - - $found = $FALSE; + $found = $false; Foreach ($encoding in $sortedlist.GetValueList()) { $preamble = $encoding.GetPreamble(); If ($preamble -and $bom) { @@ -415,9 +395,9 @@ Elseif (Test-Path $path) { If ($preamble[$i] -ne $bom[$i]) { break; } - Elseif ($i + 1 -eq $preamble.Length) { + ElseIf ($i + 1 -eq $preamble.Length) { $encodingobj = $encoding; - $found = $TRUE; + $found = $true; } } If ($found) { @@ -430,29 +410,32 @@ Elseif (Test-Path $path) { # Main dispatch - based on the value of 'state', perform argument validation and # call the appropriate handler function. - If ($state -eq "present") { - If ( $backrefs -and $regexp -eq $FALSE ) { - Fail-Json (New-Object psobject) "regexp= is required with backrefs=true"; + If ($backrefs -and -not $regexp) { + Fail-Json @{} "regexp= is required with backrefs=true"; } - If ($line -eq $FALSE) { - Fail-Json (New-Object psobject) "line= is required with state=present"; + If (-not $line) { + Fail-Json @{} "line= is required with state=present"; } - If ($insertbefore -eq $FALSE -and $insertafter -eq $FALSE) { + If ($insertbefore -and $insertafter) { + Add-Warning('Both insertbefore and insertafter parameters found, ignoring "insertafter=$insertafter"'); + } + + If (-not $insertbefore -and -not $insertafter) { $insertafter = "EOF"; } - Present $path $regexp $line $insertafter $insertbefore $create $backup $backrefs $validate $encodingobj $linesep; + Present $path $regexp $line $insertafter $insertbefore $create $backup $backrefs $validate $encodingobj $linesep $check_mode $diff_support; } -Else { +ElseIf ($state -eq "absent") { - If ($regexp -eq $FALSE -and $line -eq $FALSE) { - Fail-Json (New-Object psobject) "one of line= or regexp= is required with state=absent"; + If (-not $regexp -and -not $line) { + Fail-Json @{} "one of line= or regexp= is required with state=absent"; } - Absent $path $regexp $line $backup $validate $encodingobj $linesep; + Absent $path $regexp $line $backup $validate $encodingobj $linesep $check_mode $diff_support; }