Manifest and Module Organization, Take One

In the last article, we learned how to import modules from the puppet forge. We created a very simple, but disorganized, site manifest. We need to create some organization, which will give us the ability to apply different settings to different nodes. Here’s the manifest we ended up with:

class { '::ntp':
  servers => [ '0.pool.ntp.org', '2.centos.pool.ntp.org', '1.rhel.pool.ntp.org'],
}

user { 'dave':
  ensure     => present,
  uid        => '507',
  gid        => '507',
  shell      => '/bin/bash',
  home       => '/home/dave',
  managehome => true,
}

group { 'dave':
  ensure => 'present',
  gid    => '507',
}

include ::ssh
::ssh::server::configline { 'PermitRootLogin': value => 'yes' }

The manifest includes two modules from the puppet forge and two resources managed by puppet, one user and one group. These resources, however, are going to applied to every agent that connects. As we grow the manifests, we’re going to meet some resources that are only needed on certain agents – web servers, web apps, etc. Let’s take what we have and organize it better.

Inheritance and Declaration

Before we proceed, let’s take a quick detour. We need to have a good understanding of inheritance and declaration in the puppet DSL. Check out puppet’s documentation on classes and the style guide’s section on inheritance for some background and style guidelines for inheritance. The class documentation also covers class declaration.

Inheritance is done by saying classA inherits classB { … }. As the documentation and style guide node, there are very few use cases where inheritance is considered a good idea. I’m trying to hew close to the style guide, so I’ll largely ignore inheritance. However, there are a number of ways people have successfully used inheritance. A popular example is Craig Dunn’s Roles and Profiles article. If you plan to use inheritance, I highly suggest that article and the accompanying slides and video.

A less complex way to manage classes is through declaration. There are two types of declaration. The first is include-like behavior. We used this above, with include ::ssh. Including a class allows you to safely declare a class multiple times and it will only be added to the catalog once. If you use an external data source, like hiera, you can provide values for some class variables, otherwise an included is limited to the default values. The second is resource-like behavior. We used this when we called class { ‘::ntp’: … }. Resource-like declarations allow a manifest to override values in the class, otherwise it falls back to include-like behavior patterns – external data source and default values. However, resource-like declarations must be unique. Puppet’s DSL parsing is not order-dependent, which could cause erratic behavior if the same resource was defined twice and the order of parsing was not the same on multiple catalog runs. If you declare a resource twice, nothing will break, but your catalog compilation will fail and all puppet agent runs will exit without making changes.

(You may be wondering which type the ::ssh::server::configline declaration is. It’s neither. It’s a define, which we will cover later.)

In most cases, the correct answer is to include a class. However, until we configure an external data source, we will need resource-like declarations to override default values. As we build our own modules and classes that rely on the forge modules, this information is important so that we declare classes properly without limiting our future selves.

Creating the master’s node definition

The first thing we can do to organize our manifest is to create a node definition for our master. We’ll use a tool installed by puppet, called facter, to grab the full qualified domain name, or FQDN, that puppet recognizes for the master. Facter is used throughout puppet to provide ‘facts’ about an agent to the master. If you run facter by itself, you can see the whole list. In our case, we just want the fact known as ‘fqdn’:

[root@puppet manifests]# facter fqdn
puppet.nelson.va

With the node name, we can create a section in our site.pp that’s specific to the node. Add the following to the tail end of your site.pp

node 'puppet.nelson.va' {
  notify {"this is the master speaking":}
}
notify {"this is a test":}

Anything we add to that scope will apply to this node. Since we have defined a specific scope for the node, will non-node specific settings still apply? When we run our next agent test, will we receive one or both notify statements? Let’s find out:

[root@puppet manifests]# puppet agent --test --noop
Info: Retrieving plugin
...
Info: Caching catalog for puppet.nelson.va
Info: Applying configuration version '1391654344'
Notice: this is the master speaking
Notice: /Stage[main]/Main/Node[puppet.nelson.va]/Notify[this is the master speaking]/message: defined 'message' as 'this is the master speaking'
Notice: this is a test
Notice: /Stage[main]/Main/Notify[this is a test]/message: defined 'message' as 'this is a test'
Notice: Finished catalog run in 0.64 seconds

Both the node specific scope and the global scope apply. In the short term, we can use this while we organize the site.pp but it’s not something we want to rely on. It’s something you need to be cognizant of, as an errant statement in the global scope would affect all nodes; probably not a desirable state.

Creating a class for our current configuration

With a node definition created, we should migrate the class declarations in the global scope into a single class to apply to the master. We probably want ntp, ssh, and the user/group dave on every node, so we’ll create a class called ‘base’. Move all of the statements, except the node definition, inside this class. Inside of the node definition, include the new class. You can get rid of the errant notifies while you’re at it. After a little format and ordering cleanup (order doesn’t matter to puppet, but it does to the humans who have to read it!), here’s the new site.pp:

class base {

  include ::ssh
  ::ssh::server::configline { 'PermitRootLogin': value => 'yes' }

  class { '::ntp':
    servers => [ '0.pool.ntp.org', '2.centos.pool.ntp.org', '1.rhel.pool.ntp.org'],
  }

  user { 'dave':
    ensure     => present,
    uid        => '507',
    gid        => '507',
    shell      => '/bin/bash',
    home       => '/home/dave',
    managehome => true,
  }

  group { 'dave':
    ensure => 'present',
    gid    => '507',
  }
}

node 'puppet.nelson.va' {
  include base
}

An agent test now will succeed, though it doesn’t change anything, so no actual changes have been made since the last run. You can ensure it works by changing dave’s home directory with usermod and then performing another agent test.

[root@puppet manifests]# usermod -s /bin/sh dave
[root@puppet manifests]# puppet agent --test
Info: Retrieving plugin
...
Info: Caching catalog for puppet.nelson.va
Info: Applying configuration version '1391658154'
Notice: /Stage[main]/Base/User[dave]/shell: shell changed '/bin/sh' to '/bin/bash'
Notice: Finished catalog run in 0.68 seconds

So far, so good! We haven’t gotten there ourselves, yet, but if you had another puppet agent that could check in, that agent wouldn’t receive anything in its catalog because of how our manifest is now structured. If it had checked in 5 minutes ago, it would have received everything in the manifest.

Creating your own module

What we did above, creating a single class in the site.pp file, is pretty crude. The modules we used last time all came from the forge and were all in their own directory structure under /etc/puppet/modules. Our site manifest is one small file. It’s very simple now, but it will grow and quickly become unwieldy. Let’s nip that in the bud by creating our own module and referencing that in the site manifest. This will move our ‘base’ class out of site.pp into its own module, leaving only node definitions in the site manifest.

Puppet includes a command to create a module: puppet module generate <author>-<name>. The format <author>-<name> is intended for use with the forge. Even though the module we are building is for internal consumption, we will still use this format. It’s a good habit to get into, and it helps with populating the structure of the module properly. I’ll call my module rnelson0-base, add your own author tag to your module name. You should either cd to /etc/puppet/modules before running the command, or move the files afterward:

[root@puppet ~]# cd /etc/puppet/modules/
[root@puppet modules]# puppet module generate rnelson0-base
Notice: Generating module at /etc/puppet/modules/rnelson0-base
rnelson0-base
rnelson0-base/manifests
rnelson0-base/manifests/init.pp
rnelson0-base/Modulefile
rnelson0-base/README
rnelson0-base/spec
rnelson0-base/spec/spec_helper.rb
rnelson0-base/tests
rnelson0-base/tests/init.pp

When we imported modules in the last article, we ignored the structure, so let’s take a look at that now. Like our site manifest, the module has a manifest directory. Instead of a site.pp, though, it contains an init.pp. This is the root of all modules, so that is where we will define the class ::base. If we want to define subclasses, such as ::base::subclass, we would create the file rnelson0-base/manifests/subclass.pp. You can also create additional levels to your class layout with another directory. rnelson0-base/manifests/layer/cake.pp would be called as ::base::layer::cake. Next, Modulefile and README exist at the root of the module structure. The first file is where the dependencies of a module are defined. The README is pretty explanatory; it is also displayed to users who visit the module page on the forge. The last two directories, spec and tests, are very useful for testing, but a bit beyond the scope of today’s article. You can read more on module fundamentals and on testing at your leisure.

