From e6ffb6085517481153609927bc3d99e6561ad97f Mon Sep 17 00:00:00 2001 From: Brian Lloyd Date: Wed, 29 Jul 2015 19:38:15 -0400 Subject: [PATCH] Windows implementation of lineinfile and related documentation --- .../modules/windows/win_lineinfile.ps1 | 452 ++++++++++++++++++ lib/ansible/modules/windows/win_lineinfile.py | 155 ++++++ 2 files changed, 607 insertions(+) create mode 100644 lib/ansible/modules/windows/win_lineinfile.ps1 create mode 100644 lib/ansible/modules/windows/win_lineinfile.py diff --git a/lib/ansible/modules/windows/win_lineinfile.ps1 b/lib/ansible/modules/windows/win_lineinfile.ps1 new file mode 100644 index 00000000000..ddf1d4e3000 --- /dev/null +++ b/lib/ansible/modules/windows/win_lineinfile.ps1 @@ -0,0 +1,452 @@ +#!powershell +# 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 . + +# WANT_JSON +# POWERSHELL_COMMON + + +# Parse the parameters file dropped by the Ansible machinery + +$params = Parse-Args $args; + + +# Initialize defaults for input parameters. + +$dest= Get-Attr $params "dest" $FALSE; +$regexp = Get-Attr $params "regexp" $FALSE; +$state = Get-Attr $params "state" "present"; +$line = Get-Attr $params "line" $FALSE; +$backrefs = Get-Attr $params "backrefs" "no"; +$insertafter = Get-Attr $params "insertafter" $FALSE; +$insertbefore = Get-Attr $params "insertbefore" $FALSE; +$create = Get-Attr $params "create" "no"; +$backup = Get-Attr $params "backup" "no"; +$validate = Get-Attr $params "validate" $FALSE; +$encoding = Get-Attr $params "encoding" "auto"; +$newline = Get-Attr $params "newline" "windows"; + + +# Parse dest / name /destfile param aliases for compatibility with lineinfile +# and fail if at least one spelling of the parameter is not provided. + +$dest = Get-Attr $params "dest" $FALSE; +If ($dest -eq $FALSE) { + $dest = Get-Attr $params "name" $FALSE; + If ($dest -eq $FALSE) { + $dest = Get-Attr $params "destfile" $FALSE; + If ($dest -eq $FALSE) { + Fail-Json (New-Object psobject) "missing required argument: dest"; + } + } +} + + +# Fail if the destination is not a file + +If (Test-Path $dest -pathType container) { + Fail-Json (New-Object psobject) "destination 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, $dest, $linesep, $encodingobj, $validate) { + $temppath = [System.IO.Path]::GetTempFileName(); + $joined = $outlines -join $linesep; + [System.IO.File]::WriteAllText($temppath, $joined, $encodingobj); + + If ($validate -ne $FALSE) { + + If (!($validate -like "*%s*")) { + Fail-Json (New-Object psobject) "validate must contain %s: $validate"; + } + + $validate = $validate.Replace("%s", $temppath); + + $parts = [System.Collections.ArrayList] $validate.Split(" "); + $cmdname = $parts[0]; + + $cmdargs = $validate.Substring($cmdname.Length + 1); + + $process = [Diagnostics.Process]::Start($cmdname, $cmdargs); + $process.WaitForExit(); + + If ($process.ExitCode -ne 0) { + [string] $output = $process.StandardOutput.ReadToEnd(); + [string] $error = $process.StandardError.ReadToEnd(); + Remove-Item $temppath -force; + Fail-Json (New-Object psobject) "failed to validate $cmdname $cmdargs with error: $output $error"; + } + + } + + # Commit changes to the destination file + $cleandest = $dest.Replace("/", "\"); + Move-Item $temppath $cleandest -force; +} + + +# Backup the file specified with a date/time filename + +function BackupFile($path) { + $backuppath = $path + "." + [DateTime]::Now.ToString("yyyyMMdd-HHmmss"); + Copy-Item $path $backuppath; + return $backuppath; +} + + + +# Implement the functionality for state == 'present' + +function Present($dest, $regexp, $line, $insertafter, $insertbefore, $create, $backup, $backrefs, $validate, $encodingobj, $linesep) { + + # Note that we have to clean up the dest path because ansible wants to treat / and \ as + # interchangable in windows pathnames, but .NET framework internals do not support that. + $cleandest = $dest.Replace("/", "\"); + + # Check if destination exists. If it does not exist, either create it if create == "yes" + # was specified or fail with a reasonable error message. + If (!(Test-Path $dest)) { + If ($create -eq "no") { + Fail-Json (New-Object psobject) "Destination $dest does not exist !"; + } + # Create new empty file, using the specified encoding to write correct BOM + [System.IO.File]::WriteAllLines($cleandest, "", $encodingobj); + } + + # Read the dest file lines using the indicated encoding into a mutable ArrayList. + $content = [System.IO.File]::ReadAllLines($cleandest, $encodingobj); + If ($content -eq $null) { + $lines = New-Object System.Collections.ArrayList; + } + Else { + $lines = [System.Collections.ArrayList] $content; + } + + # Compile the regex specified, if provided + $mre = $FALSE; + If ($regexp -ne $FALSE) { + $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 = New-Object Regex $insertafter, 'Compiled'; + } + ElseIf ($insertbefore -ne $FALSE -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 = -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) { + $m = $mre.Match($cur_line); + $match_found = $m.Success; + If ($match_found) { + $matched_line = $cur_line; + } + } + Else { + $match_found = $line -ceq $cur_line; + } + If ($match_found) { + $index[0] = $lineno; + } + ElseIf ($insre -ne $FALSE -and $insre.Match($cur_line).Success) { + If ($insertafter -ne $FALSE) { + $index[1] = $lineno + 1; + } + If ($insertbefore -ne $FALSE) { + $index[1] = $lineno; + } + } + $lineno = $lineno + 1; + } + + $changed = $FALSE; + $msg = ""; + + If ($index[0] -ne -1) { + If ($backrefs -ne "no") { + $new_line = [regex]::Replace($matched_line, $regexp, $line); + } + Else { + $new_line = $line; + } + If ($lines[$index[0]] -cne $new_line) { + $lines[$index[0]] = $new_line; + $msg = "line replaced"; + $changed = $TRUE; + } + } + ElseIf ($backrefs -ne "no") { + # No matches - no-op + } + ElseIf ($insertbefore -eq "BOF" -or $insertafter -eq "BOF") { + $lines.Insert(0, $line); + $msg = "line added"; + $changed = $TRUE; + } + ElseIf ($insertafter -eq "EOF" -or $index[1] -eq -1) { + $lines.Add($line); + $msg = "line added"; + $changed = $TRUE; + } + Else { + $lines.Insert($index[1], $line); + $msg = "line added"; + $changed = $TRUE; + } + + # Write backup file if backup == "yes" + $backupdest = ""; + + If ($changed -eq $TRUE -and $backup -eq "yes") { + $backupdest = BackupFile $dest; + } + + # Write changes to the destination file if changes were made + If ($changed) { + WriteLines $lines $dest $linesep $encodingobj $validate; + } + + $encodingstr = $encodingobj.WebName; + + # Return result information + $result = New-Object psobject @{ + changed = $changed + msg = $msg + backup = $backupdest + encoding = $encodingstr + } + + Exit-Json $result; +} + + +# Implement the functionality for state == 'absent' + +function Absent($dest, $regexp, $line, $backup, $validate, $encodingobj, $linesep) { + + # Check if destination exists. If it does not exist, fail with a reasonable error message. + If (!(Test-Path $dest)) { + Fail-Json (New-Object psobject) "Destination $dest does not exist !"; + } + + # Read the dest file lines using the indicated encoding into a mutable ArrayList. Note + # that we have to clean up the dest path because ansible wants to treat / and \ as + # interchangeable in windows pathnames, but .NET framework internals do not support that. + + $cleandest = $dest.Replace("/", "\"); + $content = [System.IO.File]::ReadAllLines($cleandest, $encodingobj); + If ($content -eq $null) { + $lines = New-Object System.Collections.ArrayList; + } + Else { + $lines = [System.Collections.ArrayList] $content; + } + + # Initialize message to be returned on success + $msg = ""; + + # Compile the regex specified, if provided + $cre = $FALSE; + If ($regexp -ne $FALSE) { + $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) { + $m = $cre.Match($cur_line); + $match_found = $m.Success; + } + Else { + $match_found = $line -ceq $cur_line; + } + If ($match_found) { + $found.Add($cur_line); + $changed = $TRUE; + } + Else { + $left.Add($cur_line); + } + } + + # Write backup file if backup == "yes" + $backupdest = ""; + + If ($changed -eq $TRUE -and $backup -eq "yes") { + $backupdest = BackupFile $dest; + } + + # Write changes to the destination file if changes were made + If ($changed) { + WriteLines $left $dest $linesep $encodingobj $validate; + } + + # Return result information + $fcount = $found.Count; + $msg = "$fcount line(s) removed"; + $encodingstr = $encodingobj.WebName; + + $result = New-Object psobject @{ + changed = $changed + msg = $msg + backup = $backupdest + found = $fcount + encoding = $encodingstr + } + + Exit-Json $result; +} + + +# Default to windows line separator - probably most common + +$linesep = "`r`n"; + +If ($newline -ne "windows") { + $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) { + $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; + +# If an explicit encoding is specified, use that instead +If ($encoding -ne "auto") { + $encodingobj = [System.Text.Encoding]::GetEncoding($encoding); +} + +# Otherwise see if we can determine the current encoding of the target file. +# If the file doesn't exist yet (create == 'yes') we use the default or +# explicitly specified encoding set above. +Elseif (Test-Path $dest) { + + # Get a sorted list of encodings with preambles, longest first + + $max_preamble_len = 0; + $sortedlist = New-Object System.Collections.SortedList; + Foreach ($encodinginfo in [System.Text.Encoding]::GetEncodings()) { + $encoding = $encodinginfo.GetEncoding(); + $plen = $encoding.GetPreamble().Length; + If ($plen -gt $max_preamble_len) { + $max_preamble_len = $plen; + } + If ($plen -gt 0) { + $sortedlist.Add(-($plen * 1000000 + $encoding.CodePage), $encoding); + } + } + + # 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 $dest; + + # Iterate through the sorted encodings, looking for a full match. + + $found = $FALSE; + Foreach ($encoding in $sortedlist.GetValueList()) { + $preamble = $encoding.GetPreamble(); + If ($preamble) { + Foreach ($i in 0..$preamble.Length) { + If ($preamble[$i] -ne $bom[$i]) { + break; + } + Elseif ($i + 1 -eq $preamble.Length) { + $encodingobj = $encoding; + $found = $TRUE; + } + } + If ($found) { + break; + } + } + } +} + + +# Main dispatch - based on the value of 'state', perform argument validation and +# call the appropriate handler function. + +If ($state -eq "present") { + + If ( $backrefs -ne "no" -and $regexp -eq $FALSE ) { + Fail-Json (New-Object psobject) "regexp= is required with backrefs=true"; + } + + If ($line -eq $FALSE) { + Fail-Json (New-Object psobject) "line= is required with state=present"; + } + + If ($insertbefore -eq $FALSE -and $insertafter -eq $FALSE) { + $insertafter = "EOF"; + } + + Present $dest $regexp $line $insertafter $insertbefore $create $backup $backrefs $validate $encodingobj $linesep; + +} +Else { + + If ($regex -eq $FALSE -and $line -eq $FALSE) { + Fail-Json (New-Object psobject) "one of line= or regexp= is required with state=absent"; + } + + Absent $dest $regexp $line $backup $validate $encodingobj $linesep; +} + + + + + + + + + + + + + + + + + diff --git a/lib/ansible/modules/windows/win_lineinfile.py b/lib/ansible/modules/windows/win_lineinfile.py new file mode 100644 index 00000000000..6c54fd2bea8 --- /dev/null +++ b/lib/ansible/modules/windows/win_lineinfile.py @@ -0,0 +1,155 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# 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 . + +DOCUMENTATION = """ +--- +module: win_lineinfile +author: Brian Lloyd (brian.d.lloyd@gmail.com) +short_description: Ensure a particular line is in a file, or replace an + existing line using a back-referenced regular expression. +description: + - This module will search a file for a line, and ensure that it is present or absent. + - This is primarily useful when you want to change a single line in + a file only. +version_added: "2.0" +options: + dest: + required: true + aliases: [ name, destfile ] + description: + - The path of the file to modify. + regexp: + required: false + description: + - The regular expression to look for in every line of the file. For + C(state=present), the pattern to replace if found; only the last line + found will be replaced. For C(state=absent), the pattern of the line + to remove. Uses .NET compatible regular expressions; see + U(https://msdn.microsoft.com/en-us/library/hs600312%28v=vs.110%29.aspx). + state: + required: false + choices: [ present, absent ] + default: "present" + description: + - Whether the line should be there or not. + line: + required: false + description: + - Required for C(state=present). The line to insert/replace into the + file. If C(backrefs) is set, may contain backreferences that will get + expanded with the C(regexp) capture groups if the regexp matches. + backrefs: + required: false + default: "no" + choices: [ "yes", "no" ] + description: + - Used with C(state=present). If set, line can contain backreferences + (both positional and named) that will get populated if the C(regexp) + matches. This flag changes the operation of the module slightly; + C(insertbefore) and C(insertafter) will be ignored, and if the C(regexp) + doesn't match anywhere in the file, the file will be left unchanged. + If the C(regexp) does match, the last matching line will be replaced by + the expanded line parameter. + insertafter: + required: false + default: EOF + description: + - Used with C(state=present). If specified, the line will be inserted + after the last match of specified regular expression. A special value is + available; C(EOF) for inserting the line at the end of the file. + If specified regular expresion has no matches, EOF will be used instead. + May not be used with C(backrefs). + choices: [ 'EOF', '*regex*' ] + insertbefore: + required: false + version_added: "1.1" + description: + - Used with C(state=present). If specified, the line will be inserted + before the last match of specified regular expression. A value is + available; C(BOF) for inserting the line at the beginning of the file. + If specified regular expresion has no matches, the line will be + inserted at the end of the file. May not be used with C(backrefs). + choices: [ 'BOF', '*regex*' ] + create: + required: false + choices: [ "yes", "no" ] + default: "no" + description: + - Used with C(state=present). If specified, the file will be created + if it does not already exist. By default it will fail if the file + is missing. + backup: + required: false + default: "no" + choices: [ "yes", "no" ] + description: + - Create a backup file including the timestamp information so you can + get the original file back if you somehow clobbered it incorrectly. + validate: + required: false + description: + - validation to run before copying into place. + Use %s in the command to indicate the current file to validate. + The command is passed securely so shell features like + expansion and pipes won't work. + required: false + default: None + encoding: + required: false + default: "auto" + description: + - Specifies the encoding of the source text file to operate on (and thus what the + output encoding will be). The default of C(auto) will cause the module to auto-detect + the encoding of the source file and ensure that the modified file is written with the + same encoding. + An explicit encoding can be passed as a string that is a valid value to pass to + the .NET framework System.Text.Encoding.GetEncoding() method - see + U(https://msdn.microsoft.com/en-us/library/system.text.encoding%28v=vs.110%29.aspx). + This is mostly useful with C(create=yes) if you want to create a new file with a specific + encoding. If C(create=yes) is specified without a specific encoding, the default encoding + (UTF-8, no BOM) will be used. + newline: + required: false + description: + - Specifies the line separator style to use for the modified file. This defaults + to the windows line separator (\r\n). Note that the indicated line separator + will be used for file output regardless of the original line seperator that + appears in the input file. + choices: [ "windows", "unix" ] + default: "windows" + +""" + +EXAMPLES = r""" +- win_lineinfile: dest=C:\\temp\\example.conf regexp=^name= line="name=JohnDoe" + +- win_lineinfile: dest=C:\\temp\\example.conf state=absent regexp="^name=" + +- win_lineinfile: dest=C:\\temp\\example.conf regexp='^127\.0\.0\.1' line='127.0.0.1 localhost' + +- win_lineinfile: dest=C:\\temp\\httpd.conf regexp="^Listen " insertafter="^#Listen " line="Listen 8080" + +- win_lineinfile: dest=C:\\temp\\services regexp="^# port for http" insertbefore="^www.*80/tcp" line="# port for http by default" + +# Create file if it doesnt exist with a specific encoding +- win_lineinfile: dest=C:\\temp\\utf16.txt create="yes" encoding="utf-16" line="This is a utf-16 encoded file" + +# Add a line to a file and ensure the resulting file uses unix line separators +- win_lineinfile: dest=C:\\temp\\testfile.txt line="Line added to file" newline="unix" + +"""