We've all run into problems like this:

$ echo 12000 > /proc/sys/vm/dirty_writeback_centisecs
-bash: /proc/sys/vm/dirty_writeback_centisecs: Permission denied

The command fails because the target file is only writeable by root. The fix seems obvious and easy:

$ sudo echo 12000 > /proc/sys/vm/dirty_writeback_centisecs -bash: /proc/sys/vm/dirty_writeback_centisecs: Permission denied

Huh? It still fails. What gives? The reason it fails is that it is the shell that sets up the re-direction before running the command under sudo. The solution is to run the whole pipeline under sudo. There are several ways to do this:

echo 'echo 12000 > /proc/sys/vm/dirty_writeback_centisecs' | sudo sh
sudo sh -c 'echo 12000 > /proc/sys/vm/dirty_writeback_centisecs'
echo 12000 | sudo tee /proc/sys/vm/dirty_writeback_centisecs

This is fine for simple commands, but what if you have a complex command that already includes quotes and shell meta-characters?

Here's what I use for that:

sudo su <<\EOF
echo 12000 > /proc/sys/vm/dirty_writeback_centisecs
EOF

Note that the backslash before EOF is important to ensure meta-characters are not expanded.

Finally, here's an example of a command for which I needed to use this technique:

sudo sh  << \EOF
perl -n -e '
use strict;
use warnings;
if (/^([^=]*=)([^\$]*)(.*)/) {
  my $pre = $1;
  my $path = $2;
  my $post = $3;
  (my $newpath = $path) =~ s/usr/usr\/local/;
  $newpath =~ s/://g;
  print "$pre$newpath:$path$post\n"
}
else {
  print
}
' < /opt/rh/ruby193/enable > /opt/rh/ruby193/enable.new
EOF

Volcane recently asked in ##infra-talk on Freenode if anyone knew of "some little tool that can be used in a cronjob for example to noop the the real task if say load avg is high or similar?"

I came up with the idea to use nagios plugins. So, for example, to check load average before running a task:

/usr/lib64/nagios/plugins/check_load -w 0.7,0.6,0.5 -c 0.9,0.8,0.7 >/dev/null && echo "Run the task here"

Substitute the values used for the -w and -c args as appropriate, or use a different plugin for different conditions.

In a previous post, I described how to describe puppet environments, roles, and profiles, as modules and how to use r10k and librarian-puppet to deploy them.

One possible problem with deploying to the puppet environment directory directly is that the librarian-puppet run can take some time and there is a possibility that puppet may attempt to compile a catalogue in an incomplete or inconsistent environment. One way to overcome this is to deploy the environments into a new directory, create a symlink, and move the symlink atomically into place.

This would look something like this:

