Skip to content

Hands-On Exercise

This exercise ties the course together by building a real component module — a postfix module — using jig, EPP templates, Hiera data, and the testing tools. It assumes you have a control repo set up as described in Using OpenVox.

Prerequisites

A Linux machine with OpenVox installed, jig, git, and a control repo. Commands run as your normal user except where a # prompt indicates root. Substitute your real home directory for /home/youruser.

Create the component module

Try it yourself

From your home directory, scaffold a module and its classes:

$ jig new module example-postfix --skip-interview
$ cd postfix
$ jig new class postfix
$ jig new class postfix::install
$ jig new class postfix::config
$ jig new class postfix::service

Add the code

Edit manifests/init.pp to declare and order the subclasses:

class postfix {
  contain postfix::install
  contain postfix::config
  contain postfix::service

  Class['postfix::install']
  -> Class['postfix::config']
  ~> Class['postfix::service']
}

Edit manifests/install.pp to declare the package:

class postfix::install {
  package { 'postfix':
    ensure => installed,
  }
}

Edit manifests/service.pp to declare the service:

class postfix::service {
  service { 'postfix':
    ensure => running,
    enable => true,
  }
}

Edit manifests/config.pp to configure postfix from data, rendering each file from an EPP template:

class postfix::config (
  Hash $main_config,
  Hash $master_config,
  Hash $aliases,
) {
  file {
    default:
      owner => 'root',
      group => 'root',
      mode  => '0644',
    ;
    '/etc/postfix/main.cf':
      content => epp('postfix/main.cf.epp', { 'config' => $main_config }),
    ;
    '/etc/postfix/master.cf':
      content => epp('postfix/master.cf.epp', { 'config' => $master_config }),
    ;
    '/etc/aliases':
      content => epp('postfix/aliases.epp', { 'aliases' => $aliases }),
    ;
  }
  exec { '/usr/bin/newaliases':
    refreshonly => true,
    subscribe   => File['/etc/aliases'],
  }
}

Warning

This config class contains a deliberate flaw: because postfix::config is ordered with ~> before postfix::service, a change to /etc/aliases (or any triggered newaliases) will refresh the whole config class and restart the service. Watch for this when you test idempotency.

Try it yourself

Validate and unit-test what you have so far:

$ jig validate
$ jig test unit

Add the templates

Create templates/main.cf.epp:

<% | Hash $config | -%>
# Managed by OpenVox
<% $config.each |$key, $value| { -%>
<%= $key %> = <%= $value %>
<% } -%>

Create templates/master.cf.epp:

<% | Hash $config | -%>
# Managed by OpenVox
<% $config.each |$key, $value| { -%>
<%= ([$key] + $value).join("\t") %>
<% } -%>

Create templates/aliases.epp:

<% | Hash $aliases | -%>
# Managed by OpenVox
<% $aliases.each |$key, $value| { -%>
<%= $key %>: <%= $value %>
<% } -%>

Add the data

Edit data/common.yaml to populate the template defaults. First, a lookup_options entry so the config hashes deep-merge:

lookup_options:
  '^postfix::config::.*$':
    merge: deep

The main.cf settings:

postfix::config::main_config:
  inet_interfaces: localhost
  inet_protocols: all
  mydestination: '$myhostname, localhost.$mydomain, localhost'
  alias_maps: 'hash:/etc/aliases'
  alias_database: 'hash:/etc/aliases'
  debugger_command: '
        PATH=/bin:/usr/bin:/usr/local/bin:/usr/X11R6/bin
        ddd $daemon_directory/$process_name $process_id & sleep 5'
  sendmail_path: /usr/sbin/sendmail.postfix
  newaliases_path: /usr/bin/newaliases.postfix
  mailq_path: /usr/bin/mailq.postfix
  manpage_directory: /usr/share/man
  sample_directory: /usr/share/doc/postfix-2.10.1/samples
  readme_directory: /usr/share/doc/postfix-2.10.1/README_FILES

The master.cf service definitions:

postfix::config::master_config:
  smtp: ['inet','n','-','n','-','-','smtpd']
  pickup: ['unix','n','-','n','60','1','pickup']
  cleanup: ['unix','n','-','n','-','0','cleanup']
  qmgr: ['unix','n','-','n','300','1','qmgr']
  tlsmgr: ['unix','-','-','n','1000?','1','tlsmgr']
  rewrite: ['unix','-','-','n','-','-','trivial-rewrite']
  bounce: ['unix','-','-','n','-','0','bounce']
  defer: ['unix','-','-','n','-','0','bounce']
  trace: ['unix','-','-','n','-','0','bounce']
  verify: ['unix','-','-','n','-','1','verify']
  flush: ['unix','n','-','n','1000?','0','flush']
  proxymap: ['unix','-','-','n','-','-','proxymap']
  proxywrite: ['unix','-','-','n','-','1','proxymap']
  smtp: ['unix','-','-','n','-','-','smtp']
  relay: ['unix','-','-','n','-','-','smtp']
  showq: ['unix','n','-','n','-','-','showq']
  error: ['unix','-','-','n','-','-','error']
  retry: ['unix','-','-','n','-','-','error']
  discard: ['unix','-','-','n','-','-','discard']
  local: ['unix','-','n','n','-','-','local']
  virtual: ['unix','-','n','n','-','-','virtual']
  lmtp: ['unix','-','-','n','-','-','lmtp']
  anvil: ['unix','-','-','n','-','1','anvil']
  scache: ['unix','-','-','n','-','1','scache']

Warning

This data (carried over from the original course) defines the smtp key twice — once as the inet smtpd service and once as the unix smtp client. Because a YAML hash can't have two entries with the same key, the second wins and the smtp inet (smtpd) line is lost. A real postfix module needs both, so a production design would key master.cf services by something unique (e.g. service/type) rather than service name alone. Consider it an exercise in spotting data-model limitations.

postfix::config::aliases:
  mailer-daemon: postmaster
  postmaster: root

Test and commit

Try it yourself

Validate, test, and commit the module:

$ jig validate
$ jig test unit
$ git init
$ git add -A
$ git commit -m 'Initial commit.'

If jig test unit fails because of an unsupported operating system, remove windows from the operatingsystem_support section of metadata.json.

Declare the module from your control repo

Try it yourself

Add the new module to your control repo's Puppetfile:

mod 'example-postfix',
  :git => 'file:///home/youruser/postfix',
  :branch => :control_branch,
  :default_branch => 'main'

Edit data/common.yaml in your control repo to add postfix to the classes array. Commit the changes to Puppetfile and data/common.yaml, then deploy as root and check what would change:

# r10k deploy environment -pv
# puppet apply --noop \
  /etc/puppetlabs/code/environments/production/manifests/site.pp