Reusable Puppet Modules

Development & Testing

by Krzysztof Suszyński |@ksuszynski

Follow on

Me

Krzysztof Suszyński

Puppet & Java Developer

I work at COI & Wave Software

1st time with puppet in 2010

I've developed numerous reusable puppet modules: JBoss, Glassfish, XtreemFS, Artifactory, Flyway, Herald

Recap

  • Basics check - puppet 101
  • What are they?
  • Why bother to develop them?
  • How to do it?
  • How to test and maintain them?

REUSABLE Puppet MODULES

DevOps Process

Puppet 101

Quick repetition

Teory of operation

Puppet 101

Module Example

mymodule          # This outermost directory’s name matches the
│                 # name of the module.
├── manifests     # Contains all of the manifests in the module.
│   └── init.pp   # Contains a class definition. This class’s 
│                 # name must match the module’s name.
├── metadata.json # Contains META information about module
├── spec          # Contains spec tests for any plugins in the
│                 # lib directory.
├── templates     # Contains templates, which the module’s 
│                 # manifests can use.
├── tests         # Contains examples showing how to declare 
│   │             # the module's classes and defined types.
│   └── init.pp
└── lib           # Contains plugins, like custom facts and 
    │             # custom resource types.
    └── puppet
        ├── provider
        └── type

Puppet 101

Modules

  • Modules are stored on PM
  • Modules contain puppet code
    • puppet
    • ruby
  • Modules have layout
  • Modules can be easily installed

More on modules

What are does reusable Puppet modules?

Unity
Ease
Mastery

Unity

One single and well defined responsibility for given puppet module

 

Should support installation and management of given software

EasE

It should be possible to use module as simply as it possible. Best will be with one single line

include xtreemfs::role::directory

Module should work with it's default settings as it brings easy start for new comers

Mastery

Interfaces should have as deep input as possible.

 

  • Most used setting as parameters
  • Less often as property maps
define xtreemfs::volume (
  $ensure       = 'present',
  $volume       = $name,
  $dir_host     = undef,
  $dir_port     = undef,
  $dir_protocol = undef,
  $options      = {},
) {
  # [..]
}

Why to develop?

as a reusable

modules

Drawbacks of reuse 1

More effort is needed at the begining

Drawbacks of reuse 2

Some "reusable" modules will be dumped as one timers!

Benefits of reuse 1

Can be use multiple times, obviously

Benefits of reuse 2

Can be tested in different contexts

Benefits of reuse 3

Can be upgraded in iteration

Benefits of reuse 4

Will be simpler and easier to maintain

Benefits of reuse 5

Can be released as open source and RECEIVE PR!

How to develop?

reusable modules
Tips

1. Design
&
Plan

Plan
Your
Interface

Think about:

  • usable classes
  • usable defines
  • simplicity

Public interface

.
├── internal
│   ├── configure
│   │   ├── directory.pp
│   │   ├── metadata.pp
│   │   └── storage.pp
│   ├── packages
│   │   ├── client.pp
│   │   └── server.pp
│   ├── repo.pp
│   ├── settings.pp
│   └── workflow.pp
├── mount.pp
├── policy.pp
├── replicate.pp
├── role
│   ├── directory.pp
│   ├── metadata.pp
│   └── storage.pp
├── settings.pp
└── volume.pp

4 directories, 16 files
  • Simplify public interface
  • Put internals in internal package
  • Focus on usability for users
  • Use private function from stdlib > 4.4

Make
Covering
Defines

Covering Defines

define xtreemfs::volume (
  $volume       = $name,
  $ensure       = 'present',
  $dir_host     = undef,
  $dir_port     = undef,
  $dir_protocol = undef,
  $options      = {},
) {
  include xtreemfs::internal::packages::client
  include xtreemfs::internal::workflow
  include xtreemfs::settings

  validate_hash($options)

  $host = directory_address($dir_host, $dir_port,
    $dir_protocol, $xtreemfs::settings::dir_service)

  xtreemfs_volume { $volume:
    ensure  => $ensure,
    host    => $host,
    options => $options,
    require => Anchor[$xtreemfs::internal::workflow::service],
  }
}

Design Module

workflow

Module Workflow

# INTERNAL PRIVATE CLASS: do not use directly!
class xtreemfs::internal::workflow {
  
  $repo      = 'xtreemfs::repo'
  $packages  = 'xtreemfs::packages'
  $configure = 'xtreemfs::configure'
  $service   = 'xtreemfs::service'
  $end       = 'xtreemfs::end'
  
