]> git.openstreetmap.org Git - chef.git/blob - cookbooks/hardware/templates/default/ohai.rb.erb
74c23f731653bc755cb271e129052d48c469dc4d
[chef.git] / cookbooks / hardware / templates / default / ohai.rb.erb
1 Ohai.plugin(:Hardware) do
2   provides "hardware"
3
4   def read_sysctl_link(file)
5     File.basename(File.readlink(file))
6   rescue Errno::ENOENT
7   end
8
9   def read_sysctl_file(file)
10     IO.read(file).strip
11   rescue Errno::ENOENT, Errno::EINVAL
12   end
13
14   def parse_memory_size(size)
15     if size =~ /^(\d+(?:\.\d+)?)\s*TB/i
16       Regexp.last_match(1).to_f * 2**30
17     elsif size =~ /^(\d+(?:\.\d+)?)\s*GB/i
18       Regexp.last_match(1).to_f * 2**20
19     elsif size =~ /^(\d+(?:\.\d+)?)\s*MB/i
20       Regexp.last_match(1).to_f * 2**10
21     end
22   end
23
24   def format_disk_size(kb)
25     if kb == 0
26       ""
27     else
28       kblog10 = Math.log10(kb).floor
29
30       kb = kb.to_f * 2 / 10**kblog10
31       kb = kb.round.to_f / 2
32
33       if kblog10 >= 9
34         format "%gTB", kb * 10**(kblog10 - 9)
35       elsif kblog10 >= 6
36         format "%dGB", kb * 10**(kblog10 - 6)
37       else
38         format "%dMB", kb * 10**(kblog10 - 3)
39       end
40     end
41   end
42
43   def memory_to_disk_size(size)
44     format_disk_size(parse_memory_size(size))
45   end
46
47   def find_sas_device(address)
48     file = Dir.glob("/sys/class/scsi_generic/sg*/device/sas_address").find do |file|
49       read_sysctl_file(file) == "0x#{address}"
50     end
51
52     if file
53       dir = File.dirname(file)
54       device = Dir.glob("#{dir}/block/*").first ||
55                Dir.glob("#{dir}/scsi_generic/*").first
56
57       "/dev/#{File.basename(device)}"
58     end
59   end
60
61   def pci_devices
62     device = nil
63
64     IO.popen(["lspci", "-Dkvmm"]).each_with_object(Mash.new) do |line, devices|
65       if line =~ /^Slot:\s+((\h{4}):(\h{2}):(\h{2}).(\h))\s*$/
66         device = {
67           :slot => Regexp.last_match(1),
68           :domain => Regexp.last_match(2),
69           :bus => Regexp.last_match(3),
70           :device => Regexp.last_match(4),
71           :function => Regexp.last_match(5)
72         }
73       elsif device && line =~ /^([A-Z]+):\s+(.*)\s*$/i
74         case Regexp.last_match(1)
75         when "Class" then device[:class_name] = Regexp.last_match(2)
76         when "Vendor" then device[:vendor_name] = Regexp.last_match(2)
77         when "Device" then device[:device_name] = Regexp.last_match(2)
78         when "SVendor" then device[:subsystem_vendor_name] = Regexp.last_match(2)
79         when "SDevice" then device[:subsystem_device_name] = Regexp.last_match(2)
80         when "PhySlot" then device[:physical_slot] = Regexp.last_match(2)
81         when "Rev" then device[:revision] = Regexp.last_match(2)
82         when "ProgIf" then device[:programming_interface] = Regexp.last_match(2)
83         when "Driver" then device[:driver] = Regexp.last_match(2)
84         when "Module" then device[:modules] = Array(device[:modules]) << Regexp.last_match(2)
85         end
86       elsif device && line =~ /^\s*$/
87         devices[device[:slot]] = device
88         device = nil
89       end
90     end
91   end
92
93   def network_devices
94     Dir.glob("/sys/class/net/*").each_with_object(Mash.new) do |device, devices|
95       name = File.basename(device)
96
97       devices[name] = {
98         :device => read_sysctl_link("#{device}/device"),
99         :duplex => read_sysctl_file("#{device}/duplex"),
100         :speed => read_sysctl_file("#{device}/speed")
101       }.delete_if { |_, v| v.nil? }
102     end
103   end
104
105   def memory_devices
106     device = nil
107
108     IO.popen(["dmidecode", "-t", "memory"]).each_with_object([]) do |line, devices|
109       if line =~ /^Memory Device\s*$/
110         device = {}
111       elsif device && line =~ /^\s+([A-Z ]+):\s+(.*)\s*$/i
112         device[Regexp.last_match(1).tr(" ", "_").downcase.to_sym] = Regexp.last_match(2).strip
113       elsif device && line =~ /^\s*$/
114         devices << device
115         device = nil
116       end
117     end
118   end
119
120   def disk_devices
121     disk = Mash.new
122
123     disk[:controllers] = []
124     disk[:arrays] = []
125     disk[:disks] = []
126
127     find_direct_disks(disk)
128
129     find_hp_disks(disk) if File.exist?("/usr/sbin/hpssacli")
130     find_megaraid_disks(disk) if File.exist?("/usr/sbin/megacli")
131     find_mpt_disks(disk) if File.exist?("/usr/sbin/sas2ircu")
132     # aacraid
133     find_areca_disks(disk) if File.exist?("/opt/areca/x86_64/cli64")
134
135     find_md_arrays(disk)
136
137     disk
138   end
139
140   def find_direct_disks(devices)
141     Dir.glob("/sys/class/scsi_host/host*") do |host|
142       driver = read_sysctl_file("#{host}/proc_name")
143
144       if driver == "ahci" || driver == "mptsas"
145         bus = host.sub("/sys/class/scsi_host/host", "")
146
147         Dir.glob("/sys/bus/scsi/devices/#{bus}:0:*").each do |device|
148           next unless File.exist?("#{device}/scsi_disk")
149
150           block = Dir.glob("#{device}/block/*").first
151           vendor = read_sysctl_file("#{device}/vendor")
152           model = read_sysctl_file("#{device}/model")
153           size = read_sysctl_file("#{block}/size").to_i * 512
154
155           if vendor == "ATA" && model =~ /^(\S+)\s+(.*)$/
156             vendor = Regexp.last_match(1)
157             model = Regexp.last_match(2)
158           end
159
160           devices[:disks] << {
161             :id => devices[:disks].count,
162             :device => "/dev/#{File.basename(block)}",
163             :vendor => vendor,
164             :model => model,
165             :firmware_version => read_sysctl_file("#{device}/rev"),
166             :size => format_disk_size(size),
167             :arrays => []
168           }
169         end
170       end
171     end
172   end
173
174   def find_md_arrays(devices)
175     array = nil
176
177     File.new("/proc/mdstat", "r").each do |line|
178       if line =~ /^(md\d+) : active raid(\d+)((?: [a-z]+\d+\[\d+\](?:\([A-Z]\))*)+)$/
179         array = {
180           :id => devices[:arrays].count,
181           :device => "/dev/#{Regexp.last_match(1)}",
182           :raid_level => Regexp.last_match(2),
183           :disks => []
184         }
185
186         Regexp.last_match(3).scan(/ ([a-z]+)\d+\[\d+\](?:\([A-Z]\))*/).flatten.each do |device|
187           if disk = devices[:disks].find { |d| d[:device] == "/dev/#{device}" }
188             disk[:arrays] << array[:id]
189             array[:disks] << disk[:id]
190           end
191         end
192
193         devices[:arrays] << array
194       elsif line =~ /^\s+(\d+) blocks/
195         array[:size] = format_disk_size(Regexp.last_match(1).to_i)
196       end
197     end
198   end
199
200   def find_hp_disks(devices)
201     controllers = []
202     disks = []
203
204     controller = nil
205     array = nil
206     disk = nil
207
208     IO.popen(%w(hpssacli controller all show config detail)).each do |line|
209       if line =~ /^Smart Array (\S+) /
210         controller = {
211           :id => devices[:controllers].count,
212           :model => Regexp.last_match(1),
213           :arrays => [],
214           :disks => []
215         }
216
217         devices[:controllers] << controller
218
219         controllers << controller
220
221         array = nil
222         disk = nil
223       elsif controller && line =~ /^   (\S.*):\s+(.*)$/
224         case Regexp.last_match(1)
225         when "Serial Number" then controller[:serial_number] = Regexp.last_match(2)
226         when "Hardware Revision" then controller[:hardware_version] = Regexp.last_match(2)
227         when "Firmware Version" then controller[:firmware_version] = Regexp.last_match(2)
228         when "PCI Address (Domain:Bus:Device.Function)" then controller[:pci_slot] = Regexp.last_match(2)
229         end
230       elsif controller && line =~ /^      Logical Drive: (\d+)$/
231         array = {
232           :id => devices[:arrays].count,
233           :controller => controller[:id],
234           :number => Regexp.last_match(1),
235           :disks => []
236         }
237
238         devices[:arrays] << array
239         controller[:arrays] << array[:id]
240
241         disk = nil
242       elsif controller && line =~ /^      physicaldrive (\S+) /
243         disks << Regexp.last_match(1)
244       elsif array && line =~ /^      physicaldrive (\S+)$/
245         disk = {
246           :id => devices[:disks].count,
247           :controller => controller[:id],
248           :arrays => [array[:id]],
249           :location => Regexp.last_match(1),
250           :smart_device => "cciss,#{disks.find_index(Regexp.last_match(1))}"
251         }
252
253         devices[:disks] << disk
254         controller[:disks] << disk[:id]
255         array[:disks] << disk[:id]
256       elsif disk && line =~ /^         (\S[^:]+):\s+(.*)$/
257         case Regexp.last_match(1)
258         when "Interface Type" then disk[:interface] = Regexp.last_match(2)
259         when "Size" then disk[:size] = Regexp.last_match(2)
260         when "Rotational Speed" then disk[:rpm] = Regexp.last_match(2)
261         when "Firmware Revision" then disk[:firmware_version] = Regexp.last_match(2)
262         when "Serial Number" then disk[:serial_number] = Regexp.last_match(2)
263         when "Model" then disk[:vendor], disk[:model] = Regexp.last_match(2).squeeze(" ").strip.sub(/^ATA /, "").split
264         end
265       elsif array && line =~ /^         (\S[^:]+):\s+(.*)$/
266         case Regexp.last_match(1)
267         when "Size" then array[:size] = Regexp.last_match(2)
268         when "Fault Tolerance" then array[:raid_level] = Regexp.last_match(2)
269         when "Disk Name" then array[:device] = Regexp.last_match(2).strip
270         when "Mount Points" then array[:mount_point] = Regexp.last_match(2).split.first
271         when "Unique Identifier" then array[:wwn] = Regexp.last_match(2)
272         end
273       end
274     end
275
276     controllers.each do |controller|
277       if device = Dir.glob("/sys/bus/pci/devices/#{controller[:pci_slot]}/cciss*").first
278         controller[:device] = File.basename(device).sub(/^cciss(\d+)$/, "/dev/cciss/c\\1d0")
279       elsif device = Dir.glob("/sys/bus/pci/devices/#{controller[:pci_slot]}/host*/target0:0:0/0:0:0:0/scsi_generic/sg*").first
280         controller[:device] = "/dev/#{File.basename(device)}"
281       end
282     end
283   end
284
285   def find_megaraid_disks(devices)
286     controllers = []
287     arrays = []
288
289     controller = nil
290     array = nil
291     disk = nil
292
293     IO.popen(%w(megacli -AdpGetPciInfo -aAll -NoLog)).each do |line|
294       if line =~ /^PCI information for Controller (\d+)$/
295         controller = {
296           :id => devices[:controllers].count,
297           :arrays => [],
298           :disks => []
299         }
300
301         devices[:controllers] << controller
302
303         controllers << controller
304       elsif line =~ /^Bus Number\s+:\s+(\d+)$/
305         controller[:pci_slot] = format "0000:%02x", Integer("0x#{Regexp.last_match(1)}")
306       elsif line =~ /^Device Number\s+:\s+(\d+)$/
307         controller[:pci_slot] = format "%s:%02x", controller[:pci_slot], Integer("0x#{Regexp.last_match(1)}")
308       elsif line =~ /^Function Number\s+:\s+(\d+)$/
309         controller[:pci_slot] = format "%s.%01x", controller[:pci_slot], Integer("0x#{Regexp.last_match(1)}")
310       end
311     end
312
313     IO.popen(%w(megacli -AdpAllInfo -aAll -NoLog)).each do |line|
314       if line =~ /^Adapter #(\d+)$/
315         controller = controllers[Regexp.last_match(1).to_i]
316       elsif line =~ /^(\S.*\S)\s*:\s+(\S.*)$/
317         case Regexp.last_match(1)
318         when "Product Name" then controller[:model] = Regexp.last_match(2)
319         when "Serial No" then controller[:serial_number] = Regexp.last_match(2)
320         when "FW Package Build" then controller[:firmware_version] = Regexp.last_match(2)
321         end
322       end
323     end
324
325     IO.popen(%w(megacli -LdPdInfo -aAll -NoLog)).each do |line|
326       if line =~ /^Adapter #(\d+)$/
327         controller = controllers[Regexp.last_match(1).to_i]
328       elsif controller && line =~ /^Virtual Drive: (\d+) \(Target Id: (\d+)\)$/
329         pci_slot = controller[:pci_slot]
330         target = Regexp.last_match(2)
331         device = Dir.glob("/sys/bus/pci/devices/#{pci_slot}/host*/target*:2:#{target}/*:2:#{target}:0/block/*").first
332
333         array = {
334           :id => devices[:arrays].count,
335           :controller => controller[:id],
336           :number => Regexp.last_match(1),
337           :device => "/dev/#{File.basename(device)}",
338           :disks => []
339         }
340
341         devices[:arrays] << array
342         controller[:arrays] << array[:id]
343
344         arrays << array
345
346         disk = nil
347       elsif array && line =~ /^PD: (\d+) Information$/
348         disk = {
349           :id => devices[:disks].count,
350           :controller => controller[:id],
351           :arrays => [array[:id]]
352         }
353
354         devices[:disks] << disk
355         controller[:disks] << disk[:id]
356         array[:disks] << disk[:id]
357       elsif disk && line =~ /^(\S.*\S)\s*:\s+(\S.*)$/
358         case Regexp.last_match(1)
359         when "Device Id" then disk[:smart_devlce] = "megaraid,#{Regexp.last_match(2)}"
360         when "WWN" then disk[:wwn] = Regexp.last_match(2)
361         when "PD Type" then disk[:interface] = Regexp.last_match(2)
362         when "Raw Size" then disk[:size] = memory_to_disk_size(Regexp.last_match(2).sub(/\s*\[.*\]$/, ""))
363         when "Inquiry Data" then disk[:vendor], disk[:model], disk[:serial] = Regexp.last_match(2).split
364         end
365       elsif array && line =~ /^(\S.*\S)\s*:\s+(\S.*)$/
366         case Regexp.last_match(1)
367         when "RAID Level" then array[:raid_level] = Regexp.last_match(2).scan(/Primary-(\d+)/).first.first
368         when "Size" then array[:size] = Regexp.last_match(2)
369         end
370       end
371     end
372
373     IO.popen(%w(megacli -PDList -aAll -NoLog)).each do |line|
374       if line =~ /^Adapter #(\d+)$/
375         controller = controllers[Regexp.last_match(1).to_i]
376       elsif controller && line =~ /^Enclosure Device ID: \d+$/
377         disk = {
378           :controller => controller[:id]
379         }
380       elsif disk && line =~ /^WWN:\s+(\S+)$/
381         unless devices[:disks].find { |d| d[:wwn] == Regexp.last_match(1) }
382           disk[:id] = devices[:disks].count
383           disk[:wwn] = Regexp.last_match(1)
384
385           devices[:disks] << disk
386         end
387       elsif disk && line =~ /^(\S.*\S)\s*:\s+(\S.*)$/
388         case Regexp.last_match(1)
389         when "Device Id" then disk[:smart_devlce] = "megaraid,#{Regexp.last_match(2)}"
390         when "WWN" then disk[:wwn] = Regexp.last_match(2)
391         when "PD Type" then disk[:interface] = Regexp.last_match(2)
392         when "Raw Size" then disk[:size] = memory_to_disk_size(Regexp.last_match(2).sub(/\s*\[.*\]$/, ""))
393         when "Inquiry Data" then disk[:vendor], disk[:model], disk[:serial] = Regexp.last_match(2).split
394         end
395       end
396     end
397
398     controllers.each do |controller|
399       if device = Dir.glob("/sys/bus/pci/devices/#{controller[:pci_slot]}/host*/target*:2:0/*/scsi_generic/sg*").first
400         controller[:device] = "/dev/#{File.basename(device)}"
401       end
402     end
403   end
404
405   def find_mpt_disks(devices)
406     controllers = []
407
408     IO.popen(%w(sas2ircu list)).each do |line|
409       next unless line =~ /^\s+(\d+)\s+(\S+)\s+\h+h\s+\h+h\s+(\S+)\s+\h+h\s+\h+h\s*$/
410       controllers[Regexp.last_match(1).to_i] = {
411         :id => devices[:controllers].count,
412         :model => Regexp.last_match(2),
413         :pci_slot => Regexp.last_match(3).sub(/^(\h{2})h:(\h{2})h:(\h{2})h:0(\h)h$/, "00\\1:\\2:\\3.\\4"),
414         :arrays => [],
415         :disks => []
416       }
417
418       devices[:controllers] << controllers[Regexp.last_match(1).to_i]
419     end
420
421     controllers.each_with_index do |controller, index|
422       arrays = []
423       disks = []
424
425       array = nil
426       disk = nil
427
428       IO.popen(["sas2ircu", index.to_s, "display"]).each do |line|
429         if line =~ /^IR volume (\d+)$/
430           array = {
431             :id => devices[:arrays].count,
432             :controller => controller[:id],
433             :number => Regexp.last_match(1),
434             :disks => []
435           }
436
437           devices[:arrays] << array
438           controller[:arrays] << array[:id]
439
440           arrays << array
441         elsif line =~ /^Device is a Hard disk$/
442           disk = {
443             :id => devices[:disks].count,
444             :controller => controller[:id],
445             :arrays => []
446           }
447
448           devices[:disks] << disk
449           controller[:disks] << disk[:id]
450
451           disks << disk
452         elsif disk && line =~ /^  (\S.*\S)\s+:\s+(.*\S)\s*$/
453           case Regexp.last_match(1)
454           when "Enclosure #" then disk[:location] = Regexp.last_match(2)
455           when "Slot #" then disk[:location] = "#{disk[:location]}:#{Regexp.last_match(2)}"
456           when "SAS Address" then disk[:device] = find_sas_device(Regexp.last_match(2).tr("-", ""))
457           when "Size (in MB)/(in sectors)" then disk[:size] = memory_to_disk_size("#{Regexp.last_match(2).split('/').first} MB")
458           when "Manufacturer" then disk[:vendor] = Regexp.last_match(2)
459           when "Model Number" then disk[:model] = Regexp.last_match(2)
460           when "Firmware Revision" then disk[:firmware_version] = Regexp.last_match(2)
461           when "Serial Number" then disk[:serial_number] = Regexp.last_match(2)
462           when "Protocol" then disk[:interface] = Regexp.last_match(2)
463           end
464         elsif array && line =~ /^  PHY\[\d+\] Enclosure#\/Slot#\s+:\s+(\d+:\d+)\s*$/
465           array[:disks] << Regexp.last_match(1)
466         elsif array && line =~ /^  (\S.*\S)\s+:\s+(.*\S)\s*$/
467           case Regexp.last_match(1)
468           when "Volume wwid" then array[:device] = find_sas_device(Regexp.last_match(2))
469           when "RAID level" then array[:raid_level] = Regexp.last_match(2).sub(/^RAID/, "")
470           when "Size (in MB)" then array[:size] = "#{Regexp.last_match(2)} MB"
471           end
472         elsif line =~ /^  (\S.*\S)\s+:\s+(.*\S)\s*$/
473           case Regexp.last_match(1)
474           when "BIOS version" then controller[:bios_version] = Regexp.last_match(2)
475           when "Firmware version" then controller[:firmware_version] = Regexp.last_match(2)
476           end
477         end
478       end
479
480       arrays.each do |array|
481         array[:disks].map! do |location|
482           disk = disks.find { |disk| disk[:location] == location }
483
484           disk[:arrays] << array[:id]
485           disk[:id]
486         end
487       end
488     end
489   end
490
491   def find_areca_disks(devices)
492     controller = {
493       :id => devices[:controllers].count,
494       :arrays => [],
495       :disks => []
496     }
497
498     devices[:controllers] << controller
499
500     IO.popen(%w(/opt/areca/x86_64/cli64 sys info)).each do |line|
501       next unless line =~ /^(\S.*\S)\s+:\s+(.*\S)\s*$/
502
503       case Regexp.last_match(1)
504       when "Firmware Version" then controller[:firmware_version] = Regexp.last_match(2)
505       when "BOOT ROM Version" then controller[:bios_version] = Regexp.last_match(2)
506       when "Serial Number" then controller[:serial_number] = Regexp.last_match(2)
507       when "Controller Name" then controller[:model] = Regexp.last_match(2)
508       end
509     end
510
511     path = Dir.glob("/sys/bus/pci/devices/*/host*/scsi_host/host*/host_fw_model").find do |file|
512       read_sysctl_file(file) == controller[:model]
513     end
514
515     controller[:pci_slot] = File.basename(File.expand_path("#{path}/../../../.."))
516     controller[:device] = File.basename(Dir.glob(File.expand_path("#{path}/../../../target0:0:16/0:0:16:0/scsi_generic/*")).first)
517
518     arrays = []
519
520     IO.popen(%w(/opt/areca/x86_64/cli64 vsf info)).each do |line|
521       next unless line =~ /^\s+(\d+)\s+/
522       array = {
523         :id => devices[:arrays].count,
524         :number => Regexp.last_match(1),
525         :controller => controller[:id],
526         :disks => []
527       }
528
529       devices[:arrays] << array
530       controller[:arrays] << array[:id]
531
532       arrays << array
533     end
534
535     arrays.each do |array|
536       IO.popen(["/opt/areca/x86_64/cli64", "vsf", "info", "vol=#{array[:number]}"]).each do |line|
537         next unless line =~ /^(\S.*\S)\s+:\s+(.*\S)\s*$/
538
539         case Regexp.last_match(1)
540         when "Volume Set Name" then array[:volume_set] = Regexp.last_match(2)
541         when "Raid Set Name" then array[:raid_set] = Regexp.last_match(2)
542         when "Volume Capacity" then array[:size] = format_disk_size(Regexp.last_match(2).to_f * 1000 * 1000)
543         when "Raid Level" then array[:raid_level] = Regexp.last_match(2).sub(/^Raid/, "")
544         end
545       end
546     end
547
548     disks = []
549
550     IO.popen(%w(/opt/areca/x86_64/cli64 disk info)).each do |line|
551       next unless line =~ /^\s+(\d+)\s+.*\s+\d+\.\d+GB\s+(\S.*\S)\s*$/
552       next if Regexp.last_match(2) == "N.A."
553
554       disk = {
555         :id => devices[:disks].count,
556         :number => Regexp.last_match(1),
557         :controller => controller[:id],
558         :arrays => []
559       }
560
561       devices[:disks] << disk
562       controller[:disks] << disk[:id]
563
564       if array = arrays.find { |array| array[:raid_set] == Regexp.last_match(2) }
565         disk[:arrays] << array[:id]
566         array[:disks] << disk[:id]
567       end
568
569       disks << disk
570     end
571
572     disks.each do |disk|
573       IO.popen(["/opt/areca/x86_64/cli64", "disk", "info", "drv=#{disk[:number]}"]).each do |line|
574         if line =~ /^IDE Channel\s+:\s+(\d+)\s*$/i
575           disk[:smart_device] = "areca,#{Regexp.last_match(1)}"
576         elsif line =~ /^Device Location\s+:\s+Enclosure#(\d+) Slot#?\s*0*(\d+)\s*$/i
577           disk[:smart_device] = "areca,#{Regexp.last_match(2)}/#{Regexp.last_match(1)}"
578         elsif line =~ /^(\S.*\S)\s+:\s+(.*\S)\s*$/
579           case Regexp.last_match(1)
580           when "Model Name" then disk[:vendor], disk[:model] = Regexp.last_match(2).split
581           when "Serial Number" then disk[:serial_number] = Regexp.last_match(2)
582           when "Disk Capacity" then disk[:size] = format_disk_size(Regexp.last_match(2).to_f * 1000 * 1000)
583           end
584         end
585       end
586     end
587   end
588
589   collect_data(:default) do
590     hardware Mash.new
591
592     hardware[:pci] = pci_devices
593     hardware[:network] = network_devices
594     hardware[:memory] = memory_devices
595     hardware[:disk] = disk_devices
596   end
597 end