Red Green Repeat Adventures of a Spec Driven Junkie

Path to Puppet Magic

In my last post, time explained how to get puppet to work with vagrant. The puppet script worked, but honestly, I felt the solution was less than satisfying. I took advantage of puppet’s exec and -> commands to basically create an enhanced shell.

Where’s the puppet magic? Where the file provisioning would be a simple file like:

class { 'apt': }

apt::ppa{ 'ppa:ubuntu-elisp': }

package { 'emacs-snapshot':
  ensure  => 'latest',
  require => Apt::Ppa['ppa:ubuntu-elisp']
}

Well, I dug deep and found the answer.

I will share the end result first and document errors I encountered to help anyone else struggling with similar errors. These errors and their solutions were not really documented anywhere, so I want others to save a little pain and get started with puppet smoothly.

Simplified Manifest

As I noted earlier, puppet directly supports yum based package management, so apt based package management system have to jump through some hoops to get it working.

Well, there’s a puppet module for handling apt, like adding a ppa repository directly like:

manifest/default.pp:

apt::ppa{ 'ppa:ubuntu-elisp': }

Isn’t that a lot nicer than:

manifest/default.pp:

package { 'python-software-properties':
  ensure => 'installed'
} ->
exec { 'add repo':
  command => 'apt-add-repository -y ppa:ubuntu-elisp',
  path => '/usr/bin/'
}

The above is the puppet magic I wanted to happen. I didn’t want to script out every single step like the latter.

Puppet Magic: emacs-snapshot

This is the manifest I want to have:

manifests/default.pp:

class { 'apt': }

apt::ppa{ 'ppa:ubuntu-elisp': }

package { 'emacs-snapshot':
  ensure => 'latest',
  require => Apt::Ppa['ppa:ubuntu-elisp']
}

Vagrantfile:

Vagrant.configure("2") do |config|
  config.vm.box = 'ubuntu/trusty64'
  config.vm.hostname = "vagrant.example.com"
  config.vm.provision 'puppet'
end

I would expect things to just work… but the output of $ vagrant provision is:

==> default: Running provisioner: puppet...
==> default: Running Puppet with default.pp...
==> default: Error: Puppet::Parser::AST::Resource failed with error ArgumentError: Could not find declared class apt at /tmp/vagrant-puppet/manifests-a11d1078b1b1f2e3bdea27312f6ba513/default.pp:1 on node vagrant.example.com
==> default: Wrapped exception:
==> default: Could not find declared class apt
==> default: Error: Puppet::Parser::AST::Resource failed with error ArgumentError: Could not find declared class apt at /tmp/vagrant-puppet/manifests-a11d1078b1b1f2e3bdea27312f6ba513/default.pp:1 on node vagrant.example.com

Which is kind of expected since there is no loading of the puppet module apt anywhere…

Bootstrapping Puppet Modules

Well, the crazy part is: puppet uses puppet to get puppet modules, so there is a need a working version of puppet to load modules. Sometimes, the host system doesn’t have puppet setup (i.e. macOS…)

One way I have solved this: load the modules from the guest machine and copy the result to the shared vagrant folder.

In vagrant machine, run:

$ puppet module install puppetlabs-apt
$ cp -r .puppet/modules /vagrant/

Note: In puppet5, the modules loads from directory /opt/puppetlabs/puppet/modules, so the cp command would be: $ cp -r /opt/puppetlabs/puppet/modules /vagrant

Let’s run with the puppet provisioner with the following files:

Vagrantfile:

Vagrant.configure("2") do |config|
  config.vm.box = 'ubuntu/trusty64'
  config.vm.hostname = "vagrant.example.com"

  config.vm.provision 'puppet' do |puppet|
    puppet.module_path = 'modules'
  end
end

manifests/default.pp:

class { 'apt': }

apt::ppa{ 'ppa:ubuntu-elisp': }

package { 'emacs-snapshot':
  ensure => 'latest',
  require => Apt::Ppa['ppa:ubuntu-elisp']
}

provisioning again, the result is:

==> default: Running provisioner: puppet...
==> default: Running Puppet with default.pp...
==> default: Error: Syntax error at 'Hash'; expected ')' at /tmp/vagrant-puppet/modules-43a2450d049859e41f5e034c104286f8/apt/manifests/init.pp:6 on node vagrant.example.com
==> default: Error: Syntax error at 'Hash'; expected ')' at /tmp/vagrant-puppet/modules-43a2450d049859e41f5e034c104286f8/apt/manifests/init.pp:6 on node vagrant.example.com

There is something really wrong. I didn’t write that code. It is from a module puppet labs wrote, the authors of the puppet! So, there’s something REALLY weird.

Default Puppet Versions

When I look at the installed versions of puppet of Ubuntu 14.04 - trusty, the result is:

$ puppet --version => 3.4.3

In, Ubuntu 12.04 - precise, the version is 2.7.11!

As of this writing, the current version of puppet: 5!

The default installed version of puppet is a long way off! No wonder there are errors regarding library code! (Why didn’t the module error out on installation…?)

Upgrading to Puppet5

