Wednesday, May 29, 2013

Puppet: service checking on OS X without using the inbuilt service stanza

Normal service enforcement is easy with puppet, it looks like this for an OS X launchdaemon:
class something::myservice {

  File { owner => 'root', group => 'wheel', mode => '0644' }
  
  file { 'myservice_launchd_plist':
    ensure => file,
    path   => '/Library/LaunchDaemons/com.blah.myservice.plist',
    source => 'puppet:///modules/something/myservice/myservice_launchd.plist',
  }
   
  service { 'com.blah.myservice':
    ensure  => 'running',
    enable  => true,
    require => File['myservice_launchd_plist'],
  }
   
}
Which is great, until you don't want to manage that plist inside of puppet. In my case it was getting installed separately. If all the clients don't get upgraded properly then laying down the new plist with puppet (which points to different paths per version) will break old versions. It also means the plist needs to be updated in two places: inside puppet and inside the package for each release. So it's a hassle. I just want a simple check to restart it if it isn't running.

First attempt:
class something::myservice {

  File { owner => 'root', group => 'wheel', mode => '0644' }
  
  file { 'myservice_launchd_plist':
    path   => '/Library/LaunchDaemons/com.blah.myservice.plist',
  }
   
  service { 'com.blah.myservice':
    ensure  => 'running',
    enable  => true,
    require => File['myservice_launchd_plist'],
  }
   
}
This works fine, until the plist isn't there: i.e. the install failed for some reason, or this machine didn't get myservice installed. In that situation puppet will exit with an error code, so puppet management is effectively broken. No good. So, can we replicate what the service stanza is doing with a couple of simple exec statements? Seems easy...
class something::myservice {

  File { owner => 'root', group => 'wheel', mode => '0644' }

  exec { 'myservice':
    onlyif  => ['! /bin/launchctl list com.blah.myservice &> /dev/null',
                '/bin/test -f /Library/LaunchDaemons/com.blah.myservice.plist'],
    command => '/bin/launchctl load /Library/LaunchDaemons/com.blah.myservice.plist',
  }

}
This will run launchctl load if the service isn't running and the plist is actually there. The problem is puppet is overzealous in its command checking and will fail with this error:
Could not evaluate: Could not find command '!'
OK, what if we do onlyif and unless. Documentation is silent on what happens if you do this. It does appear to work:
class something::myservice {

  File { owner => 'root', group => 'wheel', mode => '0644' }

  exec { 'myservice':
    onlyif  => '/bin/test -f /Library/LaunchDaemons/com.blah.myservice.plist'
    command => '/bin/launchctl load /Library/LaunchDaemons/com.blah.myservice.plist',
    unless  => '/bin/launchctl list com.blah.myservice &> /dev/null',
  }

}
But both conditions always need to be evaluated, and I'm not sure this is actually going to work in the future. In my testing 'onlyif' was run before 'unless' but I wouldn't rely on that either. So lets just work around the broken commandline checking by adding a NOP with true &&:
class something::myservice {

  File { owner => 'root', group => 'wheel', mode => '0644' }

  exec { 'myservice':
    path    => ["/bin", "/usr/bin"],
    onlyif  => ['true && ! /bin/launchctl list com.blah.myservice &> /dev/null',
                '/bin/test -f /Library/LaunchDaemons/com.blah.myservice.plist'],
    command => '/bin/launchctl load /Library/LaunchDaemons/com.blah.myservice.plist',
  }

}
The path is necessary since the commandline checking wants the first part of the command to be an absolute path, or the path specified in 'path'. Since true is an inbuilt part of bash we need to specify path.

No comments: