Simple Text edits

Problem: doing minor non-invasive edits

There is currently no way to simply describe various small modifications on the galores of minor configuration files.

Solution

Here are several definitions to handle localized changes to configuration files.


Ensure that the line “line” exists in “file”: (note that grep -qFx is very literal about matching entire lines)

define line($file, $line, $ensure = 'present') {
    case $ensure {
        default : { err ( "unknown ensure value ${ensure}" ) }
        present: {
            exec { "/bin/echo '${line}' >> '${file}'":
                unless => "/bin/grep -qFx '${line}' '${file}'"
            }
        }
        absent: {
            exec { "/bin/grep -vFx '${line}' '${file}' | /usr/bin/tee '${file}' > /dev/null 2>&1":
              onlyif => "/bin/grep -qFx '${line}' '${file}'"
            }

            # Use this resource instead if your platform's grep doesn't support -vFx;
            # note that this command has been known to have problems with lines containing quotes.
            # exec { "/usr/bin/perl -ni -e 'print unless /^\\Q${line}\\E\$/' '${file}'":
            #     onlyif => "/bin/grep -qFx '${line}' '${file}'"
            # }
        }
    }
}

Usage example: Add ‘dummy’ to the list of automatically loaded modules

file { "/etc/modules": ensure => present, }

line { dummy_module:
    file => "/etc/modules",
    line => "dummy",
}

Usage example: Remove ‘dummy’ from the list of automatically loaded modules

file { "/etc/modules": ensure => present, }

line { dummy_module:
    file => "/etc/modules",
    line => "dummy",
    ensure => absent
}

This can easily adapted to removing lines by patter by removing the \Q and \E from the perl call.

You can also extend the preceding recipe to comment or uncomment lines in a shell script. Add the following anywhere into the case statement. Note that I am using pattern matching, not whole line matching here. Note we need to escape the \ char.

uncomment: {
    exec { "/bin/sed -i -e'/${line}/s/#\+//' ‘${file}’” :
        onlyif => "/bin/grep '${line}' '${file}' | /bin/grep '^#' | /usr/bin/wc -l"
    }
}
comment: {
    exec { "/bin/sed -i -e'/${line}/s/\(.\+\)$/#\1/' ‘${file}’" :
        onlyif => "/usr/bin/test `/bin/grep '${line}' '${file}' | /bin/grep -v '^#' | /usr/bin/wc -l` -ne 0"
    }
}

Usage example (activate logsentry with vixie-cron):

line { "logsentrycron":
    file => "/etc/cron.hourly/logsentry.cron",
    line => "logcheck.sh",
    ensure => uncomment
}

Ensure that a line exists, and put it at the top instead of the bottom:

define prepend_if_no_such_line($file, $line, $refreshonly = 'false') {
    exec { "/usr/bin/perl -p0i -e 's/^/$line\n/;' '$file'":
        unless      => "/bin/grep -Fxqe '$line' '$file'",
        path        => "/bin",
        refreshonly => $refreshonly,
    }
}

Delete lines matching a pattern from a file (using extended POSIX regexp):

define delete_lines($file, $pattern) {
    exec { "/bin/sed -i -r -e '/$pattern/d' $file":
        onlyif => "/bin/grep -E '$pattern' '$file'",
    }
}

Usage example: remove sysctl ‘kernel.printk’ from /etc/sysctl.conf:

delete_lines { suppress_printk:
    file => "/etc/sysctl.conf",
    pattern => "^[[:space:]]*kernel[/.]printk[=[:space:]]+",
}

Replace a perl regexp with a string:

define replace($file, $pattern, $replacement) {
    $pattern_no_slashes = slash_escape($pattern)
    $replacement_no_slashes = slash_escape($replacement)

    exec { "/usr/bin/perl -pi -e 's/$pattern_no_slashes/$replacement_no_slashes/' '$file'":
        onlyif => "/usr/bin/perl -ne 'BEGIN { \$ret = 1; } \$ret = 0 if /$pattern_no_slashes/ && ! /$replacement_no_slashes/ ; END { exit \$ret; }' '$file'",
    }
}

The slash_escape() is defined by putting the following sourcecode into $rubylibdir/puppet/parser/functions/slash_escape.rb. For more details, see Writing Your Own Functions .

# escape slashes in a String
module Puppet::Parser::Functions
    newfunction(:slash_escape, :type => :rvalue) do |args|
        args[0].gsub(/\//, '\\/')
    end
end

Usage example: manage parameters in /etc/munin/munin-node.conf, only disturbing the file when needed

define munin_parameter($ensure) {
    replace { set_munin_node_port:
        file => "/etc/munin/munin-node.conf",
        pattern => "^$name (?!$ensure).*",
        replacement => "$name $ensure # set by puppet",
    }
}

munin_parameter {
    "hostname": ensure => $fqdn;
    "port": ensure => 4950;
}

Attention: This only works for parameters which already exist in the config file. A variation on the line define can be used to ensure that.

Discussion

I’m still not satisfied with using perl in the last definition, but no other tool has the “(?!pattern)” negative look-ahead to do the onlyif condition in one command. and I still prefer starting a perl process to forking of several subprocesses which pipe around potentially big files. — David Schmitt?


None of the ‘grep’ flags in this example work with Solaris' grep. You can use GNU ‘ggrep’ if you have it, but that’s not very commonly installed in Solaris. Instead, I removed all grep flags as follows:

define line($file, $line, $ensure = 'present') {
    case $ensure {
        default: { err ( "unknown ensure value ${ensure}" ) }
        present: {
            exec { "/bin/echo '${line}' >> '${file}'":
                unless => "/bin/grep '${line}' '${file}'"
            }
        }
        absent: {
            exec { "/usr/bin/perl -ni -e 'print unless /^\\Q${line}\\E\$/' '${file}'":
                onlyif => "/bin/grep '${line}' '${file}'"
            }
        }
    }
}

—Brad Bender


Replace the rest of a line given the beginning of the line (note that you need GNU sed aka gsed)

define ensure_key_value($file, $key, $value, $delimiter = " ") {
    # append line if "$key" not in "$file"
    exec { "append $key$delimiter$value $file":
        command => "echo '$key$delimiter$value' >> $file",
        unless => "grep -qe '^[[:space:]]*$key[[:space:]]*$delimiter' -- $file",
        path => "/bin:/usr/bin:/usr/local/bin",
        before => Exec["update $key$delimiter$value $file"],
    }

    # update it if it already exists...
    exec { "update $key$delimiter$value $file":
        command => "sed --in-place='' --expression='s/^[[:space:]]*$key[[:space:]]*$delimiter.*$/$key$delimiter$value/g' $file",
        unless => "grep -xqe '$key$delimiter$value' -- $file",
        path => "/bin:/usr/bin:/usr/local/bin"
    }
}

Instead of using Unix based grep, Perl 5 has a Grep function that can be used as well for the above scripts.

— Kevin