Following our iterative process, let’s start with the simple action of moving the entirety of the existing base class from the site manifest to the new module. Open rnelson0-base/manifests/init.pp and you will notice some boilerplate, another benefit of using the generate command. Fill out the author and copyright sections. We’ll worry about the rest, no point in filling them out now as we’ll be rapidly interating through changes. Add to the skeleton of the base class with what we had in the site manifest. You should end up with this:

class base {

  include ::ssh
  ::ssh::server::configline { 'PermitRootLogin': value => 'yes' }

  class { '::ntp':
    servers => [ '0.pool.ntp.org', '2.centos.pool.ntp.org', '1.rhel.pool.ntp.org'],
  }

  user { 'dave':
    ensure     => present,
    uid        => '507',
    gid        => '507',
    shell      => '/bin/bash',
    home       => '/home/dave',
    managehome => true,
  }

  group { 'dave':
    ensure => 'present',
    gid    => '507',
  }

}

Now you can remove all of that from /etc/puppet/manifests/site.pp, plus we need to change the name of the class being included in our node definition. Here’s the new site.pp:

node 'puppet.nelson.va' {
  include ::base
}

Let’s perform an agent test and ensure we receive no errors:

Error: Could not retrieve catalog from remote server: Error 400 on SERVER: Could not find class ::base for puppet.nelson.va on node puppet.nelson.va
Warning: Not using cache on failed catalog
Error: Could not retrieve catalog; skipping run

Well, that appears to be a problem. When the puppet master looks at the modules directories, it uses the path to determine the module name. In our case, that’s rnelson0-base – not base, as we intend to reference the module. This is easy to fix by renaming the directory and running the agent test again:

[root@puppet modules]# pwd
/etc/puppet/modules
[root@puppet modules]# mv rnelson0-base/ base/
[root@puppet modules]# puppet agent --test
Info: Retrieving plugin
Info: Loading facts in /etc/puppet/modules/stdlib/lib/facter/puppet_vardir.rb
Info: Loading facts in /etc/puppet/modules/stdlib/lib/facter/pe_version.rb
Info: Loading facts in /etc/puppet/modules/stdlib/lib/facter/root_home.rb
Info: Loading facts in /etc/puppet/modules/stdlib/lib/facter/facter_dot_d.rb
Info: Loading facts in /var/lib/puppet/lib/facter/puppet_vardir.rb
Info: Loading facts in /var/lib/puppet/lib/facter/pe_version.rb
Info: Loading facts in /var/lib/puppet/lib/facter/root_home.rb
Info: Loading facts in /var/lib/puppet/lib/facter/facter_dot_d.rb
Info: Caching catalog for puppet.nelson.va
Info: Applying configuration version '1391690437'
Notice: Finished catalog run in 0.66 seconds

OK, now it looks good! Before we move on, let’s take a quick look at the Modulefile and README that puppet generated. Even though this is not something we plan to upload to the forge, it’s still helpful to populate the files. Here’s the Modulefile:

[root@puppet base]# cat Modulefile
name    'rnelson0-base'
version '0.1.0'
source 'UNKNOWN'
author 'rnelson0'
license 'Apache License, Version 2.0'
summary 'UNKNOWN'
description 'UNKNOWN'
project_page 'UNKNOWN'

## Add dependencies, if any:
# dependency 'username/name', '>= 1.2.0'

Most of the fields are descriptive. The project_page would be your GitHub page and the source would be the .git version of it (or some other code repo site). Dependencies are very simple. Because we rely on saz/ssh and puppetlabs/ntp, let’s add them. Use puppet module list to get the list of modules and their version number. Let’s add those two dependencies and require greater than or equal to the current version installed. More info on Modulefile dependencies can be found here.

