Coffee & Beer

Rantings and Ravings of the technical sort

Getting Racktables Location Info Into Puppet

At work we have had Racktables ((http://racktables.org/)) for a while for tracking where things are. Its…..okay. Its not the best,but, eh, it works. We need to do a better job with clena data etc, but, it works.

One thing we don’t like, however, is its current lack of an API. We can query the db directly, but thats kinda clunky. So, the other ngiht I had an idea. A few hours later, it was basically done. A YAML api (well, a cheap mans api) for racktables!

So, let me set this up. I want a yaml document for each host, with location info. This way, facter, or, anything else, can pull down that location info. When someone changes something in racktables, the ymal document should be updated. I don’t need real time, but liets say 30 minutes. Sounds like a cron job…

So, a script, running from cron, reading the racktables database and spitting out YAML documents of the data on a per host basis. Okay. What data? Well, I want Row (for us this is datacenter+row), Rack, RU, Height of the system (how many RU’s does it take up?). Since racktables does have some asset tags, might as well pull that so we can compaire to puppet/foreman while we’re at it.

So, a yaml document like:

(rackfact_example.yaml) download
1
2
3
4
5
6
---
ru: "16"
row: "DataCenterB Row 6"
rack: "1"
height: "1"
asset: "326859"

And I want things at a url like

1
http://server/rackfacts/systems/HOSTNAME

Also, at the request of a co-worker, just the endpoint /systems will return ALL of the systems.

So, after a BUNCH of digging into the racktables DB and dusting off my SQL, I came up with:

(rack2yaml.rb) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#!/usr/bin/env ruby
require 'yaml'
require 'mysql'
path="/var/www/rackfacts/"

my = Mysql::new("racktables","rackuser","rackpass","racktables")

rackobjs=my.query("select distinct RackObject.name,RackSpace.unit_no,Rack.name,RackRow.name,RackObject.asset_no,RackObject.id from RackObject,RackSpace,Rack,RackRow where RackObject.id = RackSpace.object_id AND Rack.id = RackSpace.rack_id AND RackRow.id = Rack.row_id AND RackObject.objtype_id = 4;")

objects=Array.new
rackobjs.each do |row|
        obj=Hash.new
        obj["name"] = row[0].to_s.downcase.strip.delete(' ').delete('#').delete('/').delete('"')
        obj["ru"] = row[1].to_s.strip.delete('"')
        obj["rack"] = row[2].to_s.strip
        obj["row"] = row[3].to_s.strip
  obj["asset"] = row[4].to_s.strip
  obj["id"] = row[5]
        objects.push(obj)
end

#Need to get the height of a given system...
objects.each do |obj|
  height=my.query("SELECT COUNT(distinct unit_no) FROM `RackSpace` WHERE object_id #{obj['id']};")
  obj["height"] = height
end
#Writing Systems, so lets do this in /systems/

path = path + "systems/"

#Lets clean the existing ones, so stale thigns are removed.
clean = "rm -rf #{path} && mkdir #{path}"
%x[ #{clean} ]
objects.each do |thing|
        fpath = path+thing["name"]
        yobj=Hash.new
        yobj["ru"]=thing["ru"]
        yobj["rack"]=thing["rack"]
        yobj["row"]=thing["row"]
  yobj["asset"]=thing["asset"]
  yobj["height"]=thing["height"]
        f=File.open(fpath,'w')
        f.write(yobj.to_yaml)
end

allpath=path + "index.html"
all=File.open(allpath,'w')
all.write(objects.to_yaml)

So, this ruby script:

  • Sets up some stuff
  • connects to mysql
  • runs a query to get most (not height) of the system info.
  • Height is a second query, as racktables doesn’t know about height, but rather has a single object us multiple RU’s in a rack…
  • So, we query for that for each system, counting the times a given object is in a Rack, distinct on unit_no’s as racktables also has a front,back,middle format (so a 4U system that goes front to back might have 12 entries!)
  • We then merge all this data together in an array of hashes
  • Clean out our path
  • dump all the yaml documents
  • dumps out the whole array for the ALL systems bit

Ta Da!

Okay, so now we have those yaml documents, and every 30 minute sthis will get done so anythign we cleanup/remove will be available as well. Now what? Lets pull that in as some facts!

(rackfacts.rb) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#!/usr/bin/env ruby

require 'facter'
require 'net/http'
require 'yaml'
require 'timeout'
begin
  Facter.hostname
rescue
  Facter.loadfacts()
end

if Facter.value('is_virtual') == "true"
  virtual = "virtual"
  Facter.add("location_ru") do
      setcode do
          virtual
      end
  end
  Facter.add("location_rack") do
      setcode do
          virtual
      end
  end
  Facter.add("location_row") do
      setcode do
          virtual
      end
  end
  Facter.add("location_height") do
      setcode do
          virtual
      end
  end
else   
  hostname = Facter.value('hostname')
  rackfact_host = "racktables"
  rackfact_dir  = "/rackfacts/systems/#{hostname}"
  unknown = "unknown"
  begin
      Timeout::timeout(2) {
          rescode=Net::HTTP.get_response rackfact_host,rackfact_dir
          if (rescode.code =~ /2|3\d{2}/ )
              rackfact = YAML::load(rescode.body)
              ru = rackfact["ru"]
              rack = rackfact["rack"]
              row = rackfact["row"]
              height = rackfact["height"]
              Facter.add("location_ru") do
                  setcode do
                      ru
                  end
              end
              Facter.add("location_rack") do
                  setcode do
                      rack
                  end
              end
              Facter.add("location_row") do
                  setcode do
                      row
                  end
              end
              if height != nil
                  Facter.add("location_height") do
                      setcode do
                          height
                      end
                  end
              end
          else
              Facter.add("location_ru") do
                  setcode do
                      unknown
                  end
              end
              Facter.add("location_rack") do
                  setcode do
                      unknown
                  end
              end
              Facter.add("location_row") do
                  setcode do
                      unknown
                  end
              end
              Facter.add("location_height") do
                  setcode do
                      unknown
                  end
              end
          end
      }
  rescue Timeout::Error
      Facter.add("location_ru") do
          setcode do
              unknown
          end
      end
      Facter.add("location_rack") do
          setcode do
              unknown
          end
      end
      Facter.add("location_row") do
          setcode do
              unknown
          end
      end
      Facter.add("location_height") do
          setcode do
              unknown
          end
      end
  end
end

Now, we can query the nice puppet/forman api’s for location data! Better yet, I can use these with storedconfigs to do things like add location info to Ganglia! Or have systems in 1 data center get specific configs (dns? puppet master? AD?)

Our config management system is now location aware!

Comments