Archive for the 'Puppet' Category

11
Feb
11

Self-Classifying Puppet Nodes

Puppet has a  very cool node classification system which pretty much lets you do what you want (by writing your own one) if the default classifier doesn’t work for you. So, there are already a couple of good posts around this, and its worth reading some of the following posts: Jordan Sissel , Gary Larizza as well as the official docs on external node classifiers.

So, from the above posts, I’m going to take a few of the ideas, mix them up, and go through the steps to reproduce on your own system. The goal of the end configuration is to have a node come online, identify itself using it’s Role, Platform, and Environment; and then issue it the relevant classes. What’s important here, is that nodes must be classified, before they reach puppet, into Roles and Platforms (as well as Environment, but this is already handled by puppet). Dividing nodes by their Platform/Role gives us the simplicity needed when you’re managing a large number of machines across different clusters. Its easier to group machines than it is to individually assign classes to each node. Of course, not all your puppetized nodes need to belong to a group as they might just be one machine performing a specific action. In cases like this, we must be able to add exceptions easily.

For the purpose of this post, let’s assume we have 2 clusters in Europe and USA, and each cluster has several Application and Web Servers. I’m also assuming you’re following the recommended puppet-mcollective-facter setup, because it works well.

From a high level overview, we want to write a facter plugin for mcollective which will read a facts file on the host. This facts file will contain the Role and Platform information that can be used from Puppet. We then need an mcollective agent so we can update this file if we need to at a later stage. Finally we look at how to create node classification system that can use these facts to hand out the right manifest.

Identification

Facter is the game, and we need a new fact. So facter gives puppet access to information about a host at run-time like what country a host is in or what distribution of linux it’s running. We’re going to put 2 new facts, and for the sake of best practices, we’ll make it extensible. I don’t like polluting the existing facter namespace with odd names, and so i’m going to prefix all facts with a name (use your company name or whatever you want).

The following is a facter plugin that will parse the file /etc/company.facts and append them to the existing facts.


require 'facter'

if File.exist?("/etc/company.facts")
    File.readlines("/etc/company.facts").each do |line|
        if line =~ /^(.+)=(.+)$/
            var = "company_"+$1.strip; 
            val = $2.strip

            Facter.add(var) do
                setcode { val }
            end
        end
    end
end

Given the following facts file /etc/company.facts:

Role = Web
Platform = USA

We will get the following from facter

...
company_role = Web
company_platform = USA
...

These variables are now available straight away in your puppet manifests.

Updating the Facts
Before i continue on using these facts in puppet, its important to have a way to update the facts. Equally important is that you implement the facts into your server deploy process. So, we have a script that installs mcollective and puppet when we commission a new server, and one of the first things that is done is to create this file and automatically populate the Role and Platform based on the commissioning paramaters.

Apart from server deploy-time, we can write a small mcollective RPC agent which will get/set/delete values from our facts file. The file has a simple key-value structure and so the following should do the job

module MCollective
	module Agent
		class Companyfact<RPC::Agent
			metadata	:name		=> "Company Fact Agent",
					:description	=> "Key/values in a text file",
					:author		=> "Puppet Master Guy",
					:license	=> "GPL",
					:version	=> "Version 1",
					:url		=> "www.company.com",
					:timeout	=> 10
			
			companyfile = "/etc/company.facts"
	
			def parse_facts(fname)
				begin
					if File.exist?(fname)
						kv_map = {}
						File.readlines(fname).each do |line|
							if line =~ /^(.+)=(.+)$/	
								@key = $1.strip;				 
								@val = $2.strip				  
								kv_map.update({@key=>@val})
							end						 
						end					 
						return kv_map
					else
						f = File.open(fname,'w')
						f.close
						return {}
					end 			
				rescue
					logger.warn("Could not access company facts file. There was an error in companyfacts.rb:parse_facts")
					return {}
				end
			end

			def write_facts(fname, facts)

				if not File.exists?(File.dirname(fname))
 				   Dir.mkdir(File.dirname(fname))
				end

				begin
					f = File.open(fname,"w+")
					facts.each do |k,v|
						f.puts("#{k} = #{v}")
					end
					f.close
					return true
				rescue
					return false
				end
			end

			action "get" do
				validate :key, String
				
				kv_map = parse_facts(companyfile)
				if kv_map[request[:key]] != nil
					reply[:value] = kv_map[request[:key]]
				end
			end

			action "put" do
				validate :key, String
				validate :value, String

				kv_map = parse_facts(companyfile)
				kv_map.update({request[:key] => request[:value]})

				if write_facts(companyfile,kv_map)
					reply[:msg] = "Settings Updated!"
				else
					reply.fail!  "Could not write file!"
				end

			end
			action "delete" do
				validate :key, String

				kv_map = parse_facts(companyfile)	
				kv_map.delete(request[:key])

				if write_facts(companyfile,kv_map)
					reply[:msg] = "Setting deleted!"
				else
					reply.fail!  "Could not write file!"
				end

			end
		end
	end
end

We also need the ddl:

metadata        :name           => "Company Fact Agent",
		:description    => "Key/values in a text file",
		:author         => "Puppet Master Guy",
		:license        => "GPL",
		:version        => "Version 1",
		:url            => "www.company.com",
		:timeout        => 10

action "get",	:description => "fetches a value from a file" do
	display :failed

	input :key,
		:prompt		=> "Key",
		:description	=> "Key you want from the file",
		:type		=> :string,
		:validation	=> '^[a-zA-Z0-9_]+$',
		:optional	=> false,
		:maxlength	=> 90
	
	output :value,
		:description	=> "Value",
		:display_as	=> "Value" 
end

action "put", :description = "Value to add to file" do
	display :failed

	input :key,
		:prompt		=> "Key",
		:description	=> "Key you want to set in the file",
		:type 		=> :string,
		:validation	=> '^[a-zA-Z0-9_]+$',
		:optional	=> false,
		:maxlength	=> 90

	input :value,
                :prompt         => "Value",
                :description    => "Value you want to set in the file",
                :type           => :string,
                :validation     => '^[a-zA-Z0-9_]+$',
                :optional       => false,
                :maxlength      => 90

	output :msg,
		:description	=> "Status",
		:display_as	=> "Status"
end

action "delete", :description = "Delete a key/value pair if it exists" do
        display :failed

        input :key,
                :prompt         => "Key",
                :description    => "Key you want to change in the file",
                :type           => :string,
                :validation     => '^[a-zA-Z0-9_]+$',
                :optional       => false,
                :maxlength      => 90

        output :msg,
                :description    => "Status",
                :display_as     => "Status"
end

For a quick refresh on using your mc-rpc agent, we can set a key using the following:
mc-rpc -v --agent companyfact --action put --argument key=role --argument value=Web

And we can get a key using the following
mc-rpc -v --agent companyfact --action get --argument key=role

And we can delete a key using the following
mc-rpc -v --agent companyfact --action delete --argument key=role

Self-Classifying Nodes
This is where we want to be. A node comes in and says to puppet, I’m a Web machine on platform USA.

The default basic setup is to use a node definition for each node, or plug some sort of external classifier on. I’m going to build on from Jordan Sissel’s blog that I mentioned at the start. Essentially, every node goes through the ‘default’ node definition, which then goes to the ‘truth enforcer’. This truth enforcer will look at the facts of the node and hand off the relevant classes accordingly. Note that if you want to add exceptions, just create a node definition for the exception node. simple.

So the enforcer node is a very basic definition:

node default {
  include truth::enforcer
}

From here, we create a truth enforcer class like so (using our example). Naturally this is just an example of how it might be used:

class truth::enforcer {

        $groupname = "$company_platform:$company_role"
        case $groupname {
                "USA:Web" : {
                        include roles::web
                }
        }

        case $company_role {
                "Application" : {
                        include roles::application
                }
        }       
}

That’s pretty much it as far as getting a self-classifying puppet node goes. One more thing that’s worth mentioning is that this also ties in well with Extlookup to manage your parameters. You can use something like the following configuration which I find works well:

$extlookup_precedence = ["fqdn_%{fqdn}", "role_%{company_role}-%{company_platform}", "platform_%{company_platform}", "common"]

Comments or questions welcome.

07
Jan
11

Pulling a list of hosts from MCollective for Puppet

In one of my previous posts, I wrote about using foreman (a node classifier and dashboard for puppet) to retrieve a list of hosts (and meta information) so that you can use it in a puppet manifest. We’re looking to do something like iterate over a set of nodes in a template (I use it for generating the Munin server config). Stored resources provide a way to centralize information from nodes, but it isn’t very intuitive, and gets a little tricky to plan and maintain.

While foreman is good for a lot of use-cases, not everyone uses it. So, I want to provide an alternative for those that don’t use foreman. The alternative uses MCollective to populate a list of hosts based on information given to us by the MC Registration plugin. Before I dive in, I’d like to quickly cover off a blog from the MCollective architect, R.I.Pienaar, on this very topic. His blog post (PuppetSearch) brings together 3 things. MongoDB, MC Registration, and Puppet in a very powerful way. It is a really good solution, is more robust, and we’ve since moved on to something similar. The following blog is more just to help understand how flexible puppet can be, and how well it integrates with MCollective. The advantage of using PuppetSearch is that you can load a specific node, and you can query using MongoDB syntax.

So, we’re looking to achieve something quite simple really. We want a subset of hosts matching a basic query, with their meta information, in a variable in a puppet manifest.

MCollective and Registration
MCollective is great, and very flexible. One of the core plugins for MCollective is called registration. Essentially, every node/host sends a registration message at a pre-determined interval set in the configuration. The registration message is sent as a broadcast, and so any client can pick up the registration messages of any other client. We only want one registration handler, but its nice to know that there can be more than one handler (1 per node).

The registration message can be anything, and in our case, we want to send the client identifier, it’s facts, and maybe you want to add some more information along the way. This message is picked up by a handler, which then processes the message.

Currently, we’ve taken the lead of R.I.Pienaar (from his blog above) and shoved the messages straight into a MongoDB instance so that we can query it and use it for different parts of our operations infrastructure. Since that’s done, I’m going to cover off the plain old text file version. It’s extremely similar in architecture, but the code does completely different things.

Installing the registration plugin isn’t too difficult. We need to do it in 2 parts which is pretty standard and has been well documented.

Part 1 is getting the clients to register with all the information they have, including facts, and anything else you like. For this, one of the documented registrations will do just fine. This file will sit in your MCollective plugins/mcollective/registration directory. You then need to adjust your config file for the server to say what registration plugin you’re using and how often.

server.cfg

registerinterval = 300
registration = Meta

After this, you should have registration messages flying around. You need to handle them. The RegistrationAgent provides a simple handler which will write the messages to a text file per client. check_mcollective is optional, and we won’t be using it. It provides a link to nagios if you wish to explore. So, for the client we want to handle the registration messages (probably your mcollective/puppet server), we want to put registration.rb in MCollective plugins/mcollective/agent.

Restart MCollective on all nodes that have been affected, and you should start seeing the registrations text files populating in /var/tmp/mcollective/. You can change this directory by specifying it in the client.cfg on the node where the handler is. (plugin.registration.directory = '/etc/mcollective/registered/')

Ok, so that’s MCollective registration done. If you want to add more information, just make some changes to meta.rb.

Hostlist (Puppet function)

Ok, here we come onto the real topic which is to use what we’ve implemented above to retrieve a list of hosts with their facts. The function itself is very easy because all the information we have is already in YAML and we can just load it, and spit it out!

So the function below is a puppet parser function which can be used in manifests. Please excuse my Ruby…


# mc_hostlist.rb
# Duncan Phillips

# Retrieve a list of hosts and their meta information by querying the data stored by the registration agent.
# info on the registration agent can be found at http://marionette-collective.org/reference/plugins/registration.html

# Usage: mc_hostlist([class],[fact])
# If neither is specified, all hosts are returned.
# Class and Fact are filters and can both be specified
# Fact can be specific or non-specific. i.e. machine with fact, or machine with fact=z

# e.g. mc_hostlist(class=hosting, fact=operatingsystem=Ubuntu)
#[hostname => {facts : {fact1 : value1}, classes : {class1 : value 1}}]

require 'yaml'

