Example of a Sensu Puppet class
Posted on Thu 13 July 2017 • Tagged with Work, Institute for Computer Vision and Computer Graphics
At the sensu-users mailing list someone asked how they could deploy Sensu plugins with Puppet. After giving a short snippet, I was asked for further help whether to implement the snippet as a class or what I would recommend. Therefore I present you: A slightly redacted example of a Sensu Puppet class taken from production.
I will attempt to walk you through the sections I used and at the end there will be a big code block with the complete class, for easier copy & paste.
Please note that this class was written with Sensu 0.26 and sensu-puppet 2.2.0 in mind and may not include all the latest features you expected to use and does not use features available in versions of Sensu or sensu-puppet.
Detailed explanation
docs
# Class: services::sensu
# Manages configuration, checks, handlers and certs for
# the sensu monitoring system
#
# parameters:
# (bool) is_main_server: makes this server the main host on which sensu is run
# (bool) consistent_connection: if set to `false`, enables high-value timeouts
# for sensu keepalive checks
# (array) subscriptions: the check groups a host should subscribe to
You will always want some form of documentation. Leaving a little bit in the code is considered good practice and puppet-lint will (rightfully) complain if you don’t. I make sure to also leave hints about class parameters and their types since I don’t use them a lot in this project.
default parameters
class services::sensu($is_main_server = false,
$consistent_connection = true,
$subscriptions = [])
{
As you might have seen, I use is_main_server
to denote the sensu-server
instance, so it defaults to false
. consistent_connection
will be manually set to false
for desktop or laptop machines that will be turned off regularly and is true
by default. In a later version of Sensu and sensu-puppet this can be solved easier with deregistration. The subscriptions
array will be filled with strings that enable subscriptions and checks that are not automatically detected and is empty by default.
manual configuration
# configuration
$rabbitmq_password = 'REDACTED'
$gitlab_health_token = 'REDACTED'
$gitlab_issues_token = 'REDACTED'
$assignments_health_token = 'REDACTED'
$sensu_monitoring_password = 'REDACTED'
# installed sensu plugins
$plugins = ['sensu-plugins-cpu-checks',
'sensu-plugins-disk-checks',
'sensu-plugins-environmental-checks',
'sensu-plugins-filesystem-checks',
'sensu-plugins-http',
'sensu-plugins-load-checks',
'sensu-plugins-memory-checks',
'sensu-plugins-network-checks',
'sensu-plugins-nvidia',
'sensu-plugins-ntp',
'sensu-plugins-postfix',
'sensu-plugins-process-checks',
'sensu-plugins-puppet',
'sensu-plugins-raid-checks',
'sensu-plugins-uptime-checks']
# kibana URL - allows clicking to jump to filtered log results
$kibana_url = "https://REDACTED/#/discover?_g=()&_a=(columns:!(_source),interval:auto,query:(query_string:(analyze_wildcard:!t,query:'host:${::hostname}')),sort:!('@timestamp',desc),index:%5Blogstash-%5DYYYY.MM.DD)#"
# grafana URL - allows clicking to jump to filtered metrics
$grafana_url = "https://REDACTED/dashboard/db/single-host-overview?var-hostname=${::hostname}"
# runbook prefix - allows linking directly to a propose solution
$runbook_prefix = 'https://REDACTED/administrators/documentation/blob/master/runbooks/sensu'
# how many times should keepalive fire before notifications
$keepalive_occurrences = '1'
# how much time needs to pass until keepalive notification is repeated (in seconds)
$keepalive_refresh = '3600'
# impact text for keepalive
$keepalive_impact = 'Host is not checking in with monitoring and may be completely unavailable.'
# suggestion text for keepalive
$keepalive_suggestion = 'Check if the host is frozen, stuck, down or offline.'
This is the section where details specific to our deployment reside. There is one block that holds tokens and passwords that used by Puppet during the deployment (rabbitmq_password
) and ones that are used by Sensu during standard operation (e.g. gitlab_health_token
when monitoring GitLab’s health API).
plugins
: lists Sensu plugins that should be installed on all machines.kibana_url
,grafana_url
: We have systems in place to collect log files and metrics from the systems we monitor. These are easy links that will be displayed in Uchiwa and notifications (e-mail, Mattermost) that link directly to data for the host in question.runbook_prefix
: I wrote runbooks for most checks so that my colleagues can resolve issues while I’m on vacation. This is prepended in checks, so that one only needs to concatenate the prefix with the filename of the runbook in question to get a full URL.
The next block describes Sensu’s keepalive events - you get these when Sensu has lost contact with a client (meaning your client hasn’t checked in with the Sensu server for some time). The keepalive_occurrences
and keepalive_refresh
attributes are used for filtering of notifications.
keepalive_impact
and keepalive_suggestion
are part of a concept I use throughout our Sensu deployment - Every check that can trigger a notification needs to have information on what the real-world impact of a failure is and what the quickest and most common solution to the problem could be.
automatic subscriptions
# automatic subscriptions computed from machine properties
if (str2bool($::is_virtual) == true)
{
$machine_type = ['virtual']
}
else
{
$machine_type = ['physical']
}
if (str2bool($::has_nvidia_graphics_card) == true and str2bool($::using_nouveau_driver) == false)
{
$gpu = ['nvidia']
}
else
{
$gpu = []
}
if (($::operatingsystem == 'Ubuntu' and versioncmp($::operatingsystemrelease, '16.04') >= 0) or
($::operatingsystem == 'Debian' and versioncmp($::operatingsystemrelease, '8.0') >= 0))
{
$systemd_enabled = ['systemd']
}
else
{
$systemd_enabled = []
}
$automatic_subscriptions = concat($machine_type, $gpu, $systemd_enabled, ['client_specific'])
After a while, hardcoding checks gets annoying and that’s why I try to automatically detect some things based on hardware or operating system.
::is_virtual
is a default Puppet fact. I’ll add checks for S.M.A.R.T. as well as RAID checks andsensors
metrics if run on a physical machine. (not included in this example)::has_nvidia_graphics_card
is a fact taken from jaredjennings/puppet-nvidia_graphics. I’ll add GPU specific metrics based on that. (not included in this example)- I’ll also try to decide whether Systemd is managing the host or not. I’ll add some specific service checks based on that. (not included in this example)
The automatic subscriptions are then combined with a pseudo-subscription called client_specific
that I use to distribute only the configuration of various client specific checks to hosts.
metrics templates
# template variables (must be in class scope)
$default_scheme = 'sensu.host.$(hostname)'
$metrics_handler = ['graphite_tcp']
$timestamp = '`date +%s`'
For easier use of metrics checks that are not written with sensu-plugin (the framework) I have some variables that are reused whenever hacking together a check on the quick.
default_scheme
is prepended to a metric, resulting in something likesensu.host.myawesomehostname.cpu.usage
metrics_handler
is an easier way of specifying the handler should we ever need to change it (or extend it).timestamp
is a simple way to get a UNIX timestamp.
sensu-server: packages and subscriptions
# SENSU SERVER
if ($is_main_server == true)
{
$combined_subscriptions = unique(concat(['proxy'], $subscriptions, $automatic_subscriptions))
$server_packages = ['redis-server', 'curl', 'jq']
$server_plugins = [ 'sensu-plugins-imap',
'sensu-plugins-slack',
'sensu-plugins-ssl',
'sensu-extensions-occurrences']
# install server-only packages
package
{
$server_packages:
ensure => present,
}
# install plugins for proxy group
package
{
$server_plugins:
ensure => present,
provider => 'sensu_gem',
require => Package[$server_packages],
}
The Sensu server is the machine handling proxy requests for me. That means that checks that check e.g. if a site is available via HTTP on another machine is a proxy check and will in my deployment be run on the Sensu server. To achieve this, a proxy
subscription is added to the subscriptions of the server.
Next, the server_packages
are installed via the default package management (e.g. apt
in my case) and the server_plugins
are Sensu specific ruby gems that are installed via the sensu_gem
provider that comes with sensu-puppet.
sensu-server: workaround
# Workaround for sensu-api not subscribing to check updates.
Class['::sensu::client::service'] ~> Class['::sensu::api::service']
Sometimes I had the problem that the results for some queries in Uchiwa were not the most recent ones and this snippet seems to have solved them.
sensu-server: configuration
This is the part where the sensu-puppet module is configured by my class.
class
{
'::sensu':
rabbitmq_password => $rabbitmq_password,
server => true,
client => true,
api => true,
api_bind => '127.0.0.1',
use_embedded_ruby => true,
rabbitmq_reconnect_on_error => true,
redis_reconnect_on_error => true,
redis_auto_reconnect => true,
subscriptions => $combined_subscriptions,
rabbitmq_host => '127.0.0.1',
redis_host => '127.0.0.1',
redact => ['password', 'pass', 'api_key','token'],
purge => true,
safe_mode => true,
require => Package[$server_packages],
client_custom =>
{
kibana_url => $kibana_url,
grafana_url => $grafana_url,
type => $::virtual,
operating_system => $::lsbdistdescription,
kernel => $::kernelrelease,
puppet_version => $::puppetversion,
gitlab_health =>
{
token => $gitlab_health_token,
},
ldap_sensu =>
{
password => $sensu_monitoring_password,
},
gitlab_issues =>
{
token => $gitlab_issues_token,
},
assignments_health =>
{
token => $assignments_health_token,
}
}
}
You can read about most parameters in the docs. Here are some general hints:
api_bind
: I bind to the machine so everything needs to be proxied (e.g. with Apache or Nginx).rabbitmq_reconnect_on_error
,redis_reconnect_on_error
,redis_auto_reconnect
: I want my deployment to be potentially self-healing.redact
: I have some additional keywords here that will be redacted in the API output. Please check out the Sensu docs on redaction, it’s a great feature.purge
: I enable this since I control all changes centrally. [queue Mass Effect 2 taking direct control soundbite]safe_mode
: Though it is more work, you probably do not want your hosts to run arbitrary commands.
The client_custom
section is where additional attributes are defined. I’ve already talked about kibana_url
and grafana_url
. I find that the operating system
, the kernel
version, the puppet_version
and whether the host is virtual or physical are helpful information to display on its dashboard page, so I include these.
The tokens and passwords are written to files on the host, and can then easily be referenced in Sensu command
s using e.g. :::gitlab_health.token:::
.
sensu-server: uchiwa
class
{
'::uchiwa':
install_repo => false,
host => '127.0.0.1',
require => Class['::sensu'],
}
I run Uchiwa, the dashboard for Sensu on the same machine and have it proxied. Note that this requires the yelp/uchiwa Puppet module.
sensu-server: includes
# sensu server specific checks
include services::sensu::core
# include all checks here, so that the master has all in order to run
# with safe_mode => true
# subscription: proxy
include services::sensu::imap
include services::sensu::certificates
include services::sensu::client_specific
include services::sensu::api_health
include services::sensu::availability
include services::sensu::remote_metrics
# automatic subscriptions
include services::sensu::nvidia
include services::sensu::physical
include services::sensu::systemd
include services::sensu::virtual
# last part is subscription name
include services::sensu::elasticsearch
include services::sensu::fail2ban
include services::sensu::kibana
include services::sensu::ldap
include services::sensu::mailman
include services::sensu::logstash
include services::sensu::seafile
include services::sensu::seahub
# include handler definitions
include services::sensu::handlers
}
Since I’m using safe_mode
, the Sensu server needs to have every single check that should be run. I include them here, manually.
Structuring your checks into neatly partitioned and readable files is a daunting task. I’ve tried to do it the following way: There is one file that holds checks that are common (core
). I’ve grouped all proxy
subscription checks into one block, automatic subscriptions into the second block and files that are automatically included based on the content of the subscriptions
array that the class receives in the third block. Handler definitions also get their own file (handlers
) since they get unwieldy even with only a few handlers.
sensu-client: subscriptions
# SENSU CLIENT
else
{
# default client configuration
$combined_subscriptions = unique(concat($subscriptions, $automatic_subscriptions))
# default include checks and metrics
include services::sensu::core
include services::sensu::client_specific
# automatically include checks for subscriptions
services::sensu::combined_subscriptions{$combined_subscriptions:}
Similar to the server, the client gets a combination of (manual) subscription
and automatic_subscriptions
. Then, the core
checks and metrics are included as well as any client_specific
ones. I include Puppet classes automatically based on the combined_subscriptions
then. For your convenience I’ll include this Puppet hack.
## combined_subscriptions.pp
# Define: services::sensu::combined_subscriptions
# use a define to dynamically include classes with checks
define services::sensu::combined_subscriptions
{
include "services::sensu::${name}"
}
sensu-client: keepalive configuration
# if the client is not consistently connected, warn after 2 weeks
# and throw a critical error after 4 weeks
# something will be wrong, outdated or the client can be removed
if ($consistent_connection == false)
{
$client_keepalive =
{
thresholds =>
{
warning => 1209600,
critical => 2419200,
},
handlers => ['default', 'mail', 'mattermost'],
runbook => "${runbook_prefix}/keepalive.markdown",
occurences => $keepalive_occurrences,
refresh => $keepalive_refresh,
impact => $keepalive_impact,
suggestion => $keepalive_suggestion,
}
}
else
{
$client_keepalive =
{
handlers => ['default', 'mail', 'mattermost'],
runbook => "${runbook_prefix}/keepalive.markdown",
occurences => $keepalive_occurrences,
refresh => $keepalive_refresh,
impact => $keepalive_impact,
suggestion => $keepalive_suggestion,
}
The configuration for keepalive events is part of the client attributes, not a separate check. If I set consistent_connection
to false
, it will take some weeks until I am notified of a “missing” device. Filters are configured via occurrences
and refresh
. The Sensu developers wrote a helpful blog post on that.
Again, if you have a new enough version of Sensu, you should not need this.
As you can see, the “check” also has a runbook, an impact description and an operator suggestion defined to make manual intervention very easy.
sensu-client: configuration
This is the part where the sensu-puppet module is configured by my class.
class
{
'::sensu':
rabbitmq_password => $rabbitmq_password,
rabbitmq_host => 'REDACTED',
rabbitmq_port => '5671',
server => false,
api => false,
client => true,
client_keepalive => $client_keepalive,
subscriptions => $combined_subscriptions,
rabbitmq_ssl => true,
rabbitmq_ssl_private_key => 'puppet:///modules/services/sensu/client-key.pem',
rabbitmq_ssl_cert_chain => 'puppet:///modules/services/sensu/client-cert.pem',
use_embedded_ruby => true,
rabbitmq_reconnect_on_error => true,
purge => true,
safe_mode => true,
require => Package['ruby-json'],
client_custom =>
{
kibana_url => $kibana_url,
grafana_url => $grafana_url,
type => $::virtual,
operating_system => $::lsbdistdescription,
kernel => $::kernelrelease,
puppet_version => $::puppetversion,
},
}
}
There is nothing especially fancy here except the client_keepalive
which gets filled with the values from a previous section. Everything else should either be taken from the docs or was already explained earlier.
Of note: rabbitmq_ssl_private_key
and rabbitmq_ssl_cert_chain
are the same for every host. This is an (unfortunate) implementation detail which allows only one cert in use for the whole Sensu transport deployment. I think I would’ve liked to piggyback onto Puppet’s certificates if possible, but am quite aware this is neither good in terms of compartmentalization nor good design.
common
package
{
$plugins:
ensure => installed,
provider => 'sensu_gem',
}
file
{
'/etc/sudoers.d/sensu':
ensure => file,
owner => 'root',
group => 'root',
mode => '0440',
source => 'puppet:///modules/services/sensu/sudoers.d',
require => Package['sudo'],
}
# all nodes need development dependencies for native extentions
$client_packages = ['g++', 'make', 'ruby-json', 'sudo']
Class['apt::update']
-> Package[$client_packages]
package
{
$client_packages:
ensure => present,
}
}
This section is for both the server and the client part. The list of sensu-plugins is installed via sensu_gem
. Some checks I use with Sensu require sudo
rights, so I distribute a customized sudoers file directly into /etc/sudoers.d/
which whitelists some commands for Sensu.
Since it is often the case that Ruby gems try to build native extensions on installation we require development tools on each host.
As a last little detail I make sure to only install packages after an apt-get update
run. I think I added this since I was often testing my setup in a Docker container via GitLab’s CI feature. It is good practice to have a container that is as small as possible, so people delete the cached apt
sources which leads to errors while installing packages if apt-get update
is not run before an apt-get install PACKAGE
.
Minimal example
Alright, so now I’ve written quite a bit about this specific class, but how would one actually use all of this? Let’s see a minimal working example.
# site.pp
node 'myhostname.mydomain.com'
{
include services::sensu
}
If you wanted to add additional (previously implemented) subscriptions, you would use something like this:
# site.pp
node 'example.domain.com'
{
class{ 'services::sensu': subscriptions => ['fail2ban', 'ldap']}
}
sensu.pp
# Class: services::sensu
# Manages configuration, checks, handlers and certs for
# the sensu monitoring system
#
# parameters:
# (bool) is_main_server: makes this server the main host on which sensu is run
# (bool) consistent_connection: if set to `false`, enables high-value timeouts
# for sensu keepalive checks
# (array) subscriptions: the check groups a host should subscribe to
class services::sensu($is_main_server = false,
$consistent_connection = true,
$subscriptions = [])
{
# configuration
$rabbitmq_password = 'REDACTED'
$gitlab_health_token = 'REDACTED'
$gitlab_issues_token = 'REDACTED'
$assignments_health_token = 'REDACTED'
$sensu_monitoring_password = 'REDACTED'
# installed sensu plugins
$plugins = ['sensu-plugins-cpu-checks',
'sensu-plugins-disk-checks',
'sensu-plugins-environmental-checks',
'sensu-plugins-filesystem-checks',
'sensu-plugins-http',
'sensu-plugins-load-checks',
'sensu-plugins-memory-checks',
'sensu-plugins-network-checks',
'sensu-plugins-nvidia',
'sensu-plugins-ntp',
'sensu-plugins-postfix',
'sensu-plugins-process-checks',
'sensu-plugins-puppet',
'sensu-plugins-raid-checks',
'sensu-plugins-uptime-checks']
# kibana URL - allows clicking to jump to filtered log results
$kibana_url = "https://REDACTED/#/discover?_g=()&_a=(columns:!(_source),interval:auto,query:(query_string:(analyze_wildcard:!t,query:'host:${::hostname}')),sort:!('@timestamp',desc),index:%5Blogstash-%5DYYYY.MM.DD)#"
# grafana URL - allows clicking to jump to filtered metrics
$grafana_url = "https://REDACTED/dashboard/db/single-host-overview?var-hostname=${::hostname}"
# runbook prefix - allows linking directly to a propose solution
$runbook_prefix = 'https://REDACTED/administrators/documentation/blob/master/runbooks/sensu'
# how many times should keepalive fire before notifications
$keepalive_occurrences = '1'
# how much time needs to pass until keepalive notification is repeated (in seconds)
$keepalive_refresh = '3600'
# impact text for keepalive
$keepalive_impact = 'Host is not checking in with monitoring and may be completely unavailable.'
# suggestion text for keepalive
$keepalive_suggestion = 'Check if the host is frozen, stuck, down or offline.'
# automatic subscriptions computed from machine properties
if (str2bool($::is_virtual) == true)
{
$machine_type = ['virtual']
}
else
{
$machine_type = ['physical']
}
if (str2bool($::has_nvidia_graphics_card) == true and str2bool($::using_nouveau_driver) == false)
{
$gpu = ['nvidia']
}
else
{
$gpu = []
}
if (($::operatingsystem == 'Ubuntu' and versioncmp($::operatingsystemrelease, '16.04') >= 0) or
($::operatingsystem == 'Debian' and versioncmp($::operatingsystemrelease, '8.0') >= 0))
{
$systemd_enabled = ['systemd']
}
else
{
$systemd_enabled = []
}
$automatic_subscriptions = concat($machine_type, $gpu, $systemd_enabled, ['client_specific'])
# template variables (must be in class scope)
$default_scheme = 'sensu.host.$(hostname)'
$metrics_handler = ['graphite_tcp']
$timestamp = '`date +%s`'
# SENSU SERVER
if ($is_main_server == true)
{
$combined_subscriptions = unique(concat(['proxy'], $subscriptions, $automatic_subscriptions))
$server_packages = ['redis-server', 'curl', 'jq']
$server_plugins = [ 'sensu-plugins-imap',
'sensu-plugins-slack',
'sensu-plugins-ssl',
'sensu-extensions-occurrences']
# install server-only packages
package
{
$server_packages:
ensure => present,
}
# install plugins for proxy group
package
{
$server_plugins:
ensure => present,
provider => 'sensu_gem',
require => Package[$server_packages],
}
# Workaround for sensu-api not subscribing to check updates.
Class['::sensu::client::service'] ~> Class['::sensu::api::service']
class
{
'::sensu':
rabbitmq_password => $rabbitmq_password,
server => true,
client => true,
api => true,
api_bind => '127.0.0.1',
use_embedded_ruby => true,
rabbitmq_reconnect_on_error => true,
redis_reconnect_on_error => true,
redis_auto_reconnect => true,
subscriptions => $combined_subscriptions,
rabbitmq_host => '127.0.0.1',
redis_host => '127.0.0.1',
redact => ['password', 'pass', 'api_key','token'],
purge => true,
safe_mode => true,
require => Package[$server_packages],
client_custom =>
{
kibana_url => $kibana_url,
grafana_url => $grafana_url,
type => $::virtual,
operating_system => $::lsbdistdescription,
kernel => $::kernelrelease,
puppet_version => $::puppetversion,
gitlab_health =>
{
token => $gitlab_health_token,
},
ldap_sensu =>
{
password => $sensu_monitoring_password,
},
gitlab_issues =>
{
token => $gitlab_issues_token,
},
assignments_health =>
{
token => $assignments_health_token,
}
}
}
class
{
'::uchiwa':
install_repo => false,
host => '127.0.0.1',
require => Class['::sensu'],
}
# sensu server specific checks
include services::sensu::core
# include all checks here, so that the master has all in order to run
# with safe_mode => true
# subscription: proxy
include services::sensu::imap
include services::sensu::certificates
include services::sensu::client_specific
include services::sensu::api_health
include services::sensu::availability
include services::sensu::remote_metrics
# automatic subscriptions
include services::sensu::nvidia
include services::sensu::physical
include services::sensu::systemd
include services::sensu::virtual
# last part is subscription name
include services::sensu::elasticsearch
include services::sensu::fail2ban
include services::sensu::kibana
include services::sensu::ldap
include services::sensu::mailman
include services::sensu::logstash
include services::sensu::seafile
include services::sensu::seahub
# include handler definitions
include services::sensu::handlers
}
# SENSU CLIENT
else
{
# default client configuration
$combined_subscriptions = unique(concat($subscriptions, $automatic_subscriptions))
# default include checks and metrics
include services::sensu::core
include services::sensu::client_specific
# automatically include checks for subscriptions
services::sensu::combined_subscriptions{$combined_subscriptions:}
# if the client is not consistently connected, warn after 2 weeks
# and throw a critical error after 4 weeks
# something will be wrong, outdated or the client can be removed
if ($consistent_connection == false)
{
$client_keepalive =
{
thresholds =>
{
warning => 1209600,
critical => 2419200,
},
handlers => ['default', 'mail', 'mattermost'],
runbook => "${runbook_prefix}/keepalive.markdown",
occurences => $keepalive_occurrences,
refresh => $keepalive_refresh,
impact => $keepalive_impact,
suggestion => $keepalive_suggestion,
}
}
else
{
$client_keepalive =
{
handlers => ['default', 'mail', 'mattermost'],
runbook => "${runbook_prefix}/keepalive.markdown",
occurences => $keepalive_occurrences,
refresh => $keepalive_refresh,
impact => $keepalive_impact,
suggestion => $keepalive_suggestion,
}
}
class
{
'::sensu':
rabbitmq_password => $rabbitmq_password,
rabbitmq_host => 'REDACTED',
rabbitmq_port => '5671',
server => false,
api => false,
client => true,
client_keepalive => $client_keepalive,
subscriptions => $combined_subscriptions,
rabbitmq_ssl => true,
rabbitmq_ssl_private_key => 'puppet:///modules/services/sensu/client-key.pem',
rabbitmq_ssl_cert_chain => 'puppet:///modules/services/sensu/client-cert.pem',
use_embedded_ruby => true,
rabbitmq_reconnect_on_error => true,
purge => true,
safe_mode => true,
require => Package['ruby-json'],
client_custom =>
{
kibana_url => $kibana_url,
grafana_url => $grafana_url,
type => $::virtual,
operating_system => $::lsbdistdescription,
kernel => $::kernelrelease,
puppet_version => $::puppetversion,
},
}
}
package
{
$plugins:
ensure => installed,
provider => 'sensu_gem',
}
file
{
'/etc/sudoers.d/sensu':
ensure => file,
owner => 'root',
group => 'root',
mode => '0440',
source => 'puppet:///modules/services/sensu/sudoers.d',
require => Package['sudo'],
}
# all nodes need development dependencies for native extentions
$client_packages = ['g++', 'make', 'ruby-json', 'sudo']
Class['apt::update']
-> Package[$client_packages]
package
{
$client_packages:
ensure => present,
}
}
Example of a Sensu Puppet class is part 2 of Sensu: