Defined Resource Types

Quest objectives

  • Understand how to manage multiple groups of resources with defined resource types.
  • Use a defined resource type to easily create home pages for users.

Getting Started

In the quest on parameterized classes, you saw how you can use parameters to customize a class as it is declared. If you recall that classes, like resources, can only be realized a single time in a given catalog, you might be wondering what to do if you want Puppet to repeat the same pattern multiple times, but with different parameters.

In most cases, the simplest answer is the defined resource type. A defined resource type is a block of Puppet code that can be declared multiple times with different parameter values. Once defined, a defined resource type looks and acts just like the core resource types you're already familiar with.

In this quest, you will create a defined resource type for a web_user. This will let you bundle together the resources you need to create a user along with their personal web homepage. This way you can handle everything with a single resource declaration.

When you're ready to get started, type the following command:

quest begin defined_resource_types

Defined resource types

Repetition - that is the actuality and the earnestness of existence.

-Søren Kierkegaard

While you can do quite a bit with Puppet's core resource types, you're sure to find sooner or later that you need to do things that don't fit well into Puppet's existing set of core resource types. In the MySQL quest, you encountered a few custom resource types that allowed you to configure MySQL grants, users, and databases. The puppetlabs-mysql module includes Ruby code that defines the behavior of these custom resource types and the providers that implement them on a system.

Writing custom providers, however, is a significant commitment. When you start writing your own providers, you're taking on responsibility for all the abstraction Puppet uses to handle the implementation of that resource on diverse operating systems and configurations. Though this kind of project can be a great contribution to the Puppet community, it's not generally appropriate for a one-off solution.

Puppet's defined resource types are a lightweight alternative. Though they don't have the same power to define wholly new functionality, you may be surprised at how much can be achieved by bundling together Puppet's core resource types and those provided by existing modules from the community.

Task 1:

To get started, let's create the module structure where we'll put our web_user module.

Make sure you're in your modules directory:

cd /etc/puppetlabs/code/environments/production/modules

And create the directories for your new module. We'll call it web_user.

mkdir -p web_user/{manifests,examples}

Before we go into the details of what we're going to do with this module, though, let's write a simple defined resource type so you can see what the syntax looks like. For now, we'll create a user and a home directory for that user. Normally, you could use the managehome parameter to tell Puppet to manage the user's home directory, but we want a little more control over the permissions of this home directory, so we'll do it ourselves.

Task 2:

Go ahead and create a user.pp manifest where we'll define our defined resource type:

vim web_user/manifests/user.pp

We'll start simple. Enter the following code in your manifest, paying careful attention to the syntax and variables.

define web_user::user {
  $home_dir = "/home/${title}"
  user { $title:
    ensure => present,
  }
  file { $home_dir:
    ensure  => directory,
    owner   => $title,
    group   => $title,
    mode    => '0755',
  }
}

What did you notice? First, you probably realized that this syntax is nearly identical to that you would use for a class. The only difference is that you use the define keyword instead of class.

Like a class, a defined resource type brings together a collection of resources into a configurable unit. The key difference is that, as we mentioned, a defined resource type can be realized multiple times on a single system, while classes are always singleton.

This brings us to the second feature of the code you may have noticed. We use the $title variable in several places, though we haven't explicitly assigned it! Also notice that this $title variable is used in the titles of both the user and file resources we're declaring. What's going on here?

Task 3:

To understand the importance of this title variable in a defined resource type, go ahead and create a test manifest:

vim web_user/examples/user.pp

Declare a web_user::user resource

web_user::user { 'shelob': }

Here, we assign the title (in this case shelob), as we would for any other resource type. This title is passed through to our defined resource type as the $title variable. You may recall from the Resources quest that the title of a resource must be unique, as it's the key Puppet uses to refer to a resource internally. When you create a defined resource type, you must ensure that all the included resources are given a title unique to their type. The best way to do that is to pass the $title variable into the title of each resource. Though the title of the file resource you declared for your user's home directory is set to the $home_dir variable, this variable is assigned a string that includes the $title variable: "/home/${title}"

You might also be wondering about the lack of parameters. If a resource or class has no parameters or has acceptable defaults for all of its parameters, it is possible to declare it in this brief form without the list of parameter key value pairs. (You will see this less often in the case of classes, as the idempotent include syntax is almost always preferred.)

Task 4:

Go ahead and run a --noop, then apply your test manifest:

puppet apply web_user/examples/user.pp

Now take a look at the /home directory:

ls -la /home

You should now see a home directory for shelob with the permissions you specified:

drwxr-xr-x   4 shelob    shelob    4096 Nov  4 18:20 shelob

Public HTML homepages

Now that you've seen a simple example of the syntax for a defined resource type, let's do something a little more useful with it.

We've already configured the Nginx server hosting the Quest Guide to alias any location beginning with a ~ to a public_html directory in the corresponding user's home directory.

You don't need to understand the details of this configuration for this quest. That said, the Puppet code we used for this configuration is a real-world example of a defined resource type, so it's worth taking a quick look. The defined resource type we used comes from the jfryman-nginx module. We declared it with a few parameters to set up a location that will automatically deal with our special ~ pages. Don't worry about the scary-looking regular expression in the title. That's specific to how our Nginx configuration works, and nothing you need to understand to use defined resource types in general.

nginx::resource::location { '~ ^/~(.+?)(/.*)?$':
  vhost          => '_',
  location_alias => '/home/$1/public_html$2',
  autoindex      => true,
}

That regular expression in the title (~ ^/~(.+?)(/.*)?$) captures any URL path segment preceded by a ~ as a first capture group, then the remainder of the URL path as a second capture group. It then maps that first group to to a user's home directory, and the rest to the contents of that user's public_html directory. So /~username/index.html will correspond to /home/username/public_html/index.html.

If you're interested, you can check the _.conf file to see how this defined resource type is translated into a location block in our Nginx configuration file:

cat /etc/nginx/sites-enabled/_.conf

Task 5:

So let's see about giving our web_user::user resource a public_html directory and a default index.html page. We'll need to add a directory and a file. Because the parameters for our public_html directory will be identical to those of the home directory, we can use an array to declare both at once. Note that Puppet's autorequires will take care of the ordering in this case, ensuring that the home directory is created before the public_html directory it contains.

We'll set the replace parameter for the index.html file to false. This means that Puppet will create that file if it doesn't exist, but won't replace an existing file. This will allow us to create a default page for the user, but will allow the user to replace that default content without having it over-written again on the next Puppet run.

Finally, we can use string interpolation to customize the default content of the user's home page. (Puppet also supports .erb and .epp style templates, which would give us a more powerful way to customize a page. We haven't covered templates, though, so string interpolation will have to do!)

Reopen your manifest:

vim web_user/manifests/user.pp

And add code to configure your user's public_html directory and default index.html file:

define web_user::user {
  $home_dir    = "/home/${title}"
  $public_html = "${home_dir}/public_html"
  user { $title:
    ensure     => present,
  }
  file { [$home_dir, $public_html]:
    ensure  => directory,
    owner   => $title,
    group   => $title,
    mode    => '0755',
  }
  file { "${public_html}/index.html":
    ensure  => file,
    owner   => $title,
    group   => $title,
    replace => false,
    content => "<h1>Welcome to ${title}'s home page!</h1>",
    mode    => '0644',
  }
}

Task 6:

Use the puppet parser validate tool to check your manifest, then run a --noop before applying your test manifest again:

puppet apply web_user/examples/user.pp

Once the Puppet run completes, take a look at your user's new default at <VM'S IP>/~shelob/index.html.

Parameters

As it is, your defined resource type doesn't give you any way to specify anything other than the resource title. Using parameters, we can pass some more information through to the contained resources to customize them to our liking. Let's add some parameters that will allow us to set a password for the user and use some custom content for the default web page.

Task 7:

The syntax for adding parameters to defined resource types is just like that used for parameterized classes. Within a set of parentheses before the opening brace of the definition, include a comma separated list of the variables to be defined by parameters. The = operator can optionally be used to assign default values.

  define web_user::user (
    $content  = "<h1>Welcome to ${title}'s home page!</h1>",
    $password = undef,
  ) {

There are a couple of details you should be sure to notice here.

First, though we're using the $title variable to set the default for content, we cannot use the value of one parameter to set the default for another. Binding of these parameters to their values happens in parallel, not sequentially. Any assignment that relies on the values of other parameters must be handled within the body of the defined resource type. The $title variable is assigned prior to the binding of other parameters, so it is an exception.

Second, we've given the $password parameter the special value of undef as a default. Any parameter without a default value specified will cause an error if you declare your defined resource type without specifying a value for that parameter. If we left the $password parameter without a default, you would always have to specify a password. For the underlying user resource type, however, the password parameter is actually optional on Linux systems. By using the special undef value as a default, we can explicitly tell Puppet to treat that value as undefined, and act as if we simply hadn't included it in our list of key value pairs for our user resource.

Now that you have these parameters set up, go ahead and update the body of your defined resource type to make use of them.

define web_user::user (
  $content  = "<h1>Welcome to ${title}'s home page!</h1>",
  $password = undef,
) {
  $home_dir    = "/home/${title}"
  $public_html = "${home_dir}/public_html"
  user { $title:
    ensure   => present,
    password => $password,
  }
  file { [$home_dir, $public_html]:
    ensure => directory,
    owner  => $title,
    group  => $title,
    mode    => '0755',
  }
  file { "${public_html}/index.html":
    ensure  => file,
    owner   => $title,
    group   => $title,
    replace => false,
    content => $content,
    mode    => '0644',
  }
}

Task 8:

Edit your test manifest, and add a new user to try this out:

web_user::user { 'shelob': }
web_user::user { 'frodo':
  content  => 'Custom Content!',
  password => pw_hash('sting', 'SHA-512', 'mysalt'),
}

Note that we're using the pw_hash function to generate a SHA-512 hash from the password 'sting' and salt 'mysalt'.

Task 9:

Once you've made your changes, do a --noop run, then apply your test manifest:

puppet apply web_user/examples/user.pp

Once the Puppet run completes, check your new user's page at <VM'S IP>/~frodo/index.html.

Review

In this quest, we introduced defined resource types, a lightweight and repeatable way to bundle a group of resource declarations into a repeatable and configurable group.

We covered a few key details you should keep in mind when you're working on a defined resource type:

  • Defined resource type definitions use similar syntax to class declarations, but use the define keyword instead of class.
  • Use the $title variable in constituent resource titles to ensure uniqueness.
  • A resource with no parameters can be declared in the short type { 'title': } form.
  • Binding of parameter variables to values happens in parallel, meaning that you cannot use the value of one parameter to set another. The exception is the $title variable.
  • Parameters without a default value are required when you declare a defined resource type. However, you can use the undef value as a default if you want to allow a value to remain unspecified.

results matching ""

    No results matching ""