module Puppet::Parser::Functions
	newfunction(:mc_hostlist, :type => :rvalue) do |args|
		#populate our array/map
		hosts = Dir.entries("/var/tmp/mcollective")
		for h in hosts do
			begin
				if (h == '.') or (h == '..')
					hosts[hosts.index(h)] = nil; 
				else
					hfile = open("/var/tmp/mcollective/"+h)
					raw = hfile.read.gsub("!ruby/sym ","")
					hosts[hosts.index(h)]=YAML.load(raw).merge({"fqdn"=>h})
				end
			rescue Exception => e
				raise Puppet::ParseError, "There was an exception: " + e + "\n"
			end
		end

		args.each do |arg|

			name, value, factvalue = arg.split("=")

			case name
			when "fact"
				hosts=hosts.compact
				for h in hosts do
					if hosts[hosts.index(h)]["facts"][value]
						if (factvalue) and (hosts[hosts.index(h)]["facts"][value] != factvalue)
							hosts[hosts.index(h)]=nil
						end
					else
						hosts[hosts.index(h)]=nil
					end
				end
			when "class"
				hosts=hosts.compact
				for h in hosts do
					if hosts[hosts.index(h)]["classes"].index(value) == nil
						hosts[hosts.index(h)]=nil
					end
				end
			 
			else
				raise Puppet::ParseError, "mc_hostlist: Invalid parameter #{name}"
			end #case
		end #args
	
		return hosts.compact

	end #func
end

This is a puppet parser, and so it needs to be installed into a module as such. You can find out more about this here. I recommend just putting it into the common module, in which case it will go into MCollective modules/common/lib/puppet/parser/functions/. After this you’ll need to resync the plugins (usually a puppet run will suffice if pluginsync is turned on in the configs).

Into the manifest

So, how do we use this? I’m going to give some insight into how one can use this to generate a Munin conf file… I won’t go into other bits, but will look at what’s relevant here.

Below is an example of how one might use a list of all hosts which have the class ‘Web‘ to create an aggregate graph. We can aggregate anything, for now we’ll create a graph of the load for every node in the list.


	$hl_web  = mc_hostlist("class=Web")

	file {	"/etc/munin/munin.conf": content => template("munin/munin.conf"), }

We can then use this in the template file as below (Once again, excuse my Ruby):


<% hl_web.each do |h| -%>

# Register the nodes

[GroupName;<%= h['fqdn'] %>]
        address <%= h['fqdn'] %>
        use_node_name yes
<% end %>

# Create a new group Totals which holds aggregate graphs

[Totals; GroupName]

        # Generate our aggregate graph

        web_load.graph_title Load Average
        web_load.graph_category GroupName
        web_load.graph_scale no
        web_load.graph_vlabel Load
        web_load.graph_order \<% hl_web.each_with_index do |h,i| -%><% if (h != '') %>
                <%= h['fqdn'][/[a-zA-Z0-9]*/] %>=GroupName;<%= h['fqdn'] %>:load.load <% if i != hl_web.size-1 -%>\<% end -%><% end -%><% end %>
        <% hl_web.each_with_index do |h,i| %>
        <% if (h != '') -%>web_load.<%= h['fqdn'][/[a-zA-Z0-9]*/] %>.draw LINE1<% end %>
        <% end %>

End result: A nice aggregate graph that will dynamically add hosts as they register.

25
Oct
10

Pulling a list of hosts from foreman for Puppet

I’m on a mission to completely automate the configuration of munin by taking the excellent work of duritong on puppet-munin, and embellishing it a little in order to create an Overview section which can aggregate a cross-section of hosts. One of the main enablers for this will be a little-known wrapper function that comes with puppet-foreman.

The function provided is a ‘Query Interface’ to foreman, and queries foreman via an API, which returns a list of hosts. This wrapper function actually makes a call via a ruby script provided with the foreman installation. As the docs say, you’ll have to modify line 8 of this script to point to your foreman installation.

If you would like to test that the function is pointing correctly and working, create a hostgroup, attach to a host, and use the following curl:

curl -v http://foreman?hostgroup=somehostgroup&format=yml&state=all

If all is good you’ll see something like

---
  - hostname.domain

The next thing we want to do is use the puppet-foreman provided (example) function, foreman, to make a call for us and return a list of hosts. To do this, we just need to import the foreman module into our namespace and make the call. The call comes standard as taking a number of optional parameters as follows:

hostgroup=groupname : list hosts that are in the group

fact=factname : list hosts with a specific fact.

class=classname : list hosts with a class attached

verbose=yes : output the hosts facts as well

A simple class that implemented this might look like:


class test {
    $hostlist = split(gsub(foreman("hostgroup=cluster1"),"[ -]*",""),'\n')
    file {
        ... some template that uses hostlist
    }
}