  anchor { $repo: } ->
  anchor { $packages: } ->
  anchor { $configure: } ~>
  anchor { $service: } ->
  anchor { $end: }
  
}

2. Tools

&
Structure

Puppet module Tool

Generate a module

puppet module generate company-service

Generates a scaffold for module with:

  • sample manifests installing and managing a `service`
  • unit & acceptance tests
  • build tools

Puppet Module tool - Example

$ puppet module generate wavesoftware-acpid
We need to create a metadata.json file for this module.  Please answer the
following questions; if the question is not applicable to this module, feel free
to leave it blank.

Puppet uses Semantic Versioning (semver.org) to version modules.
What version is this module?  [0.1.0]
--> 

# [...] MORE QUESTIONS

----------------------------------------
{
  "name": "wavesoftware-acpid",
  "version": "0.1.0",
  "author": "wavesoftware",
  "summary": "A module that manages and install a Acpid service",
  "license": "Apache-2.0",
  "source": "github.com/wavesoftware/puppet-acpid",
  "project_page": "https://github.com/wavesoftware/puppet-acpid",
  "issues_url": "https://github.com/wavesoftware/puppet-acpid/issues",
  "dependencies": [
    {"name":"puppetlabs-stdlib","version_requirement":">= 1.0.0"}
  ]
}
----------------------------------------

About to generate this metadata; continue? [n/Y]
--> 
Notice: Generating module at /home/ksuszynski/git/wavesoftware/puppet-acpid...

Puppet Module tool - 2

wavesoftware-acpid/
├── CHANGELOG
├── CONTRIBUTING.md
├── CONTRIBUTORS
├── files
├── Gemfile
├── Guardfile
├── lib
│   └── puppet
│       ├── provider
│       └── type
├── LICENSE
├── manifests
│   ├── config.pp
│   ├── init.pp
│   ├── install.pp
│   ├── params.pp
│   └── service.pp
├── metadata.json
├── Modulefile
├── Rakefile
├── README.markdown
├── spec
│   ├── acceptance
│   │   ├── class_spec.rb
│   │   └── nodesets
│   │       ├── centos-64-x64.yml
│   │       ├── default.yml
│   │       └── ubuntu-server-12042-x64.yml
│   ├── classes
│   │   ├── coverage_spec.rb
│   │   └── example_spec.rb
│   ├── spec_helper_acceptance.rb
│   └── spec_helper.rb
├── templates
└── tests
    └── init.pp

Delivers a working structure of new module with:

 

  • manifests
  • lib directories
  • Rakefile & Gemfile
  • smoke tests
  • spec test
  • beaker tests

RVM & Bundler

Gems & Ruby libraries Install

rvm use 2.1 --install

Install and use ruby 2.1.x in user directory

bundle install --path .vendor

Install all required libraries

RVM & Bundler

 $ rvm use 2.1
Using /home/ksuszynski/.rvm/gems/ruby-2.1.4
 $ export PUPPET_VERSION="~> 3.7.0"
 $ bundle install --path .vendor
Fetching https://github.com/rodjek/rspec-puppet.git
Fetching gem metadata from https://rubygems.org/.........
Fetching additional metadata from https://rubygems.org/..
Resolving dependencies...
Installing rake 10.4.2

# [..] more lines

Installing facter 1.7.6
Installing hiera 1.3.4
Installing beaker 2.4.1
Installing rspec-core 2.99.2
Installing rspec-expectations 2.99.2
Installing rspec-mocks 2.99.3
Installing rspec 2.99.0
Installing specinfra 1.27.5
Installing serverspec 1.16.0
Installing beaker-rspec 4.0.0
Installing puppet 3.7.4
Using rspec-puppet 2.0.0 from https://github.com/rodjek/rspec-puppet.git (at master)
Installing puppetlabs_spec_helper 0.8.2
Using bundler 1.7.9
Your bundle is complete!
It was installed into ./.vendor

Gems & Ruby libraries Install

RVM & Bundler - 2

bundle exec command

bundle exec

Runs a command in bundle context

Tip: bundle exec bash will enable you permanently run command without bundle exec

RVM & Bundler - 2

