Authors: Jeffrey McCune James Turnbull
In this section we're going to show you a slightly more complex type and provider used to manage HTTP authentication password files. It's a similarly ensureable type and provider, but with some more sophisticated components.
Let's start by looking at thehttpauth
type shown in
Listing 10-10
.
Listing 10-10.
The httpauth type
Puppet::Type.newtype(:httpauth) do
@doc = "Manage HTTP Basic or Digest password files." +
" httpauth { 'user': " +
" file => '/path/to/password/file', " +
" password => 'password', " +
" mechanism => basic, " +
" ensure => present, " +
" } "
ensurable do
newvalue(:present) do
provider.create
end
newvalue(:absent) do
provider.destroy
end
defaultto :present
end
newparam(:name) do
desc "The name of the user to be managed."
isnamevar
end
newparam(:file) do
desc "The HTTP password file to be managed. If it doesn't exist it is created."
end
newparam(:password) do
desc "The password in plaintext."
end
newparam(:realm) do
desc "The realm - defaults to nil and mainly used for Digest authentication."
defaultto "nil"
end
newparam(:mechanism) do
desc "The authentication mechanism to use - either basic or digest. Default to basic."
newvalues(:basic, :digest)
defaultto :basic
end
# Ensure a password is always specified
validate do
raise Puppet::Error, "You must specify a password for the user." unless @parameters.include?(:password)
end
end
In thehttpauth
type we're managing a number of attributes, principally the user, password and password file. We also provide some associated information, like the realm (A HTTP Digest Authentication value) and the mechanism we're going to use, Basic or Digest Authentication.
First, notice that we've added some code to ourensurable
method. In this case, we're telling Puppet some specifics about the operation of ourensure
attribute. We're specifying that for each state,present
andabsent
, exactly which method in the provider should be called, herecreate
anddestroy,
respectively. We're also specifying the default behavior of theensure
attribute. This means that if we omit theensure
attribute that thehttpauth
resource will assumepresent
as the value. The resource will then check for the presence of the user we want to manage, and if it doesn't exist, then it will create that user.
We've also used some other useful methods. The first is thedefaultto
method that specifies a default value for a parameter or property. If the resource does not specify this attribute, then Puppet will use to this default value to populate it. The other is thenewvalues
method that allows you to specify the values that the parameter or property will accept. In
Listing 10-10
, you can see themechanism
parameter that thenewvalues
method specifies will take the values ofbasic
ordigest
.
Lastly, you can see that we used thevalidate
method to return an error if thehttpauth
resource is specified without thepassword
attribute.
Now let's look at the provider for thehttpauth
type, shown in
Listing 10-11
.
Listing 10-11.
The httpauth provider
begin
require 'webrick'
rescue
Puppet.warning "You need WEBrick installed to manage HTTP Authentication files."
end
Puppet::Type.type(:httpauth).provide(:httpauth) do
desc "Manage HTTP Basic and Digest authentication files"
def create
# Create a user in the file we opened in the mech method
@htauth.set_passwd(resource[:realm], resource[:name], resource[:password])
@htauth.flush
end
def destroy
# Delete a user in the file we opened in the mech method
@htauth.delete_passwd(resource[:realm], resource[:name])
@htauth.flush
end
def exists?
# Check if the file exists at all
if File.exists?(resource[:file])
# If it does exist open the file
mech(resource[:file])
# Check if the user exists in the file
cp = @htauth.get_passwd(resource[:realm], resource[:name], false)
# Check if the current password matches the proposed password
return check_passwd(resource[:realm], resource[:name], resource[:password], cp)
else
# If the file doesn't exist then create it
File.new(resource[:file], "w")
mech(resource[:file])
return false
end
end
# Open the password file
def mech(file)
if resource[:mechanism] == :digest
@htauth = WEBrick::HTTPAuth::Htdigest.new(file)
elsif resource[:mechanism] == :basic
@htauth = WEBrick::HTTPAuth::Htpasswd.new(file)
end
end
# Check password matches
def check_passwd(realm, user, password, cp)
if resource[:mechanism] == :digest
WEBrick::HTTPAuth::DigestAuth.make_passwd(realm, user, password) == cp
elsif resource[:mechanism] == :basic
# Can't ask webbrick as it uses a random seed
password.crypt(cp[0,2]) == cp
end
end
end
This provider is more complex than what we've seen before. We've still got the methods that handle Puppet's ensurable capabilities:create
,destroy
andexists?
. In addition, though, we've got additional methods that manipulate our password files.
Our provider first checks for the existence of the Webrick library, which it needs in order to manipulate HTTP password files. The provider will fail to run if this library is not present. Fortunately, Webrick is commonly present in most Ruby distributions (and indeed, is used by Puppet as its basic server framework, as we learned in 2).
Tip
As an alternative to requiring the Webrick library, we could use Puppet's feature capability. You can see some examples of this in
https://github.com/puppetlabs/puppet/blob/master/lib/puppet/feature/base.rb.
This capability allows you to enabled or disable features based on whether certain capabilities are present or not. The obvious limitation is that this approach requires adding a new feature to Puppet's core, rather than simply adding a new type or provider.
Our provider then has the three ensurable methods. Thecreate
anddestroy
methods are relatively simple. They use methods from the Webrick library to either set or delete a password specified in the HTTP password file managed by the resource. The file being referred to here using theresource[:file]
value which is controlled by setting thefile
attribute in thehttpauth
resource, for example:
httpauth { “bob”:
file => “/etc/apache2/htpasswd.basic”,
password => “password”,
mechanism => basic,
}
Lastly, you'll also see in thecreate
anddestroy
methods that we call theflush
method. This flushes the buffer and writes out our changes.
Theexists?
method is more complex and calls several helper methods to check whether the user and password already exist, and if they do, whether the current and proposed passwords match.
Like facts, you can test your types and providers. The best way to do this is add them to a module in your development or testing environment and enable pluginsync to test them there before using them in your production environment, for example let's add our HTTPAuth type to a module called httpauth, first adding the required directories:
$ mkdir –p /etc/puppet/modules/httpauth/(manifests,files,templates,lib}
$ mkdir –p /etc/puppet/modules/httpauth/lib/{type,provider}
$ mkdir –p /etc/puppet/modules/httpauth/lib/provider/httpauth
Then copying in the type and provider to the requisite directories.
# cp type/httpauth.rb /etc/puppet/modules/lib/type/httpauth.rb
# cp provider/httpauth.rb /etc/puppet/modules/lib/provider/httpauth/httpauth.rb
When Puppet is run (and pluginsync enabled) it will find your types and providers in these directories, deploy them and make them available to be used in your Puppet manifests.
The last type of custom Puppet code we're going to look at is the function. You've seen a number of functions in this book already, for example:include
,notice
andtemplate
are all functions we've used. But you can extend the scope of the available functions by writing your own.
There are two types of functions: statements and rvalues. Statements perform some action, for example thefail
function, and rvalues return a value, for example if you pass in a value, the function will process it and return a value. Thesplit
function is an example of an rvalue function.
Note
Remember that functions are executed on the Puppet master. They only have access to resources and data that are contained on the master.
We're going to write a simple function and distribute it to our agents. Like plug-ins, we can use plug-in sync to distribute functions to agents; they are stored in:
custom/lib/puppet/parser/functions
The file containing the function must be named after the function it contains; for example, thetemplate
function should be contained in thetemplate.rb
file.
Let's take a look at a simple function in
Listing 10-12
.
Listing 10-12.
The SHA512 function
Puppet::Parser::Functions::newfunction(:sha512, :type => :rvalue, :doc => "Returns a SHA1
hash value from a provided string.") do |args|
require 'sha1'
Digest::SHA512.hexdigest(args[0])
end
Puppet contains an existing function called sha1 that generates a SHA1 hash value from a provided string. In
Listing 10-12
, we've updated that function to support SHA512 instead. Let's break that function down. To create the function we call thePuppet::Parser::Functions::newfunction
method and pass it some values. First, we name the function, in our casesha512
. We then specify the type of function it is, here rvalue, for a function that returns a value. If we don't specify the type at all then Puppet assumes the function is a statement. Lastly, we specify a:doc
string to document the function.
Thenewfunction
block takes the incoming argument and we process it, first adding in support for working with SHA hashes by requiring thesha1
library, and then passing the argument to thehexdigest
method. As this is an rvalue function, it will return the created hash as the result of the function.
Note
The last value returned by thenewfunction
block will be returned to Puppet as the rvalue.