Creating our first module

Puppet modules are the fundamental building block of puppet and are used for abstracting away the differences between different platforms. A good module for some software should not define how you want the software but provide an API so that the software can be used on multiple platforms without needing to know the intricacies of that platform. An important part of writing reusable modules is that other people can easily understand you code. To this end Puppetlabs has published a Puppetlab Style Guide which is based on best practices in the Puppet community.

Create a module structure

PuppetForge module naming convention is the name or organisation that is developing them and the name module name separated by a hyphen such as this:

fullstackpuppet-ntp

Lets create a directory to store all our modules we are going to create and then use the puppet module command to create a basic module structure:

mkdir ~/ruby_workingdir/modules
cd ~/ruby_workingdir/modules
[thughes@titanium: ~/ruby_workingdir/modules]$ puppet module generate fullstackpuppet-ntp
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]
-->

Who wrote this module?  [fullstackpuppet]
-->

What license does this module code fall under?  [Apache 2.0]
-->

How would you describe this module in a single sentence?
--> Manages NTP on linux machines.

Where is this module's source code repository?
--> https://github.com/fullstack-puppet/fullstackpuppet-ntp.git

Where can others go to learn more about this module?  [https://github.com/fullstack-puppet/fullstackpuppet-ntp]
-->

Where can others go to file issues about this module?  [https://github.com/fullstack-puppet/fullstackpuppet-ntp/issues]
-->

----------------------------------------
{
  "name": "fullstackpuppet-ntp",
  "version": "0.1.0",
  "author": "fullstackpuppet",
  "summary": "Manages NTP on linux machines.",
  "license": "Apache 2.0",
  "source": "https://github.com/fullstack-puppet/fullstackpuppet-ntp.git",
  "project_page": "https://github.com/fullstack-puppet/fullstackpuppet-ntp",
  "issues_url": "https://github.com/fullstack-puppet/fullstackpuppet-ntp/issues",
  "dependencies": [
    {"version_requirement":">= 1.0.0","name":"puppetlabs-stdlib"}
  ]
}
----------------------------------------

About to generate this metadata; continue? [n/Y]
--> y

Notice: Generating module at /home/thughes/ruby_workingdir/fullstackpuppet-ntp...
Notice: Populating templates...
Finished; module generated in fullstackpuppet-ntp.
fullstackpuppet-ntp/tests
fullstackpuppet-ntp/tests/init.pp
fullstackpuppet-ntp/Rakefile
fullstackpuppet-ntp/spec
fullstackpuppet-ntp/spec/classes
fullstackpuppet-ntp/spec/classes/init_spec.rb
fullstackpuppet-ntp/spec/spec_helper.rb
fullstackpuppet-ntp/Gemfile
fullstackpuppet-ntp/manifests
fullstackpuppet-ntp/manifests/init.pp
fullstackpuppet-ntp/metadata.json
fullstackpuppet-ntp/README.md
cd fullstackpuppet-ntp

Create .gitignore file

cat <<EOF > .gitignore
pkg/
Gemfile.lock
.bundle/
.rspec_system/
.*.sw*
/spec/fixtures/.librarian
/spec/fixtures/.tmp
/spec/fixtures/Puppetfile.lock
/spec/fixtures/modules
/spec/fixtures/manifests
/spec/fixtures/vendor
EOF

Customise the module

Now we have a basic module structure which we can use to create our working NTP module. The main file and entry point to the module is manifests/init.pp and if we open that up we will see a load of comments and a empty ntp class construct. This class will later provide the API to the ntp module but for now we will leave it as it is.

# == Class: ntp
#
# Full description of class ntp here.
#
# === Parameters
#
# Document parameters here.
#
# [*sample_parameter*]
#   Explanation of what this parameter affects and what it defaults to.
#   e.g. "Specify one or more upstream ntp servers as an array."
#
# === Variables
#
# Here you should define a list of variables that this module would require.
#
# [*sample_variable*]
#   Explanation of how this variable affects the funtion of this class and if
#   it has a default. e.g. "The parameter enc_ntp_servers must be set by the
#   External Node Classifier as a comma separated list of hostnames." (Note,
#   global variables should be avoided in favor of class parameters as
#   of Puppet 2.6.)
#
# === Examples
#
#  class { 'ntp':
#    servers => [ 'pool.ntp.org', 'ntp.local.company.com' ],
#  }
#
# === Authors
#
# Author Name <author@domain.com>
#
# === Copyright
#
# Copyright 2014 Your name here, unless otherwise noted.
#
class ntp {


}

The basic pattern of most modules on Linux servers involves 3 components:

Software installation
This can be anything from RPM, DEB package management through Ruby Gems through to a Zip or TAR file.
Configure the software
This is usually through a configuration file but may involve creating databases or directories with the correct permissions.
Start the software
This is usually through whatever service management system is being used on the system. This may be things like systemd, chkconfig, supervisord. This step may not be needed if it not software that runs as a service.

To make our code easy to understand we split these three jobs into separate classes and keep them in their own files.

install.pp:class modulename::install
config.pp:class modulename::config
service.pp:class modulename::service

We create one more class which contains the default parameters for our module.

params.pp:class modulename::params

Lets start with the software installation step of our module in manifests/install.pp.

Content for manifests/install.pp

# == Class: ntp::install
class ntp::install inherits ntp {

  package { 'ntp':
    ensure => installed,
  }

}

In this class we inherit from ntp. This inheritance will be used later to inherit properties that the main ntp class configures. The second thing it does in define a Puppet type package named ntp and ensure it is installed. Puppet abstracts away the actual installation so that this will install ntp on systems with different package managers. For instance, on Debian it will use apt-get and on Redhat it will use yum install

Next we need to configure our ntp software. The config file for ntp is /etc/ntp.conf

Content for manifests/config.pp

# == Class: ntp::config
class ntp::config inherits ntp {

  file { '/etc/ntp.conf':
    ensure  => file,
    owner   => 'root',
    group   => 'root',
    mode    => 0644,
    content => template($module_name/ntp.conf.erb),
  }

}

As with ntp::install we inherit from the main ntp class. Next we use the Puppet type file to manage the ntp config file setting it’s permissions and content which is created by a template

Templates go in the template directory of our module. They can contain variables and basic logic to construct the end file but for now we will just create one with no template logic and we can come back to it later.

Content for templates/ntp.conf.erb

driftfile /var/lib/ntp/drift

restrict default kod nomodify notrap nopeer noquery
restrict -6 default kod nomodify notrap nopeer noquery

restrict 127.0.0.1
restrict -6 ::1

server 0.centos.pool.ntp.org iburst
server 1.centos.pool.ntp.org iburst
server 2.centos.pool.ntp.org iburst
server 3.centos.pool.ntp.org iburst

includefile /etc/ntp/crypto/pw

keys /etc/ntp/keys

The final part of a basic module is making sure that the software is running. This is managed in the manifests/service.pp file.

Content for manifests/service.pp

# == Class: ntp::service
class ntp::service inherits ntp {

  service { 'ntp':
    ensure     => running,
    enable     => true,
    hasstatus  => true,
    hasrestart => true,
    require => Package['ntp'],
  }

}

Once again we inherit from the main ntp class. This time we use the Puppet type service which is an abstraction of most major service control systems on Linux. We ensure that the service ntp is running and that it is enabled on boot. It also requires the package ntp because there is no point trying to start software that isn’t installed.

We haven’t created the manifests/params.pp file yet because everything is hard coded in our module.

Now we have our three subclasses defined we need to let the main ntp class know about them. To do this we need to include our subclasses in our main ntp class.

class ntp {

    include ntp::install
    include ntp::config
    include ntp::service
}

If you haven’t used git before then you will need to do a little setup. Set you name and email and editor of choice.

git config --global user.email "you@example.com"
git config --global user.name "Your Name"

git config --global core.editor vim
git config --global push.default simple

Initialize git repo:

git init .
git add --all .

Do the initial commit:

git commit -am "Initial commit of fullstackpuppet-ntp"