$ bundle exec rake -T
rake acceptance                              # Run acceptance tests
rake beaker                                  # Run beaker acceptance tests
rake beaker_nodes                            # List available beaker nodesets
rake build                                   # Build puppet module package
rake clean                                   # Clean a built module package
rake coverage                                # Generate code coverage information
rake help                                    # Display the list of available rake tasks
rake lint                                    # Check puppet manifests with puppet-lint / Run puppet-lint
rake module:bump                             # Bump module version to the next minor
rake module:bump_commit                      # Bump version and git commit
rake module:clean                            # Runs clean again
rake module:dependency[module_name,version]  # Set specific module dependency version
rake module:push                             # Push module to the Puppet Forge
rake module:release                          # Release the Puppet module, doing a clean, build, tag, push, bump_commit
rake module:tag                              # Git tag with the current module version
rake spec                                    # Run spec tests in a clean fixtures directory
rake spec_clean                              # Clean up the fixtures directory
rake spec_prep                               # Create the fixtures directory
rake spec_standalone                         # Run spec tests on an existing fixtures directory
rake syntax                                  # Syntax check Puppet manifests and templates
rake syntax:hiera                            # Syntax check Hiera config files
rake syntax:manifests                        # Syntax check Puppet manifests
rake syntax:templates                        # Syntax check Puppet templates
rake test                                    # Run syntax, lint, and spec tests
rake validate                                # Check syntax of Ruby files and call :syntax

bundle exec - runs in context

Puppet Ide's

Geppetto

Puppet Ide

others

  • Sublime Text

  • InteliJ IDEA

  • VIM, Emacs

Install puppet plugins to:

3. Code

Use GIT

if possible

Tip: One repo per module!

Puppet

code

  • puppetlabs standards: /guides/style_guide.html
  • keep manifest's code on 1, max 2 screens
  • document your manifests: puppet rdoc
  • use puppet lint: rake lint
  • use metadata lint: rake metadata

Ruby

code

Puppet_X

convention - example

require File.join(File.dirname(__FILE__),
  '../../puppet_x/wavesoftware/xtreemfs/type/replicable')

Puppet::Type.newtype :xtreemfs_replicate do
  desc "The xtreemfs_replicate type"
  newparam :file do
    isnamevar
  end
  newproperty :factor do
    Puppet_X::Wavesoftware::Xtreemfs::Type::Replicable
      .configure_factor(self)
  end
  newproperty :policy do
    Puppet_X::Wavesoftware::Xtreemfs::Type::Replicable
      .configure_policy(self)
  end
  Puppet_X::Wavesoftware::Xtreemfs::Type::Replicable
    .configure_global_validation(self)
end

Puppet_X

convention

lib/
├── puppet
│   ├── provider
│   │   ├── xtreemfs_policy
│   │   │   └── xtfsutil.rb
│   │   └── xtreemfs_replicate
│   │       └── xtfsutil.rb
│   └── type
│       ├── xtreemfs_policy.rb
│       └── xtreemfs_replicate.rb
└── puppet_x
    └── wavesoftware
        └── xtreemfs
            ├── provider
            │   └── xtfsutil.rb
            └── type
                └── replicable.rb
  • Safely separate your code from other's code
  • Can be easily indexed by tools operating on standard ruby code

4. Testing your Code

Puppet Testing

Smoke tests

  • Easy
  • Just use your code
  • Inside test folder
# contents of tests/role/directory.pp
include xtreemfs::role::directory
bundle exec rake spec_prep
bundle exec puppet apply tests/role/directory.pp --noop \
  --modulepath spec/fixtures/modules/

Test with:

Puppet Testing

Smoke tests Output

Notice: Compiled catalog for ksuszynski-gs70.suszynski.org 
in environment production in 1.24 seconds
Notice: /Stage[main]/Xtreemfs::Internal::Repo/Apt::Source[xtreemfs]
/Apt::Key[Add key: 07D6EA4F2FA7E736 from Apt::Source xtreemfs]
/Apt_key[Add key: 07D6EA4F2FA7E736 from Apt::Source xtreemfs]/ensure:
 current_value absent, should be present (noop)
Notice: Apt::Key[Add key: 07D6EA4F2FA7E736 from 
Apt::Source xtreemfs]: Would have triggered 'refresh' from 1 events
Notice: /Stage[main]/Apt/File[/etc/apt/apt.conf.d/15update-stamp]
/content: current_value {md5}b9de0ac9e2c9854b1bb213e362dc4e41,
 should be {md5}4355b3e7824866a503fc221621fc65ba (noop)
Notice: /Stage[main]/Xtreemfs::Internal::Repo/Apt::Source[xtreemfs]
/File[xtreemfs.list]/ensure: current_value absent, should be present (noop)
Notice: /Stage[main]/Apt::Update/Exec[apt_update]: Would have 
triggered 'refresh' from 1 events
Notice: Class[Apt::Update]: Would have triggered 'refresh' from
1 events
Notice: Apt::Source[xtreemfs]: Would have triggered 'refresh' 
from 2 events
Notice: Class[Apt]: Would have triggered 'refresh' from 1 events
Notice: /Stage[main]/Xtreemfs::Internal::Packages::Server/
Package[xtreemfs-server]/ensure: current_value purged, should be present (noop)
Notice: Finished catalog run in 0.35 seconds

