0

As I work with puppet, I find myself wanting to automate more complex setups, for example vhosts for X number of websites. As my puppet manifests get more complex I find it difficult to apply the DRY (don't repeat yourself) principle. Below is a simplified snippet of what I am after, but doesn't work because puppet throws various errors depending up whether I use classes or defines. I'd like to get some feed back from some seasoned puppetmasters on how they might approach this solution.

# site.pp
import 'nodes'

# nodes.pp
node nodes_dev {
    $service_env = 'dev'
}

node nodes_prod {
    $service_env = 'prod'
}

import 'nodes/dev'
import 'nodes/prod'

# nodes/dev.pp
node 'service1.ownij.lan' inherits nodes_dev {
    httpd::vhost::package::site { 'foo': }
    httpd::vhost::package::site { 'bar': }
}

# modules/vhost/package.pp
class httpd::vhost::package {

    class manage($port) {

        # More complex stuff goes here like ensuring that conf paths and uris exist
        # As well as log files, which is I why I want to do the work once and use many

        notify { $service_env: }
        notify { $port: }
    }    

    define site {

        case $name {

            'foo': {

                class 'httpd::vhost::package::manage':
                    port => 20000
                }
            }

            'bar': {

                class 'httpd::vhost::package::manage':
                    port => 20001
                }
            }
        }
    }
}

That code snippet gives me a Duplicate declaration: Class[Httpd::Vhost::Package::Manage] error, and if I switch the manage class to a define, and attempt to access a global or pass in a variable common to both foo and bar, I get a Duplicate declaration: Notify[dev] error.

Any suggestions how I can implement the DRY principle and still get puppet to work?

-- UPDATE --

I'm still having a problem trying to ensure that some of my vhosts, which may share a parent directory, are setup correctly. Something like this:

node 'service1.ownij.lan' inherits nodes_dev {
    httpd::vhost::package::site { 'foo_sitea': }
    httpd::vhost::package::site { 'foo_siteb': }
    httpd::vhost::package::site { 'bar': }
}

What I need to happen is that sitea and siteb have the same parent "foo" folder. The problem I am having is when I call a define to ensure the "foo" folder exists. Below is the site define as I have it, hopefully it will make sense what I am trying to accomplish.

class httpd::vhost::package {

    File {
        owner   => root,
        group   => root,
        mode    => 0660
    }

    define site() {

        $app_parts = split($name, '[_]')

        $app_primary = $app_parts[0]

        if ($app_parts[1] == '') {
            $tpl_path_partial_app = "${app_primary}"
            $app_sub = ''
        } else {
            $tpl_path_partial_app = "${app_primary}/${app_parts[1]}"
            $app_sub = $app_parts[1]
        }

        include httpd::vhost::log::base

        httpd::vhost::log::app { $name:
            app_primary => $app_primary,
            app_sub     => $app_sub
        }
    }
}

class httpd::vhost::log {

    class base {

        $paths = [ '/tmp', '/tmp/var', '/tmp/var/log', '/tmp/var/log/httpd', "/tmp/var/log/httpd/${service_env}" ]

        file { $paths:
            ensure  => directory
        }
    }

    define app($app_primary, $app_sub) {

        $paths = [ "/tmp/var/log/httpd/${service_env}/${app_primary}", "/tmp/var/log/httpd/${service_env}/${app_primary}/${app_sub}" ]

        file { $paths:
            ensure  => directory
        }
    }
}

The include httpd::vhost::log::base works fine, because it is "included", which means it is only implemented once, even though site is called multiple times. The error I am getting is: Duplicate declaration: File[/tmp/var/log/httpd/dev/foo]. I looked into using exec, but not sure this is the correct route, surely others have had to deal with this before and any insight is appreciated as I have been grappling with this for a few weeks. Thanks.

Mike Purcell
  • 1,708
  • 7
  • 32
  • 54

1 Answers1

3

First off - you should definitely be using define for httpd::vhost::package::manage, not class, since it's going to be defined multiple times for the same node.

notify { $service_env: } will always blow things up, since $service_env is dev for both calls of the define. You can't ever declare the same resource a second time with the same name.

You can fix this by changing over to something like notify { "${service_env}-${port}": }, but you'll likely run into other issues if this wasn't built properly to handle being set up as a defined type.

The class structure getting all the way out to httpd::vhost::package::manage is strange - I'd recommend really slimming things down to a class structure that'll simplify things and allow you to have only the resources that're needed.

httpd (init.pp - have it include the install, config, service classes)
httpd::install (install.pp - have it define Package[httpd] to install)
httpd::service (service.pp - have it define Service[httpd]; the vhost defines can
                notify this to update)
httpd::config (config.pp - any configuration, such as handling of httpd.conf,
               that won't be per-vhost)
httpd::vhost (vhost.pp - the defined type that can be used multiple times.  Have
              it include httpd so that the base classes are handled, and
              notify httpd::service from your vhost config file resources)

A simple example of vhost management is actually the example for defined types in the documentation - let me know if you need any other clarification on how to set this up to be a nice reusable bit of config!

Edit:

Update for the shared parent directories problem.

Managing a file from two separate places is a pain in Puppet, and it's given me problems before too. Especially frustrating when there's no conflict between the two resource declarations like in your current conundrum.

You're right that an exec resource is likely your best answer.

define app($app_primary, $app_sub) {
    exec { "mkdir_${service_env}_${app_primary}_${app_sub}":
        command  => "/bin/mkdir -p /tmp/var/log/httpd/${service_env}/${app_primary}/${app_sub}",
        creates  => "/tmp/var/log/httpd/${service_env}/${app_primary}/${app_sub}",
        require  => Class["httpd::vhost::log::base"],
    }
}

This works for both cases (with and without a subdirectory) because you're setting $app_sub to an empty string if it's not in use.

I'd argue that it's probably a lot cleaner to just not share a directory tree (use log/httpd/foo_sitea as the log dir instead of log/httpd/foo/sitea), but this should do the trick. Splitting the string and behaving differently based on that, and all these separate defines when this can really just live in the site class.. make sure to keep it simple!

Shane Madden
  • 114,520
  • 13
  • 181
  • 251
  • @MikePurcell I've updated my answer for your new question. – Shane Madden Nov 04 '12 at 19:35
  • Thanks for the additional info. It's unfortunate that puppet has this type of restriction, where it's not easy to ensure that a parent directory is available to multiple, shared child directories. Never liked using apis that dictated implementation. However it's far better than having to write shell scripts to do the same. – Mike Purcell Nov 05 '12 at 05:26
  • 1
    You can use `if ! defined(File[$paths]) { file { $paths: ensure => directory } }` to declare a parent multiple times. If the resource has not been declared, it gets declared; else, nothing happens. – Evan Feb 28 '13 at 05:54