cd /etc/puppet/envs
# create a new dir under /etc/puppet/envs - I use a timestamp in the name so I know when it was created
NEW_ENV_DIR=$(mktemp --directory envs.$(date -Isec).XXX")
cd /etc/puppet
# use r10k deploy the environments into the new dir
PUPPETFILE_DIR="envs/${NEW_ENV_DIR}" r10k puppetfile install
# loop over all the environments and use librarian-puppet to deploy all the roles/profiles/modules
while read env ; do
  pushd $env
  LIBRARIAN_PUPPET_PATH=modules librarian-puppet install --no-use-v1-api --strip-dot-git
  popd
done < <(find "/etc/puppet/envs/${NEW_ENV_DIR}" -maxdepth 1 -mindepth 1 -type d)
ln -s /etc/puppet/envs/${NEW_ENV_DIR} /etc/puppet/envs/environments
mv /etc/puppet/envs/environments /etc/puppet

I have written a script that does all of this in a more robust way and also uses parallel to speed up the deployment process.

The script and the role and profile modules references in my previous article are in this github repo.

By treating roles and profiles as puppet modules, we can use r10k and librarian-puppet to manage the deployment of our puppet code into our puppet environements.

I shall assume that puppet is configured to use to use directory environments and that the environment path is $confdir/environments (ie. the default location). I also assume that both r10k and librarian-puppet are installed and in the path.

You should also understand and embrace the role-profile-module pattern, first described by Craig Dunn and subsequently by Adrian Thebo and Gary Larizza. Quoting Gary:

  • Roles abstract profiles
  • Profiles abstract component modules
  • Hiera abstracts configuration data
  • Component modules abstract resources
  • Resources abstract the underlying OS implementation 

I find the following points useful to clarify the purpose of each of the layers in this model:

  • Roles, profiles, and component modules can all be implemented as puppet modules
  • Each node is assigned exactly one role (either in site.pp or, preferably, using some external node classifier)
  • Each role includes one or more profiles
  • Each profile loads configuration data and feeds it into the component modules – this is where your business logic should go
  • Each component module should be generic and contain no site-specific data. You should be able to publish all your component modules on PuppetForge without leaking any secrets.

We can further extend this model to include environments. An environment can be thought of as a group of roles and can also be implemented as a puppet module.

So, how do we set this up?

At the top-level, we put a Puppetfile in the puppet config dir containing a list of our environments. This will look something like this:

#!/usr/bin/env ruby
#^syntax detection

forge 'https://forgeapi.puppetlabs.com'

mod 'yo61-env_production',
  :git => 'git@github.com:yo61/puppet-demo_env_production.git'

mod 'yo61-env_staging',
  :git => 'git@github.com:yo61/puppet-demo_env_staging.git'

Each environment is defined as a puppet module. Any valid Puppetfile syntax may be used to specifiy the module location, including alternate branches or specific version tags. 

Each of the environment modules should contain all the usual things you would put in a puppet environment, eg. a manifests/site.pp, etc. as well as a Puppetfile containing a list of all the roles to be deployed to this environment. The Puppetfile for a simple environment would look something like this:

#!/usr/bin/env ruby
#^syntax detection

forge "https://forgeapi.puppetlabs.com"

# list all the roles that are included in this environment
mod 'yo61-role_default',
  :git => 'git@github.com:yo61/puppet-demo-roles-profiles.git',
  :path => 'modules/role_default'

mod 'yo61-role_foo',
  :git => 'git@github.com:yo61/puppet-demo-roles-profiles.git',
  :path => 'modules/role_foo'

mod 'yo61-role_bar',
  :git => 'git@github.com:yo61/puppet-demo-roles-profiles.git',
  :path => 'modules/role_bar'

Like the top-level Puppetfile used to defined environments, each role is defined as a puppet module.

Each of the role modules will contain a simple class that loads the profiles used by the role, and a Puppetfile containing a list of all profiles used by the role. The Puppetfile for a simple role would look something like this:

#!/usr/bin/env ruby
#^syntax detection

forge "https://forgeapi.puppetlabs.com"

# list all the profiles that are included in this role
mod 'yo61-profile_common',
  :git => 'git@github.com:yo61/puppet-demo-roles-profiles.git',
  :path => 'modules/profile_common'

Each of the profile modules will contain all the puppet code required to define the business logic, load configuration data, etc. and a Puppetfile containing a list of all the component modules used by the profile. The Puppetfile for a simple profile would look something like this:

#!/usr/bin/env ruby
#^syntax detection

forge "https://forgeapi.puppetlabs.com"

# include all the modules used by this profile
mod 'puppetlabs-stdlib',
mod 'stahnma-epel'
mod 'puppetlabs-ntp'

Again, any valid Puppetfile syntax may be used.

We've now defined all our environments, roles, and profiles and we're ready to deploy each environment.

First, we run r10k to deploy each of the environment modules into the environment dir (/etc/puppet/environments):

# switch to the location of the top-level Puppetfile
cd /etc/puppet
PUPPETFILE_DIR=/etc/puppet/environments r10k puppetfile install

This will create a directory in /etc/puppet/environments for each of the environments defined in the top-level Puppetfile.

Next, we change into each of the newly-created environment directories and run librarian-puppet to install all the roles required by that environment.

cd /etc/puppet/environments/production
LIBRARIAN_PUPPET_PATH=modules librarian-puppet install --no-use-v1-api --strip-dot-git

The best bit is that librarian-puppet supports recursive module dependency resolution so this one command installs not only the roles, but also all the profiles, and component modules required by each of the roles.

My next article will present a script that runs r10k and librarian-puppet as described in this article, and also updates puppet environments atomically

 

We're using puppet + puppetdb in an EC2 environment where nodes come and go quite regularly. We have a custom autosign script that uses ec2 security info to validate the nodes before allowing the autosigning. This is all good, but it can leave a lot of "dead" nodes in puppet, eg. if a bunch of nodes are created by an autoscale policy and then terminated.

To get rid of these zombie nodes from puppet/puppetdb we can just use:

puppet node deactivate <certname1> <certname2> ... <certnameN>

We can query puppetdb to get a list of nodes that have not sent puppet reports for, say, 24 hours. The puppetdb query we need is something like this:

'query=["<", "report-timestamp", "$cutoff_date"]'

where $cutoff_date is a date in ISO8601 format, eg. 2015-03-05T13:39:45+0000

We can use date to generate the cutoff date with something like this:

$cutoff_date=$(date -d '-1 day' -Isec)

We then plug this into the query string and send it with curl as follows:

curl --silent -G 'http://localhost:8080/v4/nodes' \
  --data-urlencode "query=[\"<\", \"report-timestamp\", \"$(date -d '-1 day' -Isec)\"]"

Finally, we filter through jq to get a list of certnames:

curl --silent -G 'http://localhost:8080/v4/nodes' \
  --data-urlencode "query=[\"<\", \"report-timestamp\", \"$(date -d '-1 day' -Isec)\"]" \
  | jq '.[].certname'

We can then pass the list of nodes to the "puppet node deactivate" command.

One of my very early frustrations with puppet was that it allows variables to be used when they were undefined. Primarily this bit me by not catching typos in variable names which were often very hard to track down. I was very pleased when Puppetlabs introduced a strict_variables mode which throws an error if a manifest attempts to use an undefined variable.

I recently need to check for the existence of a fact. Without strict_variables, this is straight-forward:

if $::some_fact {
  # do stuff here
}

If the fact "some_fact" exists, the variable is a non-empty string and evaluates as true in boolean context. If the fact doesn't exist, the variable is an empty string which evaluates as false in boolean context.

But, with strict_variables enforced, this throws an error:

Error: Undefined variable "::some_fact"; Undefined variable "some_fact" at line ...

The solution is to use the getvar function from stdlib:

if getvar('::some_fact') {
  # do stuff here
}

This works exactly the same as in the previous example, but doesn't throw an error if "some_fact" doesn't exist.

The team at bitly has written an http reverse proxy that provides authentication using Google's OAuth2 API. They write about it in a blog post.

The proxy is written in Go but builds to a single, statically-linked executable, ie. there are no complex run-time dependencies, which is great.

I've built an RPM for EL7 which also includes a sample systemd unit file, and sample configuration file. Both source and binary RPMs are available in my yum repo.

Additionally, I've create a puppet module that installs the RPM, creates a systemd service, and sets up an nginx front end to the proxy service. The module is available from the Puppetforge, and also on github.

I'd be interested in any feedback/comments/bug reports/pull requests.

I recently had need to install uwsgi on EL7 (CentOS 7, actually, but RHEL 7 will be the same).

I ended up rebuilding the uwsgi SRPM from Fedora 21 which was relatively straight-forward but it required a few tweaks to the .spec file. I also had to build a chain of dependencies: mongodb, perl-Cora, libecb, perl-EV, libev, zeromq, perl-BDB, perl-AnyEvent-BDB, perl-AnyEvent-AIO.

All packages (including SRPMs) are in my repo: http://repo.yo61.net/el/7/

I'm setting up a new puppet master running under passenger on CentOS 7 using packages from the puppetlabs and foreman repos. I used a fork of Stephen Johnson's puppet module to set everything up (with puppet apply). All went swimmingly, except I would see this error in the logs the first time the puppet master app loaded (ie. the first time it got a request):

[ 2014-11-07 23:22:13.2600 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] *** Phusion Passenger: no passenger_native_support.so found for the current Ruby interpreter. Compiling one (set PASSENGER_COMPILE_NATIVE_SUPPORT_BINARY=0 to disable)...
[ 2014-11-07 23:22:13.2600 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] # mkdir -p /usr/share/gems/gems/passenger-4.0.18/lib/phusion_passenger/locations.ini/buildout/ruby/ruby-2.0.0-x86_64-linux
[ 2014-11-07 23:22:13.2600 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] Not a valid directory. Trying a different one...
[ 2014-11-07 23:22:13.2600 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] -------------------------------
[ 2014-11-07 23:22:13.2600 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] # mkdir -p /var/lib/puppet/.passenger/native_support/4.0.18/ruby-2.0.0-x86_64-linux
[ 2014-11-07 23:22:13.2600 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] # cd /var/lib/puppet/.passenger/native_support/4.0.18/ruby-2.0.0-x86_64-linux
[ 2014-11-07 23:22:13.2600 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] # /usr/bin/ruby '/usr/share/gems/gems/passenger-4.0.18/ruby_extension_source/extconf.rb'
[ 2014-11-07 23:22:13.3048 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] /usr/bin/ruby: No such file or directory -- /usr/share/gems/gems/passenger-4.0.18/ruby_extension_source/extconf.rb (LoadError)
[ 2014-11-07 23:22:13.3156 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] Compilation failed.
[ 2014-11-07 23:22:13.3156 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] -------------------------------
[ 2014-11-07 23:22:13.3157 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] Ruby native_support extension not loaded. Continuing without native_support.

I double checked, and I do have the native libs installed – they're in the rubygem-passenger-native-libs rpm – the main library is in /usr/lib64/gems/ruby/passenger-4.0.18/native/passenger_native_support.so.

Digging in the passenger code, it tries to load the native libs by doing:

require 'native/passenger_native_support'

If I hacked this to:

require '/usr/lib64/gems/ruby/passenger-4.0.18/native/passenger_native_support'

then it loaded correctly.

It seems that /usr/lib64/gems/ruby/passenger-4.0.18 is not in the ruby load path.

Additional directories can be added to the ruby load path by setting an environment variable, RUBYLIB.

To set RUBYLIB for the apache process, I added the following line to /etc/sysconfig/httpd and restarted apache:

RUBYLIB=/usr/lib64/gems/ruby/passenger-4.0.18

The passenger native libraries now load correctly.