It matches a given node's names against lists of regexps found on the filesystem and sets classes and parameters where it finds matches. Also added some examples of both under their respective directories and set up the script so it will use them as input out of the box.
Signed-off-by: Eric Sorenson <[email protected]> --- ext/regexp_nodes.rb | 215 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 215 insertions(+), 0 deletions(-) create mode 100644 ext/regexp_nodes.rb diff --git a/ext/regexp_nodes.rb b/ext/regexp_nodes.rb new file mode 100644 index 0000000..e56a956 --- /dev/null +++ b/ext/regexp_nodes.rb @@ -0,0 +1,215 @@ +#!/usr/bin/env ruby + +# = Synopsis +# This is an external node classifier script, after +# http://reductivelabs.com/trac/puppet/wiki/ExternalNodes +# +# = Usage +# regexp_nodes.rb <host> +# +# = Description +# This classifier implements filesystem autoloading: +# We look through classes/ and parameters/ subdirectories, looping +# through each file we find there - the contents are a regexp-per-line +# which, if they match the hostname passed us as ARGV[0], will cause +# a class or parameter named the same thing as the file to be set. +# +# = Examples +# In the distribution there are two subdirectories regexp_classes/ and +# regexp_parameters, which are passed as parameters to MyExternalNode.new. +# regexp_classes/database will set the 'database' class for any hostnames +# matching %r{db\d{2}} (that is, 'db' followed by two digits) or with 'mysql' +# anywhere in the hostname. Similarly, hosts beginning with 'www' or 'web' +# or the hostname 'leterel' (my workstation) will be assigned the 'webserver' +# class. +# +# Under regexp_parameters/ there is one subdirectory 'environment' which +# sets the a parameter called 'environment' to the value 'prod' for production +# hosts (whose hostnames always end with three numbers for us), 'qa' for +# anything that starts with 'qa-' 'qa2-' or 'qa3-', and 'sandbox' for any +# development machines which are naturally named after Autechre songs. +# +# +# = Author +# Eric Sorenson <[email protected]> + + +# we need yaml or there's not much point in going on +require 'yaml' + +# Sets are like arrays but automatically de-duplicate elements +require 'set' + +# set up some nice logging +require 'logger' +# XXX flip this for production vs local sandbox +# $LOG = Logger.new("/var/puppet/log/extnodes.log") +# $LOG.level = Logger::FATAL +$LOG = Logger.new($stderr) +$LOG.level = Logger::DEBUG + +# paths for files we use will be relative to this directory +# XXX flip this for production vs local sandbox +# WORKINGDIR = "/var/puppet/bin" +WORKINGDIR = Dir.pwd + +# This class holds all the methods for creating and accessing the properties +# of an external node. There are really only two public methods: initialize() +# and a special version of to_yaml() + +class ExternalNode + # Make these instance variables get/set-able with eponymous methods + attr_accessor :classes, :parameters, :hostname + + # initialize() takes three arguments: + # hostname:: usually passed in via ARGV[0] but it could be anything + # classdir:: directory under WORKINGDIR to look for files named after + # classes + # parameterdir:: directory under WORKINGDIR to look for directories to set + # parameters + def initialize(hostname, classdir = 'classes/', parameterdir = 'parameters/') + # instance variables that contain the lists of classes and parameters + @hostname + @classes = Set.new ["baseclass"] + @parameters = Hash.new("unknown") # sets a default value of "unknown" + + self.parse_argv(hostname) + self.match_classes(WORKINGDIR + "/" + classdir) + self.match_parameters(WORKINGDIR + "/" + parameterdir) + end + + # private method called by initialize() which sanity-checks our hostname. + # good candidate for overriding in a subclass if you need different checks + def parse_argv(hostname) + if hostname =~ /^([-\w]+?)\.([-\w\.]+)/ # non-greedy up to the first . is hostname + @hostname = $1 + elsif hostname =~ /^([-\w]+)$/ # sometimes puppet's @name is just a name + @hostname = hostname + else + $LOG.fatal("didn't receive parsable hostname, got: [#{hostname}]") + exit(1) + end + end + + # to_yaml massages a copy of the object and outputs clean yaml so we don't + # feed weird things back to puppet []< + def to_yaml + classes = self.classes.to_a + if self.parameters.empty? # otherwise to_yaml prints "parameters: {}" + parameters = nil + else + parameters = self.parameters + end + ({ 'classes' => classes, 'parameters' => parameters}).to_yaml + end + + # Private method that expects an absolute path to a file and a string to + # match - it returns true if the string was matched by any of the lines in + # the file + def matched_in_patternfile?(filepath, matchthis) + + patternlist = [] + + begin + open(filepath).each { |l| + pattern = %r{#{l.chomp!}} + patternlist << pattern + $LOG.debug("appending [#{pattern}] to patternlist for [#{filepath}]") + } + rescue Exception + $LOG.fatal("Problem reading #{filepath}: #{$!}") + exit(1) + end + + $LOG.debug("list of patterns for #{filepath}: #{patternlist}") + + if matchthis =~ Regexp.union(patternlist) + $LOG.debug("matched #{$~.to_s} in #{matchthis}, returning true") + return true + + else # hostname didn't match anything in patternlist + $LOG.debug("#{matchthis} unmatched, returning false") + return nil + end + + end # def + + # private method - takes a path to look for files, iterates through all + # readable, regular files it finds, and matches this instance's @hostname + # against each line; if any match, the class will be set for this node. + def match_classes(fullpath) + Dir.foreach(fullpath) do |patternfile| + filepath = "#{fullpath}/#{patternfile}" + next unless File.file?(filepath) and + File.readable?(filepath) + $LOG.debug("Attempting to match [...@hostname}] in [#{filepath}]") + if matched_in_patternfile?(filepath,@hostname) + @classes << patternfile.to_s + $LOG.debug("Appended #{patternfile.to_s} to classes instance variable") + end # if + end # Dir.foreach + end # def + + # Parameters are handled slightly differently; we make another level of + # directories to get the parameter name, then use the names of the files + # contained in there for the values of those parameters. + # + # ex: cat /var/puppet/bin/parameters/environment/production + # ^prodweb + # would set parameters["environment"] = "production" for prodweb001 + def match_parameters(fullpath) + Dir.foreach(fullpath) do |parametername| + + filepath = "#{fullpath}/#{parametername}" + next if File.basename(filepath) =~ /^\./ # skip over dotfiles + + next unless File.directory?(filepath) and + File.readable?(filepath) # skip over non-directories + + $LOG.debug "Considering contents of #{filepath}" + + Dir.foreach("#{filepath}") do |patternfile| + secondlevel = "#{filepath}/#{patternfile}" + $LOG.debug "Found parameters patternfile at #{secondlevel}" + next unless File.file?(secondlevel) and + File.readable?(secondlevel) + $LOG.debug("Attempting to match [...@hostname}] in [#{secondlevel}]") + if matched_in_patternfile?(secondlevel, @hostname) + @parameters[ parametername.to_s ] = patternfile.to_s + $LOG.debug("Set @parameters[#{parametername.to_s}] = #{patternfile.to_s}") + end # if + end # Dir.foreach #{filepath} + end # Dir.foreach #{fullpath} + end # def + +end # Class + +# Logic for local hacks that don't fit neatly into the autoloading model can +# happen as we initialize a subclass +class MyExternalNode < ExternalNode + + def initialize(hostname, classdir = 'classes/', parameterdir = 'parameters/') + + super + + # Set "hostclass" parameter based on hostname, + # stripped of leading environment prefix and numeric suffix + if @hostname =~ /^(\w*?)-?(\D+)(\d{2,3})$/ + match = Regexp.last_match + + hostclass = match[2] + $LOG.debug("matched hostclass #{hostclass}") + @parameters[ "hostclass" ] = hostclass + else + $LOG.debug("hostclass couldn't figure out class from #...@hostname}") + end # if + end # def + +end # Class + + +# Here we begin actual execution by calling methods defined above + +mynode = MyExternalNode.new(ARGV[0], classes = 'regexp_classes', parameters = 'regexp_parameters') + +puts mynode.to_yaml -- 1.6.4.3 --~--~---------~--~----~------------~-------~--~----~ You received this message because you are subscribed to the Google Groups "Puppet Developers" group. To post to this group, send email to [email protected] To unsubscribe from this group, send email to [email protected] For more options, visit this group at http://groups.google.com/group/puppet-dev?hl=en -~----------~----~----~----~------~----~------~--~---
