Puppet woes managing rc, sysctl, and loader

I'm setting up a new server with Puppet to manage the native OS and many jails. I've been having horrible problems trying to automate the management of key/value pairs for rc, sysctl, and loader. I'm wondering if anyone had advice or had been successful managing these key files.

I started with https://web.archive.org/web/2015011...uppetlabs.com/projects/1/wiki/Puppet_Free_Bsd .

First I tried using the shell_config type using grep/sed/etc. That had edge cases for editing keys and values. I either had duplicate keys or a problem with a specific text match of key+value required to manage the setting.

Next I tried the augeas shell_config using the Shellvars lens. This fails on /etc/sysctl.conf, and I've had problems with extra quotes.

Tried using the augeas Sysctl lens, and that adds spaces around equals signs (linuxism?), breaking all three file formats. Augeus is completely undocumented, and I have little faith in it now.

Puppet Forge has incomplete modules which use the same shell_config code. No help there.

Both /etc/rc.conf and /boot/loader.conf have options to use a ".d" directory. Unfortunately /etc/rc.conf.d/ only loads files by service name, so I can't place settings in one file at a time to load globally.

The new sysrc command could set or remove rc values nicely, but fails on sysctl and loader.conf due to .'s in key names. There seems to be no corresponding tools for sysctl.conf or loader.conf.

I also haven't seen a command to validate that rc/sysctl/loader file content.

It shouldn't be this hard to manage persistent key/value pairs.
 