I wish upgrading puppet was easy, but version 4 had big changes in how puppet operates with the system. This is the method I used to upgrade to the latest version of puppet, following puppet platform install method

  • use apt-get purge puppet to remove installed version of puppet
  • install the associated .deb package for new puppet repositories
  • update repository listings
  • install puppet-agent

Note: puppet is not installed, because this guide focuses with running puppet agent on a local machine, not to connect with a master puppet node.

Note: puppet5 needs a newer glibc than the one install on Ubuntu 12.04 - precise.

Vagrantfile:

Vagrant.configure("2") do |config|
  config.vm.box = 'ubuntu/trusty64'
  config.vm.hostname = "vagrant.example.com"

  # http://www.terrarum.net/blog/masterless-puppet-with-vagrant.html
  config.vm.provision 'shell', :inline => <<-SHELL
    # install puppet5...
    apt-get purge -y puppet # remove puppet3
    wget http://apt.puppetlabs.com/puppet5-release-trusty.deb
    dpkg -i puppet5-release-trusty.deb
    apt-get update
    apt-get install -y puppet-agent # install puppet-agent
  SHELL

  config.vm.provision 'puppet' do |puppet|
    puppet.module_path = 'modules'
  end
end

Now, let’s provision to have puppet5 installed and have it provision using the new manifest and modules…

$ vagrant provision

error now:

==> default: Processing triggers for ureadahead (0.100.0-16) ...
==> default: Running provisioner: puppet...
==> default: Running Puppet with default.pp...
==> default: Error: Could not parse application options: invalid option: --manifestdir
The SSH command responded with a non-zero exit status. Vagrant
assumes that this means the command failed. The output for this command
should be in the log above. Please read the output to determine what
went wrong.

What the?? I did not set manifestdir anywhere in the Vagrantfile…

manifestdir Error

Well, exploring further, it looks like with puppet4, the manifestdir marked for deprecation, and removed in puppet5.

Vagrant by default passes manifestdir to the guest’s puppet provisioner, which in this case is puppet5. Puppet5 throws an error since it does not take manifestdir as an option anymore.

Looking at the vagrant github discussion on this issue, it looks like the solution is to set environment paths in the puppet provisioner configuration:

puppet.environment      = 'production'
puppet.environment_path = 'environments'

Magic!

This is the setup for vagrant to get puppet to magically provision a system to have the latest emacs installed.

Vagrantfile:

Vagrant.configure("2") do |config|
  config.vm.box = 'ubuntu/trusty64'
  config.vm.hostname = "vagrant.example.com"

  # http://www.terrarum.net/blog/masterless-puppet-with-vagrant.html
  config.vm.provision 'shell', :inline => <<-SHELL
    # install puppet5...
    apt-get purge -y puppet # remove puppet3
    wget http://apt.puppetlabs.com/puppet5-release-trusty.deb
    dpkg -i puppet5-release-trusty.deb
    apt-get update
    apt-get install -y puppet-agent # install puppet-agent
  SHELL

  # https://github.com/mitchellh/vagrant/issues/3740#issuecomment-92106636
  config.vm.provision 'puppet' do |puppet|
    puppet.environment_path = 'environments'
    puppet.environment = 'production'
    puppet.module_path = 'modules'
  end
end

environments/production/manifests/default.pp:

class { 'apt': }

apt::ppa{ 'ppa:ubuntu-elisp': }

package { 'emacs-snapshot':
  ensure  => 'latest',
  require => Apt::Ppa['ppa:ubuntu-elisp']
}

and run the provisioner:

$ vagrant provision

and the result:

==> default: Notice: /Stage[main]/Apt/Apt::Setting[conf-update-stamp]/File[/etc/apt/apt.conf.d/15update-stamp]/content: content changed '{md5}b9de0ac9e2c9854b1bb213e362dc4e41' to '{md5}0962d70c4ec78bbfa6f3544ae0c41974'
==> default: Notice: /Stage[main]/Main/Apt::Ppa[ppa:ubuntu-elisp]/Exec[add-apt-repository-ppa:ubuntu-elisp]/returns: executed successfully
==> default: Notice: /Stage[main]/Apt::Update/Exec[apt_update]: Triggered 'refresh' from 1 event
==> default: Notice: /Stage[main]/Main/Apt::Ppa[ppa:ubuntu-elisp]/File[/etc/apt/sources.list.d/ppa:ubuntu-elisp.list]/ensure: created
==> default: Notice: /Stage[main]/Main/Package[emacs-snapshot]/ensure: created
==> default: Notice: Applied catalog in 54.07 seconds

going in and checking:

vagrant@vagrant:~$ emacs --version
GNU Emacs 26.0.50.2
Copyright (C) 2016 Free Software Foundation, Inc.
GNU Emacs comes with ABSOLUTELY NO WARRANTY.
You may redistribute copies of GNU Emacs
under the terms of the GNU General Public License.
For more information about these matters, see the file named COPYING.

Success!

Conclusion

There is a lot of magic with puppet, and when things don’t work out, debugging it can take a lot of work. It’s even harder when there’s another moving part, here it is vagrant.

I’ve documented the solution that works for me at the time of writing. Including the errors here to help others trying to understand the problem better.

If there are any changes needed, please reach out to me directly and I’ll do my best to help (and update this article.)

Next time, I will go over the whole setup again and include additional manifests to make a system more secure.