[root@puppet base]# puppet module list
/etc/puppet/modules
├── base (???)
├── puppetlabs-ntp (v3.0.1)
├── puppetlabs-stdlib (v4.1.0)
├── saz-ssh (v1.4.0)
├── yguenane-augeas (v0.1.1)
└── yguenane-ygrpms (v0.1.0)
/usr/share/puppet/modules (no modules installed)
[root@puppet base]# vi Modulefile
[root@puppet base]# cat Modulefile
name    'rnelson0-base'
version '0.1.0'
source 'UNKNOWN'
author 'rnelson0'
license 'Apache License, Version 2.0'
summary 'Base class to apply to all agents'
description 'This class contains our common configuration that is required across all managed nodes'
project_page 'UNKNOWN'

## Add dependencies, if any:
dependency 'puppetlabs/ntp', '>= 3.0.1'
dependency 'saz/ssh', '>= 1.4.0'

The README is very boring and you can fill this out later. Now our initial draft of the module is complete. If you look in the output above, the module is reported with the name base and shows ??? for the version number. Even though we have updated the Modulefile, it will still show question marks. You have to build your module with puppet module build. This will generate a tarball with some metadata in it. This is ideal for uploading to the forge. When someone installs the module from the forge, it will extract the contents of your module plus a file called metadata.json. We can short circuit the upload/download cycle a bit with some creative tar usage.

[root@puppet base]# puppet module list | grep base
├── base (???)
[root@puppet base]# puppet module build
Notice: Building /etc/puppet/modules/base for release
Module built: /etc/puppet/modules/base/pkg/rnelson0-base-0.1.0.tar.gz
[root@puppet base]# tar xzf pkg/rnelson0-base-0.1.0.tar.gz  rnelson0-base-0.1.0/metadata.json -O > metadata.json
[root@puppet base]# puppet module list | grep base
├── rnelson0-base (v0.1.0)

Our young module is starting to look pretty good!

Users and defines

We implemented our user in a pretty shallow way – direct manipulation of the resources, which includes having to define the group as well. After the user is created, it does not have a known password, so we’re still involved in a manual touch effort. We also haven’t addressed password aging, comments, shells, uid and guid, and other values that we may want to address consistently across nodes. There’s another way to do this, and it has the added bonus of teaching us about defined types.

A defined type is similar to a normal resource type, but is composed of other resources. At its simplest level, it’s a macro for resource creation, but it can be far more complex. Some common uses are for apache vhosts or, as above, ssh configuration lines. In these cases, you want to declare multiple resources of the same type, but don’t want to repeat the implementation code for each instance. Could you imagine having to add 100 users and 100 groups as we did above? That would be quite lengthy and prone to errors. We can create a defined resource type with the define keyword to avoid this.

First, we have to identify what collection of resources we need. Currently we have a user and a group, but is that all we need? We need to do something about the password, perhaps there are other resources, so let’s see what other people have done. Some google-fu helped me find Joseft Schiggerl’s blog. Joseft defined two resources. One is the user itself, the other is an exec statement. The exec statement is what we will use to set the initial password. In the middle, you see the definition of a variable that contains a sed statement. The specifics vary by osfamily, which is another fact provided by facter. RedHat and Debian use different formats for disabled new accounts in /etc/shadow, so we need two actions. This define does not provide a group resource. We’ll skip that for now and come back to it later.

The first resource, the user, looks similar to our manual definition. Note that the name of the resource, $id, is a variable rather than a string. The reason is that every resource in a catalog needs to be unique. If our define is used twice and the user resource name is a string, catalog compilation will fail. Make sure all resources in a define are unique. The rest of the user definition should make sense. Comment, groups, and password_max_age are new but should be explanatory.

We touched upon the case statement before – different OSes do things differently. The code in the next section should be easy to follow. We use $::osfamily as our differentiator, which comes from the fact osfamily. When it has the value RedHat, set $action to one string; if it is Debian, set $action to a different string. We don’t use it here, but there’s also a default response that can be set. We’ll come back and add that shortly.

Next up is the exec resource. An exec is a callout to the system to run a command. The command to run is $action that was defined above (note that $action will be unique for every user, ensuring that catalog compilation does not fail). The $PATH variable is defined, we make no assumptions about what the $PATH contents are so we are not surprised. The onlyif attribute is another command handed to the system. If it returns a 0, the exec command is run. If it returns any other exit code, the exec is skipped. The command provided will check /etc/shadow to see if the specified $id‘s entry indicates it is disabled. If it is, the exec runs, if it is not, the exec ends early.

There’s one more attribute of the exec – require. We haven’t seen this yet. A require attribute is used to create ordering. In this case, it says that the user resource $id must exist before the exec resource can be applied. Read up on manifest ordering and the new MOAR feature. As always, check the style guide, there is another ordering command before, but require is much preferred.

New subclass

In our module’s manifest directory, add a new file called user.pp. We’ll implement the user definition from Joseft’s blog as base::user.

[root@puppet manifests]# pwd
/etc/puppet/modules/base/manifests
define base::user ( $state, $id, $uid, $gid, $pass, $realname, $sgroups) {
  user { $id:
    ensure => $state,
    uid => $uid,
    gid => $gid,
    shell => "/bin/bash",
    home => "/home/$id",
    comment => $realname,
    managehome => true,
    groups => $sgroups,
    password_max_age => '90',
  }

  case $::osfamily {
    RedHat: {$action = "/bin/sed -i -e 's/$id:!!:/$id:$pass:/g' /etc/shadow; chage -d 0 $id"}
    Debian: {$action = "/bin/sed -i -e 's/$id:x:/$id:$pass:/g' /etc/shadow; chage -d 0 $id"}
  }

  exec { "$action":
    path => "/usr/bin:/usr/sbin:/bin",
    onlyif => "egrep -q  -e '$id:!!:' -e '$id:x:' /etc/shadow",
    require => User[$id]
  }
}

Next, we have to implement the user. Here’s how the user dave is implemented with ::base::user. Note that the sgroups attribute, which is used to add additional groups to a user, is an empty array:

  ::base::user { 'dave':
    state    => 'present',
    id       => 'dave',
    uid      => '507',
    gid      => '507',
    pass     => '$1$qj3Ks0$pNT55P98zsdJE5GeRUdHh0',
    realname => 'Dave Smith',
    sgroups  => [],
  }

The question is, where to define the user. If you add this to site.pp, you will receive an error:

Error: Could not retrieve catalog from remote server: Error 400 on SERVER: Duplicate declaration: User[dave] i
s already declared in file /etc/puppet/modules/base/manifests/init.pp:54; cannot redeclare at /etc/puppet/mani
fests/site.pp:11 on node puppet.nelson.va

Even though we are using a different method to create a resource of type user, uniqueness still needs to be preserved. The ::base class is where we defined the user previously. Remove the previous user declaration and insert the ::base::user declaration in base/manifests/init.pp. This will create the user on all nodes that are assigned the ::base class. Here is the new init.pp (minus headers):

class base {

  include ::ssh
  ::ssh::server::configline { 'PermitRootLogin': value => 'yes' }

  class { '::ntp':
    servers => [ '0.pool.ntp.org', '2.centos.pool.ntp.org', '1.rhel.pool.ntp.org'],
  }

  ::base::user { 'dave':
    state    => 'present',
    id       => 'dave',
    uid      => '507',
    gid      => '507',
    pass     => '$1$qj3Ks0$pNT55P98zsdJE5GeRUdHh0',
    realname => 'Dave Smith',
    sgroups  => [],
  }

  group { 'dave':
    ensure => 'present',
    gid    => '507',
  }

}

If everything is correct, an agent test will update the comment field and password age for Dave Smith:

[root@puppet ~]# puppet agent --test
Info: Retrieving plugin
...
Info: Caching catalog for puppet.nelson.va
Info: Applying configuration version '1391737199'
Notice: /Stage[main]/Main/Node[puppet.nelson.va]/Base::User[dave]/User[dave]/comment: comment changed '' to 'Dave Smith'
Notice: /Stage[main]/Main/Node[puppet.nelson.va]/Base::User[dave]/User[dave]/password_max_age: password_max_age changed '99999' to '90'
Notice: Finished catalog run in 1.14 seconds

So far, so good!

Default passwords

We still have some work to do. Dave has a password we set. If we define additional users, what password will they get? The ::base::user definition will change the password to an encrypted password of our choice. Earlier, we copied Joseft’s encrypted password, and we don’t know what it is. We can see it in action, though. Delete dave with userdel -f dave and perform another agent test. The account will be recreated and the password changed. Check the bottom of /etc/shadow to see the password:

[root@puppet ~]# userdel -f dave
[root@puppet ~]# puppet agent --test
Info: Retrieving plugin
...
Info: Applying configuration version '1391738038'
Notice: /Stage[main]/Base/Group[dave]/ensure: created
Notice: /Stage[main]/Base/Base::User[dave]/User[dave]/ensure: created
Notice: /Stage[main]/Base/Base::User[dave]/Exec[/bin/sed -i -e 's/dave:!!:/dave:$1$qj3Ks0$pNT55P98zsdJE5GeRUdHh0:/g' /etc/shadow; chage -d 0 dave]/returns: executed successfully
Notice: Finished catalog run in 2.14 seconds
[root@puppet ~]# tail -1 /etc/shadow
dave:$1$qj3Ks0$pNT55P98zsdJE5GeRUdHh0:0:0:90:7:::

Let’s say you want the initial password for users to be “changeme123”. You can use mkpasswd at the CLI to generate the crypted string. The problem is that we performed a minimal install, so we do not have it. You can install expect, which will provide it, or you can use passwd to set dave’s password and then copy the string. Either way, grab the resulting string.

Open up base/manifests/init.pp. We will store the string in a variable $defaultpass and replace the string in the user definition with this variable. Don’t forget to escape the backslash in the string, or the sed search and replace will fail. Here is the pertinent section of the file:

  $defaultpass = '$6$nD909ONL$1qwS35SaB4TzatxANgnokos5AJ4gy6.E8eOKeIcOhHd\/V4eIFsyYSlvkB4f1G4ecvNXVSxx4UdQCRdS0dTBXX1'

  ::base::user { 'dave':
    state    => 'present',
    id       => 'dave',
    uid      => '507',
    gid      => '507',
    pass     => $defaultpass,
    realname => 'Dave Smith',
    sgroups  => [],
  }

Delete dave again and run your agent test. Ssh to localhost as dave with the password “changeme123”. You should be prompted to change your password.

[root@puppet ~]# userdel -f dave
[root@puppet ~]# puppet agent --test
Info: Retrieving plugin
...
Info: Applying configuration version '1391739551'
Notice: /Stage[main]/Base/Group[dave]/ensure: created
Notice: /Stage[main]/Base/Base::User[dave]/User[dave]/ensure: created
Notice: /Stage[main]/Base/Base::User[dave]/Exec[/bin/sed -i -e 's/dave:!!:/dave:$6$nD909ONL$1qwS35SaB4TzatxANgnokos5AJ4gy6.E8eOKeIcOhHd\/V4eIFsyYSlvkB4f1G4ecvNXVSxx4UdQCRdS0dTBXX1:/g' /etc/shadow; chage -d 0 dave]/returns: executed successfully
Notice: Finished catalog run in 1.29 seconds
[root@puppet ~]# ssh dave@localhost
The authenticity of host 'localhost (::1)' can't be established.
RSA key fingerprint is bb:6c:9e:12:4b:8e:f4:ae:74:02:80:d4:58:3b:41:b7.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'localhost' (RSA) to the list of known hosts.
dave@localhost's password:
You are required to change your password immediately (root enforced)
WARNING: Your password has expired.
You must change your password now and login again!
Changing password for user dave.
Changing password for dave.
(current) UNIX password:

Earlier, I mentioned the lack of a group resource in the user define. There’s a reason – you don’t absolutely need the group. When you create a user, an eponymous group is created by default. Let’s try this out by removing the group definition from our module, as well as the gid from the user definition and declaration, and see how we fare. Here’s the updated init.pp and user.pp:

[root@puppet ~]# cat /etc/puppet/modules/base/manifests/init.pp
...
class base {

  include ::ssh
  ::ssh::server::configline { 'PermitRootLogin': value => 'yes' }

  class { '::ntp':
    servers => [ '0.pool.ntp.org', '2.centos.pool.ntp.org', '1.rhel.pool.ntp.org'],
  }

  $defaultpass = '$6$nD909ONL$1qwS35SaB4TzatxANgnokos5AJ4gy6.E8eOKeIcOhHd\/V4eIFsyYSlvkB4f1G4ecvNXVSxx4UdQCRdS0dTBXX1'

  ::base::user { 'dave':
    state    => 'present',
    id       => 'dave',
    uid      => '507',
    pass     => $defaultpass,
    realname => 'Dave Smith',
    sgroups  => [],
  }

}
[root@puppet ~]# cat /etc/puppet/modules/base/manifests/user.pp
define base::user ( $state, $id, $uid, $pass, $realname, $sgroups) {
  user { $id:
    ensure => $state,
    uid => $uid,
    shell => "/bin/bash",
    home => "/home/$id",
    comment => $realname,
    managehome => true,
    groups => $sgroups,
    password_max_age => '90',
  }

  case $::osfamily {
    RedHat: {$action = "/bin/sed -i -e 's/$id:!!:/$id:$pass:/g' /etc/shadow; chage -d 0 $id"}
    Debian: {$action = "/bin/sed -i -e 's/$id:x:/$id:$pass:/g' /etc/shadow; chage -d 0 $id"}
  }

  exec { "$action":
    path => "/usr/bin:/usr/sbin:/bin",
    onlyif => "egrep -q  -e '$id:!!:' -e '$id:x:' /etc/shadow",
    require => User[$id]
  }
}
[root@puppet ~]# userdel -f dave
[root@puppet ~]# puppet agent --test
Info: Retrieving plugin
...
Info: Caching catalog for puppet.nelson.va
Info: Applying configuration version '1391740077'
Notice: /Stage[main]/Base/Base::User[dave]/User[dave]/ensure: created
Notice: /Stage[main]/Base/Base::User[dave]/Exec[/bin/sed -i -e 's/dave:!!:/dave:$6$nD909ONL$1qwS35SaB4TzatxANgnokos5AJ4gy6.E8eOKeIcOhHd\/V4eIFsyYSlvkB4f1G4ecvNXVSxx4UdQCRdS0dTBXX1:/g' /etc/shadow; chage -d 0 dave]/returns: executed successfully
Notice: Finished catalog run in 1.42 seconds
[root@puppet ~]# groups dave
dave : dave

If you need the flexibility of defining a non-eponymous group or a group id that isn’t the same as the user id for users, then you’ll need to add a group resource to the definition. Since we don’t need that currently, we’ll leave our module the way it is now.

Hopefully in a production environment you have access to LDAP or ADS for authentication and your templates will be configured to use it. If you do need some local users – or need to manage some built-in or application accounts, like root, apache, mysql, or puppet – this solution is acceptable. Be sure to force your users to change their passwords promptly and protect the unencrypted string.

Conclusion

We’ve created a module and started organizing our unwieldy site manifest into three separate components: the site manifest that declares the per-node classes, the module/class ::base that all nodes should inherit, and the module’s define ::base::user. In the next article, we’ll start adding the agents to the network with their own manifests.

3 thoughts on “Manifest and Module Organization, Take One

  1. Finally!!! Someone explained clearly the how modules and file names and classes within classes are defined! Took me forever to finally find someone who explained it all together!

  2. “The onlyif attribute is another command handed to the system. If it returns a 0, the exec is skipped. If it returns any other exit code, the exec is run. ”
    Shouldn’t that be, “If it returns 0, the exec is run,. If it returns any other exit code, the exec is skipped.” ?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s