Puppet Testing

RSpec Unit example

# contents of spec/unit/defines/mount_spec.rb
require 'spec_helper'
describe 'xtreemfs::mount', :type => :define do
  let :facts do
    {
      :osfamily               => 'RedHat',
      :operatingsystem        => 'CentOS',
      :operatingsystemrelease => '6.6',
      :fqdn                   => 'slave1.vm'
    }
  end
  let :title do
    '/mnt/my-xtreemfs-mount'
  end
  describe 'shouldn\'t work with only default parameters, must pass a volume' do
    it { expect { should compile }.to raise_error(/Must pass volume to Xtreemfs::Mount/) }
  end
  describe 'should work with only volume passed' do
    let :params do
      { :volume      => 'myVolume' }
    end
    it { should compile }
    it { should contain_class('xtreemfs::internal::packages::client') }
    it do
      should contain_xtreemfs__mount('/mnt/my-xtreemfs-mount').with(
        'ensure'      => 'mounted',
        'volume'      => 'myVolume',
        'dir_host'    => nil,
        'atboot'      => false,
        'options'     => 'defaults,allow_other'
      )
    end
  end
end

Puppet Testing

RSpec Unit output

xtreemfs::mount
  shouldn't work with only default parameters, must pass a volume
    should compile into a catalogue without dependency cycles
  should work with only volume passed
    should compile into a catalogue without dependency cycles
    should contain Class[xtreemfs::internal::packages::client]
    should contain Xtreemfs::Mount[/mnt/my-xtreemfs-mount] with ensure => "mounted", volume => "myVolume", dir_host defined, atboot => false and options => "defaults,allow_other"
    should contain File[/mnt/my-xtreemfs-mount] with ensure => "directory"
    should contain Mount[/mnt/my-xtreemfs-mount] that requires Anchor[xtreemfs::service]

Finished in 1.85 seconds
6 examples, 0 failures
Coverage report generated for RSpec to xtreemfs/coverage. 0.0 / 0.0 LOC (100.0%) covered.

Beaker

rspec acceptance

testing

Beaker

HOSTS:
  ubuntu-server-1404-x64-docker:
    roles:
      - master
    platform: ubuntu-14.04-amd64
    image: ubuntu:14.04
    hypervisor: docker
    docker_cmd: '/usr/sbin/sshd -D -o "PermitRootLogin yes"
       -o "PasswordAuthentication yes"'
CONFIG:
  type: foss

Nodeset configuration

Beaker

require 'spec_helper_acceptance'
describe 'xtreemfs::role::directory class' do

  describe 'executing without installing packages' do
    pp = <<-eos
      class { 'xtreemfs::role::directory':
        install_packages => false,
      }
    eos
    it 'shouldn\'t work' do
      apply_manifest(pp, :expect_failures => true)
    end
  end

  describe 'executing simple include with puppet code' do
    pp = <<-eos
    include xtreemfs::role::directory
    eos    
    it 'should work without errors' do
      apply_manifest(pp, :catch_failures => true)
    end
    it 'should not make any changes when executed twice' do
      apply_manifest(pp, :expect_changes => false)
    end
    describe service('xtreemfs-dir') do 
      it { should be_running }
    end    
  end
end

RSpec + Serverspec

Good to do

  • Calculate manifest coverage               

     
  • Calculate rspec coverage

     
  • Keep them above 95%

Puppet & Ruby Testing

at_exit { RSpec::Puppet::Coverage.report! }
gem 'simplecov'

All at Once 

bundle exec rake test acceptance

5. Online Services & Tools

Online services

Better visibility of code

Open Source

Closed Source

  • GitHub
  • Travis
  • Coveralls
  • Code Climate
  • Inch
  • Gemnasium
  • Puppet Forge
     

many more!!

  • Stash
  • Jenkins
  • Sonar
  • "puppet forge mirrors"

 

not so much more

Closed Source

Tools

Stash

Jenkins

Open Source

Tools

Github

Sheilds

Gemnasium

Travis CI

Coveralls

Code Climate

Inch

6. Learn
to
CODE

Puppetlabs

documentation

Type & Providers

Book

Write
Quality
CODE

Thanks!

Any questions?

Reusable Puppet Modules - Development & Testing

By Krzysztof Suszyński

Reusable Puppet Modules - Development & Testing

  • 2,751