where some template could be as simple as listing the fqdn of the hosts that match your query. Later on we will have a look at how to use the facts supplied from clients in a central manifest.


<% hostlist.each do |host| %>
    <%= host %>
<% end >%

The code for the manifest is a bit obscure and I came across it in some old mail archive’s. Essentially, the output from the wrapper function is a string and so it needs to get passed through the split and gsub functions provided in the common module. The split function returns an array which we can then use to pass into our template. This is something we’ll look into just now.

Something that caught me out initially, is that if you are trying to list hosts by classname, the classes have to be directly attached, as opposed to implicitly via a hostgroup. e.g. If you have a node belonging to a hostgroup which has class x. A search for hosts with class x will not return the node.

The example wrapper that has been given in the docs is a little basic and has some elements missing which are available in the actual Query Interface. For instance, when specifying a fact, you can only specify whether a host contains has that fact, and not whether it has a fact with a specific value.

A better puppet parser function
Looking at some missing elements from the query interface, there a couple improvements we would like to get from the wrapper function:

  1. Be able to return a list of hosts with fact=value.
  2. Return an array / hash instead of a string and use those facts given in a verbose response
  3. Option to include all hosts irrespective of their state

Here is my code for the updated wrapper function based on the original with some modifications.
/etc/puppet/modules/foreman/lib/puppet/parser/functions/foreman.rb


require 'net/http'

# Query Foreman
module Puppet::Parser::Functions
 newfunction(:foreman, :type => :rvalue) do |args|
	#URL to query
	host = "foreman"
  url = "/hosts/query?"
  query = []
  args.each do |arg|
    name, value, fact_val = arg.split("=")
    case name
    when "fact"
      query << "#{name}=#{value}-seperator-#{fact_val}"
    when "class", "hostgroup"
      query << "#{name}=#{value}"
    when "verbose"
      query << "verbose=yes" if value == "yes"
    when "state"
      query << "state=all" if value == "all"  
    else
      raise Puppet::ParseError, "Foreman: Invalid parameter #{name}"     
    end   
end   

begin
     response = Net::HTTP.get host,url+query.join("&")+"&format=yml"
rescue Exception => e
    raise Puppet::ParseError, "Failed to contact Foreman #{e}"
end

  begin
    hostlist = YAML::load response
  rescue Exception => e
    raise Puppet::ParseError, "Failed to parse response from Foreman #{e}"
  end
  return hostlist
 end
end

With this modified wrapper function in place (after running a pluginsync) we can simplify our class to:


class test {

    $hostlist_cluster1 = foreman("hostgroup=cluster1", "state=all")
    $hostlist_os = foreman("fact=operatingsystem=Ubuntu", "state=all")

    file {
        ... some template that uses hostlist
    }
}

where some template could be


<% hostlist.each do |host| %>
    <%= host %>
<% end >%

Now, if you’re using puppet 2.6, you can also use the verbose option to return hosts and their facts which just adds another level of awesomeness. The verbose output adds in the meta information to the YAML, and if you do a curl as follows:

curl -v http://foreman?hostgroup=somehostgroup&format=yml&state=all&verbose=yes

#You can also try parse through ruby to get a cleaner output:
curl -v 'http://foreman?hostgroup=somehostgroup&format=yml&state=all&verbose=yes' > /tmp/f.list
cat /tmp/f.list | ruby -e 'require "yaml"; y=STDIN.read; y=y.gsub("!ruby/sym ",""); print YAML.dump(YAML.load(y))'

You should see a yours hosts coming back with this kind of structure

---
   - host.domain
        - facts
             - somefact=x
             - somefact=x
        - classes
             - someclass

So, to use this information from the updated foreman function, we use verbose as:


    $hostlist = foreman("fact=operatingsystem=Ubuntu", "state=all", "verbose=yes")

An in our template, we can reference the facts:


<% hostlist.each do |host| %>
    <%= host['facts']['fqdn'] %> runs <%= host['facts']['operatingsystem'] %>
<% end >%

This final piece of the puzzle, bringing facts and meta-data into the picture, will enable fairly complex setups for distributed systems with a centralized configuration. There are other options and I’d encourage anyone reading this to get familiar with ExtLookup from RI Pienaar, as well as the exported resources which are available as part of the core.




Follow

Get every new post delivered to your Inbox.