[…] I'm wondering if anyone […] had been successful managing these key files.
I use sysutils/puppet to manage some FreeBSD nodes.​
[…] Tried using the augeas Sysctl lens, and that adds spaces around equals signs (linuxism?) […]
Yeah, sysctl on Linux is OK with that, but it is optional.​
[…] Augeus is completely undocumented, and I have little faith in it now. […]
There is a principle: Do not use undocumented features. If it is not documented, it does not exist. I try to obey to this principle as often as possible (and, side note, it is definitely a reason why I eventually switched to FreeBSD).​
[…] Puppet Forge has incomplete modules […]
I intend to publish some modules, but only as soon as I am confident they are fairly complete and more importantly the documentation is complete (expect another half year). Nothing is worse than undocumented modules. (OK, broke modules are annoying.)​
Unfortunately /etc/rc.conf.d/ only loads files by service name, so I can't place settings in one file at a time to load globally.
I am not sure what you mean. You can aggregate the contents cat /etc/rc.conf.d/* > single.rc.conf? Or what do you want to achieve?​
I also haven't seen a command to validate that rc/sysctl/loader file content.
For my rc.conf(5)s it is is:
Code:
                validate_cmd => '/bin/sh -o noexec %',
 
rc.conf(5): I use plain file resources with parameterized epp templates populating /etc/rc.conf.d/. I deem this the cleanest solution.

Do you have an example of EPP? That looks like a whole new templating language. Oddly enough, rc.conf should be easiest as I can just call sysrc.

sysctl.conf(5): Similar, except that it’s really just /etc/sysctl.conf.

Can't validate this with sh -n, and the dots in the names break sysrc. I'd think it appropriate to call sysctl to set the flags at the same time.

loader.conf(5): There was no need (yet).

Unfortunately I do need to ensure some settings are there, perhaps I could get away with a static file resource.

I am not sure what you mean. You can aggregate the contents cat /etc/rc.conf.d/* > single.rc.conf? Or what do you want to achieve?

My point was that the files in /etc/rc.conf.d are named by an associated service. At boot they are not used, but if you start service sshd it will load /etc/rc.conf.d/sshd. This was opposite my expectation that all files would be used and could contain useful single items aligned with a puppet profile.

Yes, I could write a directory of little files and trigger a cat, but I was trying to stick to BSD norms first if I could.

After discovering sysrc, I'm surprised that sysctl doesn't have the option to write out the parameters passed to systcl.conf or loader.conf.
 
I'm going to try using the Puppet file_line call today, with a match attribute to help ensure there aren't duplicates. Perhaps that will work.
 
Augeas isn't documented? It is, but the augeas documentation on puppet doc is rather poor. Just look on the augeas site itself: https://augeas.net/docs/index.html
Granted, it is rather difficult. It's a very complex tool, with some really powerful features, it took me a lot of reading and experimenting to understand how to use it properly.

I think I have something somewhere for /etc/sysctl.conf using augeas. Will try and find it. It essentially created a resource defined class you can use in other modules and you could simply do something like this:
Code:
sysctl { 'some.sysctl.setting':
  ensure => 'present',
  value => "1234",
}
 
Tried using the augeas Sysctl lens, and that adds spaces around equals signs (linuxism?), breaking all three file formats.
Those space are fine for /etc/sysctl.conf actually.

Code:
root@wintermute:~ # cat /etc/sysctl.conf
net.inet.ip.forwarding = 1
root@wintermute:~ # sysctl -f /etc/sysctl.conf
net.inet.ip.forwarding: 0 -> 1

Anyway, this is what I used; sysctl/manifests/init.pp:
Code:
define sysctl (
    Enum['present', 'absent'] $ensure = 'present',
    String $value = "",
) {

    case $ensure {
        'present': {
            augeas { "sysctl $title":
                lens => 'Sysctl.lns',
                incl => '/etc/sysctl.conf',
                changes => "set ${title} '${value}'",
                onlyif  => "get ${title} != '${value}'",
            }
        }

        'absent': {
            augeas { "sysctl rm ${title}":
                lens => 'Sysctl.lns',
                incl => '/etc/sysctl.conf',
                changes => "rm ${title}",
            }
        }
    }
}
This way you can simply use it like so:
Code:
  sysctl { 'net.inet.ip.forwarding':
    ensure => 'present',
    value  => '1',
  }

Only thing you need to watch out for is trying to define the same sysctl twice, as that will result in a duplicate declaration error.
 
I remember reading somewhere that you're not supposed to use that one, it's for internal use only. Can't find that reference anymore though.

Edit: Apparently not, I've actually used it in some code I wrote:
Code:
      augeas { "DNS${nr}_Settings_${interface}":
        lens    => 'Shellvars.lns',
        incl    => "/etc/sysconfig/network-scripts/ifcfg-${interface}",
        onlyif  => "get DNS${nr} != '${dns}'",
        changes => "set DNS${nr} '${dns}'",
        notify  => Service['NetworkManager'],
      }
Snippet is meant to be used on RedHat. There are probably better ways to solve it but it does demonstrate the use of Shellvars.lns
 
Slight variation on the theme, specifically for rc.conf.

sysrc/manifests/init.pp:
Code:
define sysrc (
    Enum['present', 'absent'] $ensure = 'present',
    String $value = "",
) {

    case $ensure {
        'present': {
            augeas { "sysctl $title":
                lens => 'Shellvars.lns',
                incl => '/etc/rc.conf',
                changes => "set ${title} '${value}'",
                onlyif  => "get ${title} != '${value}'",
            }
        }

        'absent': {
            augeas { "sysctl rm ${title}":
                lens => 'Shellvars.lns',
                incl => '/etc/rc.conf',
                changes => "rm ${title}",
            }
        }
    }
}

Usage is similar to the sysctl class:
Code:
sysrc { 'syslogd_flags':
  ensure => 'present',
  value   => '-ss',
}

Code:
root@wintermute:~ # puppet agent -t -E dev_dice
Info: Using environment 'dev_dice'
Info: Retrieving pluginfacts
Info: Retrieving plugin
Info: Loading facts
Info: Caching catalog for wintermute.dicelan.home
Info: Applying configuration version '1685711778'
Notice: Augeas[sysctl syslogd_flags](provider=augeas):
--- /etc/rc.conf        2023-06-01 20:01:56.373532000 +0200
+++ /etc/rc.conf.augnew 2023-06-02 15:17:32.429500000 +0200
@@ -17,3 +17,4 @@
 zabbix_agentd_enable="YES"
 puppet_enable="YES"
 openntpd_enable="YES"
+syslogd_flags=-ss

Notice: /Stage[main]/Puppet_agent/Sysrc[syslog_flags]/Augeas[sysctl syslog_flags]/returns: executed successfully
Notice: Applied catalog in 73.45 seconds
 
You could probably roll everything into one declaration and use a $type variable to switch the lens and file to change, both bits of code are mostly the same anyway. Something like
Code:
if $type = 'rcconf' {
  $lens = 'Shellvars.lns'
  $sysfile = '/etc/rc.conf'
} elseif $type = 'sysctl' {
  $lens = 'Sysctl.lns'
  $sysfile = '/etc/sysctl.conf'
}

augeas { "$type $value":
  lens => $lens,
  incl => $sysfile,
  changes => "set ${title} '${value}'",
  onlyif  => "get ${title} != '${value}'",
}
Or something similar.

You should also have enough example code here to work out how to do /boot/loader.conf using augeas.
 
You could probably roll everything into one declaration and use a $type variable to switch the lens and file to change, both bits of code are mostly the same anyway. Something like

Did you look at the code in the archive.org link? They have both Augeus and shell implementations just like that. Unfortunately I found edge conditions in the shell code, and the Augueas one had failures on sysctl and loader with Shellvars.

I just got some code working for rc.conf which wraps sysrc nicely. I'll post it later.
 
Did you look at the code in the archive.org link?
I quickly skimmed through it. I wouldn't use portsnap(8) for the ports tree but instead use puppetlabs/vcsrepo and get it through git. But I wouldn't even bother with a ports tree. Stick to puppetlabs/pkgng and my own repository. Then you can simply use the package { ... } resource to install something. There's certainly a lot of exec { ... } happening in that code. That's generally bad puppet code. Try to avoid using exec { ... } as much as you can.

And I have some basic stanza I use:
Code:
class somesoftware (
  String $package_name,
  Boolean $auto_update,
  String $service_name,
  Boolean $service_enabled,
  Stdlib::Absolutepath $config,
  Optional[Stdlib::Host] $server,
) {

  if $auto_update {
    $package_ensure = 'latest'
  } else {
    $package_ensure = 'installed'
  }

  package { 'somesoftware_package':
    ensure => $package_ensure,
    name   => $package_name,
    notify => Service['somesoftware_service']
  }

  file { 'somesoftware_conf':
    ensure  => 'file',
    name    => $config,
    owner   => 'root',
    group   => 'wheel',
    mode    => '0644',
    require => Package['somesoftware_package'],
    notify  => Service['somesoftware_service'],
  }

  if $service_enabled {
    $service_ensure = 'running'
  } else {
    $service_ensure = 'stopped'
  }

  service { 'somesoftware_service':
    ensure => $service_ensure,
    enable => $service_enabled,
    name   => $service_name,
  }
}

Then add a hiera.yaml for some of the data, you can make those depend on various facts.
Code:
---
version: 5

hierarchy:
  - name: "osfamily/major release"
    paths:
        # Used to distinguish between Debian and Ubuntu
      - "os/%{facts.os.name}/%{facts.os.release.major}.yaml"
      - "os/%{facts.os.family}/%{facts.os.release.major}.yaml"
        # Used for Solaris
      - "os/%{facts.os.family}/%{facts.kernelrelease}.yaml"
  - name: "osfamily"
    paths:
      - "os/%{facts.os.name}.yaml"
      - "os/%{facts.os.family}.yaml"
  - name: "Common data"
    path: "common.yaml"

Now you can create a common.yaml with some of the data that's 'common' and a os/FreeBSD.yaml with FreeBSD specific values, for example:
Code:
---
somesoftware::package_name: 'somesoftware'
somesoftware::service_name: 'theservice'
somesoftware::config: '/usr/local/etc/somesoftware/myconfig.conf'

That way you can easily adjust the hiera data to add support for different operating systems without having to change a single line of the puppet code itself.
 
Ok, I have written a manifest using sysrc, sed, and grep to maintain /etc/rc.conf, /etc/sysctl.conf, /boot/loader.conf, and /etc/periodic.conf.

I've done rough testing on each. So far it seems correct on my main host and jails.

I'm preferring to use sysrc to manage shell syntax files (/etc/rc.conf and /etc/periodic.conf), and using grep/sed on others (/etc/sysctl.conf and /boot/loader.conf). One extra item is any sysctl is set in the running environment and saved to file.

Ruby:
######################################################################
# Function library

# Credit to:
#  https://web.archive.org/web/20150112032204/http://projects.puppetlabs.com/projects/1/wiki/Puppet_Free_Bsd

# Custom resources to edit common files with augeas.

# Shellvars fails on /etc/sysctl.conf, but there's a Sysctl lens that works
# sysctl messes up rc!

# rc can have single files in /etc/rc.conf.d/
# loader can have single files in /boot/loader.conf.d/
# sysctl is solo, but maybe the augeas lense works?
# and shellvars dies on lodaer.conf

# if rc can have single files in /etc/rc.conf.d/, why not have each profile
# create a static file by profile name
# same with loader.conf
# Crap! /etc/rc.conf.d/ doesn't load all files, it loads files by SERVICE name

# /etc/rc.conf uses sh syntax, but they seem to always use key="VALUE". # for comment
# /etc/sysctl.conf use key="VALUE", # for comment, may ignore whitespace?
# /etc/loader.conf use key="VALUE", # for comment

# Avoid using augeas, undocumented and cannot troubleshoot
# Prefer sysrc where possible, or sed/grep/awk

# Use sysrc to edit /etc/periodic.conf, as it is shell formatted.
define periodic_conf($value = '', $ensure = 'present') {
  case $ensure {
    default: { err ( "Unknown ensure value ${ensure}" ) }
    present: {
      exec {
        default: provider => 'shell';
        "rc_conf_${name}_present":
          # test if the setting is already current
          unless => "/bin/test '${value}' = \"`/sbin/sysrc -f /etc/periodic.conf -n ${name}`\"",
          # or set it, this updates /etc/rc.conf
          command => "/usr/sbin/sysrc -f /etc/periodic.conf ${name}=\"${value}\"";
      }
    }
    absent: {
      exec {
        default: provider => 'shell';
        # always try to remove it
        "rc_conf_${name}_absent":
          command => "/usr/sbin/sysrc -f /etc/periodic.conf -x ${name}";
      }
    }
  }
}


# - /etc/rc.conf : Use sysrc to set/change/remove, prevent run every time?
#                  sysrc -c key=value || sysrc key=value
#                  set live and save to make persistent
# Tested OK
# If a value name changes, make sure to force the old name absent
define rc_conf($value = '', $ensure = 'present') {
  case $ensure {
    default: { err ( "Unknown ensure value ${ensure}" ) }
    present: {
      exec {
        default: provider => 'shell';
        "rc_conf_${name}_present":
          # test if the setting is already current
          unless => "/usr/sbin/sysrc -c ${name}=\"${value}\"",
          # or set it, this updates /etc/rc.conf
          command => "/usr/sbin/sysrc ${name}=\"${value}\"";
      }
    }
    absent: {
      exec {
        default: provider => 'shell';
        # always try to remove it
        "rc_conf_${name}_absent":
          command => "/usr/sbin/sysrc -x ${name}";
      }
    }
  }
}

# - /boot/loader.conf : Cannot set via sysctl
#                  sed to clear key
#                  printf to append
# tested OK
define loader_conf($value = '', $ensure = 'present') {
  case $ensure {
    default: { err ( "Unknown ensure value ${ensure}" ) }
    present: {
      exec {
        default: provider => 'shell';
        "loader_conf_${name}_present":
          logoutput => true,
          # test if the setting is already current
          unless => "egrep -q '^[ \t]*${name}=${value}' /boot/loader.conf",
#          unless => "egrep -q '${name}=${value}' /etc/sysctl.conf ", # check the file, not running setting
          # purge all lines with that key, then set it and add it with sysctl
          command => "sed -i '' -e '/^[ \t]*${name}[ \t]*=.*$/d' /boot/loader.conf ; echo '${name}=${value}' >> /boot/loader.conf ";
      }
    }
    absent: {
      exec {
        default: provider => 'shell';
        # always try to remove it
        "loader_conf_${name}_absent":
          command => "egrep -q '[ \t]*${name}[ \t]*' /boot/loader.conf && sed -i '' -e '/^[ \t]*${name}[ \t]*=.*$/d' /boot/loader.conf";
      }
    }
  }
}

# - /etc/sysctl.conf : Set live via sysctl, and save to file
#                  sysctl -n key returns value, check against new value?
#                  sed to clear key from file
#                  sysctl -e key outputs the proper text to append to file
# Tested OK, if a value name changes make sure to add an absent
define sysctl_conf($value = '', $ensure = 'present') {
  case $ensure {
    default: { err ( "Unknown ensure value ${ensure}" ) }
    present: {
      exec {
        default: provider => 'shell';
        "sysctl_conf_${name}_present":
          logoutput => true,
          # test if the setting is already current
          unless => "/bin/test '${value}' = \"`/sbin/sysctl -n ${name}`\"",
#          unless => "egrep -q '${name}=${value}' /etc/sysctl.conf ", # check the file, not running setting
          # purge all lines with that key, then set it and add it with sysctl
          command => "sed -i '' -e '/^[ \t]*${name}[ \t]*=.*$/d' /etc/sysctl.conf ; /sbin/sysctl '${name}=${value}' && /sbin/sysctl -e '${name}' >> /etc/sysctl.conf ";
      }
    }
    absent: {
      exec {
        default: provider => 'shell';
        # always try to remove it
        "sysctl_conf_${name}_absent":
          command => "egrep -q '[ \t]*${name}[ \t]*' /etc/sysctl.conf && sed -i '' -e '/^[ \t]*${name}[ \t]*=.*$/d' /etc/sysctl.conf";
      }
    }
  }
}

file {
  default:
  ensure   => 'file',
  owner    => 'root',
  mode     => '0644',
  group    => 'wheel';

  '/etc/periodic.conf': ;
  '/etc/rc.conf': ;
  '/etc/sysctl.conf': ;
}
 
After some chat on IRC, I refactored the code to use Puppet's file_line function. The result is much cleaner! Sysctl updates still call a refresh.

Ruby:
define freebsd_conf($key = '', $value = '', $ensure = 'present', $file = '') {
  case $ensure {
    default: { err ( "Unknown ensure value ${ensure}" ) }
    present: {
      file_line {
        "${name}_${key}_present":
          ensure => 'present',
          path => $file,
          line => "${key}=\"${value}\"",
          match => "^[ \t]*${key}[ \t]*="; # match even poorly formatted lines
      }
    }
    absent: {
      file_line {
        "${name}_${key}_absent":
          ensure => 'absent',
          path => $file,
          match_for_absence => true,
          match => "^[ \t]*${key}[ \t]*="; # match even poorly formatted lines
      }
    }
  }
}

define sysctl_conf($value = '', $ensure = 'present') {
  freebsd_conf {
    "sysctl_conf_${name}_${ensure}":
      ensure => $ensure,
      file => '/etc/sysctl.conf',
      key => $name,
      value => $value,
      notify => Exec['sysctl-reload'];
    }
}

exec {
  "sysctl-reload":
    command => "/sbin/sysctl -f /etc/sysctl.conf",
    logoutput => true,
    refreshonly => true;
}

define loader_conf($value = '', $ensure = 'present') {
  freebsd_conf {
    "loader_conf_${name}_${ensure}":
      ensure => $ensure,
      file => '/boot/loader.conf',
      key => $name,
      value => $value;
    }
}

define rc_conf($value = '', $ensure = 'present') {
  freebsd_conf {
    "rc_conf_${name}_${ensure}":
      ensure => $ensure,
      file => '/etc/rc.conf',
      key => $name,
      value => $value;
    }
}

define periodic_conf($value = '', $ensure = 'present') {
  freebsd_conf {
    "periodic_conf_${name}_${ensure}":
      ensure => $ensure,
      file => '/etc/periodic.conf',
      key => $name,
      value => $value;
    }
}

file {
  default:
  ensure   => 'file',
  owner    => 'root',
  mode     => '0644',
  group    => 'wheel';

  '/etc/periodic.conf': ;
  '/etc/rc.conf': ;
  '/etc/sysctl.conf': ;
}
 
Back
Top