3

I'm trying to write a puppet function that calls my hosting environment (rackspace cloud atm) to list servers, then update my hosts file.

My get_hosts function is currently this:

require 'rubygems'
require 'cloudservers'

module Puppet::Parser::Functions
  newfunction(:get_hosts, :type => :rvalue) do |args|
    unless args.length == 1
      raise Puppet::ParseError, "Must provide the datacenter"
    end

    DC       = args[0] 
    USERNAME = DC == "us" ? "..." : "..."
    API_KEY  = DC == "us" ? "..." : "..."
    AUTH_URL = DC == "us" ? CloudServers::AUTH_USA : CloudServers::AUTH_UK
    DOMAIN   = "..."

    cs = CloudServers::Connection.new(:username => USERNAME, :api_key => API_KEY, :auth_url => AUTH_URL)

    cs.list_servers_detail.map {|server|
      server.map {|s| { s[:name] + "." + DC + DOMAIN => {
                          :ip      => s[:addresses][:private][0],
                          :aliases => s[:name]
      }}}
    }
  end
end

And I have a hosts.pp that calls this and 'should' write it to /etc/hosts.

class hosts::us {
    $hosts = get_hosts("us")

    hostentry { $hosts: }
}

define hostentry() {
  host{ $name: ip => $name[ip], host_aliases => $name[aliases] }
}

As you can imagine, this isn't currently working and I'm getting a 'Symbol as array index at /etc/puppet/manifests/hosts.pp:2' error. I imagine, once I've realised what I'm currently doing wrong there will be more errors to come.

Is this a good idea? Can someone help me work out how to do this?

Update

Finally managed to get this working (with help from the comments) ! Here is my get_hosts.rb:

require 'rubygems'
require 'cloudservers'

module Puppet::Parser::Functions
  newfunction(:get_hosts, :type => :rvalue) do |args|
    unless args.length == 1
      raise Puppet::ParseError, "Must provide the datacenter"
    end

    dc       = args[0] 
    username = dc == "us" ? "..." : "..."
    api_key  = dc == "us" ? "..." : "..."
    auth_url = dc == "us" ? CloudServers::AUTH_USA : CloudServers::AUTH_UK
    domain   = "...."

    cs = CloudServers::Connection.new(:username => username, :api_key => api_key, :auth_url => auth_url)

    cs.list_servers_detail.map {|server|
      server[:name] + "." + dc + domain + "," + 
          server[:addresses][:private][0] + "," + 
          server[:name]
    }
  end
end

and the hosts.pp

class hosts::us {
    $hosts = get_hosts("us")

    hostentry { $hosts: }
}

define hostentry() {
  $parts   = split($name, ',')
  $address = $parts[0]
  $ip      = $parts[1]
  $aliases = $parts[2]
  host{ $address: ip => $ip, host_aliases => $aliases }
}

It's quite nasty marshalling the namevar like that, but it was the only way I could seem to get it to work. Any improvements welcome.

Ben Smith
  • 157
  • 5

2 Answers2

1

I wouldn't call it a bad idea, but you really need to stop using constants everywhere in your code (anything that starts with an uppercase letter in Ruby is a constant).

To debug your problem, you probably want to use the --trace option to your puppetmaster invocation, so it'll print a backtrace instead of eating the real exception and giving you a useless error message. You've got a whole pile of dereferencing going on in your map calls; my guess would be that you've misunderstood part of the data structure coming out of the API and your code is tripping over that misunderstanding. Fire up a debugger (or liberally sprinkle puts through your code) and you'll see what you've got wrong in pretty short order.

womble
  • 95,029
  • 29
  • 173
  • 228
  • Yes, please excuse the Ruby, it was mostly copied from the cloudservers gem example. The problem I'm really suffering is marshalling the input to the define correctly. – Ben Smith Apr 04 '12 at 22:11
  • Well, given that the only way to do iteration in Puppet is to pass in an array of namevars to a define, I just write a function to represent all the fields I want for each resource as a single string and return an array of those, then have the defined type parse that back out again into fields that it uses to instantiate whatever primitive resource(s) I require. Ugly, but the least-worst option. – womble Apr 04 '12 at 22:17
  • Have you got an example of that. I think I know what you mean though, some kind of name=dev.blah.com&ip=127.0.0.1&aliases=dev string? – Ben Smith Apr 04 '12 at 22:26
  • That's the one. I don't have an example available just now, but it sounds like you've got the right idea. – womble Apr 04 '12 at 23:17
0

I'd separate this functionality differently.

Use your function to build up the pure data in an hash of hashes representing the list of host resources.

Then use the create_resources function to create actual host resources out of that data:

https://puppet.com/docs/puppet/latest/function.html#createresources

The example there should give you enough to get going:

    # A hash of user resources:
    $myusers = {
      'nick' => { uid    => '1330',
                  group  => allstaff,
                  groups => ['developers', 'operations', 'release'], }
      'dan'  => { uid    => '1308',
                  group  => allstaff,
                  groups => ['developers', 'prosvc', 'release'], }
    }

    create_resources(user, $myusers)

Does that make sense?

Then you can avoid the defined type, and you can do all the data munging in your function call to get the hash of hashes, and you've got good separation between the function that collects data, and the function that instantiates resources.

This should solve any need for classical iteration if I'm understanding your problem correctly.

рüффп
  • 620
  • 1
  • 11
  • 24