Node discovery agent for Fuel
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

agent 48KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410
  1. #!/usr/bin/env ruby
  2. # Copyright 2013 Mirantis, Inc.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  5. # not use this file except in compliance with the License. You may obtain
  6. # a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. # License for the specific language governing permissions and limitations
  14. # under the License.
  15. begin
  16. require 'rubygems'
  17. rescue LoadError
  18. end
  19. require 'facter'
  20. require 'json'
  21. require 'httpclient'
  22. require 'logger'
  23. require 'optparse'
  24. require 'yaml'
  25. require 'ipaddr'
  26. require 'rethtool'
  27. require 'digest'
  28. require 'timeout'
  29. require 'uri'
  30. require 'optparse'
  31. # TODO(vsharshov): replace below lines by this string after excluding Ruby 1.8
  32. require 'pathname'
  33. require 'rexml/document'
  34. require 'socket'
  35. include REXML
  36. unless Process.euid == 0
  37. puts "You must be root"
  38. exit 1
  39. end
  40. ENV['PATH'] = "/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin"
  41. AGENT_CONFIG = "/etc/nailgun-agent/config.yaml"
  42. # look at https://github.com/torvalds/linux/blob/master/Documentation/devices.txt
  43. # KVM virtio volumes has code 252 in CentOS, but 253 in Ubuntu
  44. # Please also update the device codes here
  45. # https://github.com/stackforge/fuel-astute/blob/master/mcagents/erase_node.rb#L81
  46. # NVMe has code 259
  47. STORAGE_CODES = [3, 8, 9, 65, 66, 67, 68, 69, 70, 71, 104, 105, 106, 107, 108, 109, 110, 111, 202, 251, 252, 253, 259]
  48. REMOVABLE_VENDORS = [
  49. "Adaptec", "IBM", "ServeRA",
  50. ]
  51. # PCI vendor IDs for Adaptec
  52. REMOVABLE_PCI_VENDORS = [
  53. "0x1044", "0x9004", "0x9005",
  54. ]
  55. # Set default data structure for SR-IOV
  56. DEFAULT_SRIOV = {
  57. "sriov_totalvfs" => 0,
  58. "available" => false,
  59. "pci_id" => ""
  60. }
  61. def digest(body)
  62. if body.is_a? Hash
  63. digest body.map { |k,v| [digest(k),digest(v)].join("=>") }.sort
  64. elsif body.is_a? Array
  65. body.map{ |v| digest v }.join('|')
  66. else
  67. [body.class.to_s, body.to_s].join(":")
  68. end
  69. end
  70. def createsig(body)
  71. Digest::SHA1.hexdigest( digest body )
  72. end
  73. class Offloading
  74. def initialize(name, sub)
  75. @name, @sub = name, sub
  76. end
  77. def to_json(options = {})
  78. {'name' => @name, 'state' => nil, 'sub' => @sub}.to_json()
  79. end
  80. def to_s
  81. "#{@name}: #{@sub}"
  82. end
  83. end
  84. class NodeAgent
  85. API_DEFAULT_ADDRESS = "localhost"
  86. API_DEFAULT_PORT = "8443"
  87. API_LEGACY_PORT = "8000"
  88. def initialize(logger, dry_run)
  89. @logger = logger
  90. @settings = get_settings()
  91. unless dry_run
  92. @api_ip = URI(@settings['url']).host || API_DEFAULT_ADDRESS
  93. scheme, api_port = get_scheme_and_port
  94. @api_url = "#{scheme}://#{@api_ip}:#{api_port}/api"
  95. @logger.info("API URL is #{@api_url}")
  96. end
  97. @facter = facter_system_info
  98. @network = _network
  99. @numa_topology = get_numa_topology
  100. end
  101. def get_scheme_and_port
  102. scheme, api_port = nil
  103. begin
  104. res = htclient.get("https://#{@api_ip}:#{API_DEFAULT_PORT}/")
  105. scheme, api_port = "https", API_DEFAULT_PORT
  106. rescue Errno::ECONNREFUSED
  107. @logger.warn("Connection Refused catched when trying connect to HTTPS port. Use plain HTTP")
  108. scheme, api_port = "http", API_LEGACY_PORT
  109. end
  110. return scheme, api_port
  111. end
  112. # transform string into Dictionary
  113. # For example, line: "initrd=/images/bootstrap/initramfs.img ksdevice=bootif lang="
  114. # will be transformed into: {"mco_user"=>"mcollective", "initrd"=>"/images/bootstrap/initramfs.img", "lang"=>nil}
  115. def string_to_hash(string)
  116. hash = Hash.new
  117. string.split(' ').each do |pair|
  118. key,value = pair.split(/=/, 2)
  119. hash[key] = value
  120. end
  121. hash
  122. end
  123. def get_settings
  124. agent_settings = YAML.load_file(AGENT_CONFIG) rescue {}
  125. cmdline_settings = string_to_hash(File.read("/proc/cmdline")) rescue {}
  126. agent_settings.merge(cmdline_settings)
  127. end
  128. def facter_system_info
  129. Facter.loadfacts
  130. Facter.to_hash
  131. end
  132. def put
  133. headers = {"Content-Type" => "application/json"}
  134. @logger.debug("Trying to put host info into #{@api_url}")
  135. res = htclient.put("#{@api_url}/nodes/agent/", _data.to_json, headers)
  136. @logger.debug("Response: status: #{res.status} body: #{res.body}")
  137. if res.status < 200 or res.status >= 400
  138. @logger.error("HTTP PUT failed: #{res.inspect}")
  139. end
  140. res
  141. end
  142. def post
  143. headers = {"Content-Type" => "application/json"}
  144. @logger.debug("Trying to create host using #{@api_url}")
  145. res = htclient.post("#{@api_url}/nodes/", _data.to_json, headers)
  146. @logger.debug("Response: status: #{res.status} body: #{res.body}")
  147. res
  148. end
  149. def htclient
  150. client = HTTPClient.new
  151. client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
  152. client.ssl_config.ssl_version = :TLSv1
  153. client.connect_timeout = 10
  154. client.send_timeout = 10
  155. client.receive_timeout = 10 # (mihgen): Nailgun may hang for a while, but 10sec should be enough for him to respond
  156. client
  157. end
  158. def _get_iface_info(ifname)
  159. info = {}
  160. info[:name] = ifname
  161. info[:addresses] = {}
  162. if ifname =~ /^(\D+)(\d+.*)/ # enp0s11, enp0, eth0
  163. info[:type] = $1 # enp, enp, eth
  164. info[:number] = $2 # 0s11, 0, 0
  165. end
  166. data = `ip a s dev #{ifname}`
  167. #2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast master br-fw-admin state UP group default qlen 1000
  168. # link/ether 64:de:13:ab:f4:1d brd ff:ff:ff:ff:ff:ff
  169. # inet6 fe80::66de:13ff:feab:f41d/64 scope link
  170. # valid_lft forever preferred_lft forever
  171. data.each_line do |line|
  172. case line.strip
  173. when /(\d+): #{ifname}: <([^>]*)> mtu (\d+) (.+) state (\w+)/
  174. info[:flags] = $2.split(',')
  175. info[:mtu] = $3
  176. info[:state] = $5.downcase
  177. when /link\/(\w+) ([\da-f\:]+) brd ([\da-f\:]+)/
  178. info[:addresses][$2.upcase] = { :family => "lladdr" } if $2 != "00:00:00:00:00:00"
  179. info[:encapsulation] = case $1
  180. when /loopback/i then 'Loopback'
  181. when /IPIP Tunnel/ then 'IPIP'
  182. when /Point-to-Point Protocol/ then 'PPP'
  183. when /IPv6-in-IPv4/ then '6to4'
  184. when /ether/ then'Ethernet'
  185. else nil
  186. end
  187. when /inet (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(\/(\d{1,2}))( brd (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}))? scope (\w+)?/
  188. info[:addresses][$1] = { :family => "inet", "prefixlen" => $3 ||32 }
  189. info[:addresses][$1][:scope] = ($6.eql?("host") ? "Node" : $6.capitalize)
  190. info[:addresses][$1][:netmask] = IPAddr.new("255.255.255.255").mask(($3 ||32).to_i).to_s
  191. info[:addresses][$1][:broadcast] = $5
  192. when /inet6 ([a-f0-9\:]+)\/(\d+) scope (\w+)/
  193. info[:addresses][$1] = { :family => "inet6", "prefixlen" => $2, "scope" => ($3.eql?("host") ? "Node" : $3.capitalize) }
  194. end
  195. end
  196. data = `ip -d link show dev #{ifname}`
  197. #2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast master br-fw-admin state UP mode DEFAULT group default qlen 1000
  198. # link/ether 64:de:13:ab:f4:1d brd ff:ff:ff:ff:ff:ff promiscuity 1
  199. # bridge_slave state forwarding priority 32 cost 4 hairpin off guard off root_block off fastleave off learning on flood on addrgenmode eui64
  200. data.each_line do |line|
  201. next if line =~ /^\d+/
  202. if line =~ /state (\w+)/
  203. info[:state] = $1.downcase
  204. end
  205. if line =~ /vlan id (\d+)/
  206. vid = $1
  207. info[:state][:vlan] = {}
  208. info[:state][:vlan][:id] = vid
  209. end
  210. end
  211. info
  212. end
  213. def _get_all_interfaces_info
  214. res = {}
  215. res[:interfaces] = {}
  216. Facter::Util::IP.get_interfaces().each do |ifname|
  217. res[:interfaces][ifname] = _get_iface_info(ifname)
  218. end
  219. %w[inet inet6].each do |family|
  220. #default via 10.109.3.1 dev br-ex
  221. #10.109.0.0/24 dev br-fw-admin proto kernel scope link src 10.109.0.4
  222. #10.109.1.0/24 dev br-mgmt proto kernel scope link src 10.109.1.3
  223. #10.109.2.0/24 dev br-storage proto kernel scope link src 10.109.2.3
  224. #10.109.3.0/24 dev br-ex proto kernel scope link src 10.109.3.3
  225. #240.0.0.0/30 dev hapr-host proto kernel scope link src 240.0.0.1
  226. #240.0.0.4/30 dev vr-host-base proto kernel scope link src 240.0.0.5
  227. `ip -f #{family} route show`.each_line do |line|
  228. if line =~ /^([^\s]+)\s(.*)$/
  229. rdest = $1
  230. rend = $2
  231. next if not rend =~ /\bdev\s+([^\s]+)\b/
  232. rint = $1
  233. next if not res[:interfaces].has_key?(rint)
  234. rent = {:destination => rdest, :family => family}
  235. %w[via scope metric proto src].each do |k|
  236. rent[k] = $1 if rend =~ /\b#{k}\s+([^\s]+)\b/
  237. end
  238. next if rent[:src] and not res[:interfaces][rint].has_key?(rent[:src])
  239. res[:interfaces][rint][:routes] = [] if not res[:interfaces][rint][:routes]
  240. res[:interfaces][rint][:routes] << rent
  241. end
  242. end
  243. end
  244. res
  245. end
  246. def _network
  247. iface = nil
  248. gw = nil
  249. route = `ip r list 0/0`.strip # 'default via 10.21.5.1 dev eth0'
  250. if route =~ /^default via ?([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) dev ([a-zA-Z0-9_-]+)/
  251. gw = $1
  252. iface = $2
  253. end
  254. result = _get_all_interfaces_info
  255. if gw and iface
  256. result[:default_gateway] = gw
  257. result[:default_interface] = iface
  258. result[:mac] = @facter["macaddress_#{iface.gsub('-', '_')}"].upcase
  259. end
  260. result
  261. end
  262. def _get_detailed_cpuinfo
  263. real = {}
  264. info = {}
  265. info[:total] = 0
  266. curr_proc = nil
  267. File.open('/proc/cpuinfo').each do |l|
  268. case l.strip
  269. when /processor\s+:\s(.+)/
  270. info[:total] += 1
  271. curr_proc = $1
  272. info[curr_proc] = {}
  273. when /^cpu MHz\s+:\s(.+)/
  274. info[curr_proc][:mhz] = $1
  275. when /^physical id\s+:\s(.+)/
  276. info[curr_proc][:physical_id] = $1
  277. real[$1] = true
  278. when /^flags\s+:\s(.+)/
  279. info[curr_proc][:flags] = $1.split
  280. when /^address sizes\s+:\s(\d+) bits (\w+), (\d+) bits (\w+)/
  281. info[curr_proc][:address_sizes] = {}
  282. info[curr_proc][:address_sizes][$2.to_sym] = $1
  283. info[curr_proc][:address_sizes][$4.to_sym] = $3
  284. when /(.+)\s+:\s(.+)/
  285. value = $2
  286. key = $1.strip.downcase.gsub(/ /, '_')
  287. info[curr_proc][key.to_sym] = value
  288. end
  289. end
  290. info[:real] = real.keys.size
  291. info
  292. end
  293. def _get_blkdev_info
  294. info = {}
  295. if File.directory?('/sys/block/')
  296. begin
  297. Timeout::timeout(10) do
  298. Dir['/sys/block/*'].each do |blkdir|
  299. blkdev = File.basename(blkdir)
  300. info[blkdev] = Hash.new
  301. Dir.glob("/sys/block/#{blkdev}/{size,removable}").each do |g|
  302. File.open(g) { |f| info[blkdev][File.basename(g).to_sym] = f.read_nonblock(1024).strip }
  303. end
  304. Dir.glob("/sys/block/#{blkdev}/device/{model,rev,state,timeout,vendor}").each do |g|
  305. File.open(g) { |f| info[blkdev][File.basename(g).to_sym] = f.read_nonblock(1024).strip }
  306. end
  307. end # of blkdir
  308. end # of timeout
  309. rescue => e
  310. @logger.error("Error '#{e.message}' in gathering disks metadata: #{e.backtrace}")
  311. end
  312. end # File.directory
  313. info
  314. end
  315. def _get_dmi_info
  316. info = {}
  317. Dir['/sys/class/dmi/id/*'].each do |key|
  318. if File.file?(key)
  319. case File.basename(key)
  320. when /product_uuid/
  321. File.open(key) {|f| info[:uuid] = f.read_nonblock(1024).strip}
  322. when /sys_vendor/
  323. File.open(key) {|f| info[:sys_vendor] = info[:manufacturer] = f.read_nonblock(1024).strip}
  324. else
  325. File.open(key) {|f| info[File.basename(key).to_sym] = f.read_nonblock(1024).strip}
  326. end
  327. end
  328. end
  329. info
  330. end
  331. def _get_dmidecode_system_info
  332. info = {}
  333. info[:system] = {}
  334. # dmidecode -t system
  335. ## dmidecode 3.0
  336. #Getting SMBIOS data from sysfs.
  337. #SMBIOS 2.8 present.
  338. #
  339. #Handle 0x0100, DMI type 1, 27 bytes
  340. #System Information
  341. # Manufacturer: QEMU
  342. # Product Name: Standard PC (i440FX + PIIX, 1996)
  343. # Version: pc-i440fx-2.4
  344. # Serial Number: Not Specified
  345. # UUID: 0C2041CE-94E4-453D-95DD-1682D5D8E487
  346. # Wake-up Type: Power Switch
  347. # SKU Number: Not Specified
  348. # Family: Not Specified
  349. #
  350. #Handle 0x2000, DMI type 32, 11 bytes
  351. #System Boot Information
  352. # Status: No errors detected
  353. `dmidecode -t system`.each_line do |l|
  354. case l.strip
  355. when /^SMBIOS (\S+)/
  356. info[:dmidecode_version] = $1.strip
  357. when /(.+):(.+)/
  358. k = $1
  359. v = $2
  360. info[:system][k.downcase.gsub(/([ -])/, '_').to_sym] = v.strip
  361. end
  362. end
  363. info
  364. end
  365. # transform input array into array of the objects
  366. # Example:
  367. # [{
  368. # "state":null,
  369. # "sub":[
  370. # {
  371. # "state":null,
  372. # "sub":[],
  373. # "name":"tx-checksum-ipv6"
  374. # },
  375. # ...........
  376. # ],
  377. # "name":"tx-checksumming"
  378. # },
  379. # {
  380. # "state":null,
  381. # "sub":[],
  382. # "name":"generic-segmentation-offload"
  383. # },
  384. # .............
  385. # ]
  386. def _parse_offloading(offloading_arr)
  387. return [] if offloading_arr.empty?
  388. inner = []
  389. current = offloading_arr.shift()
  390. while offloading_arr.any? && offloading_arr.first().start_with?("\t") do
  391. inner << offloading_arr.shift()[1..-1]
  392. end
  393. res = _parse_offloading(offloading_arr)
  394. res << Offloading.new(current, _parse_offloading(inner))
  395. end
  396. # Gets information about SR-IOV for specified pci slot
  397. # using 'lspci' utility. Example of output to parse:
  398. # ...
  399. # Capabilities: [160 v1] Single Root I/O Virtualization (SR-IOV)
  400. # IOVCap: Migration-, Interrupt Message Number: 000
  401. # IOVCtl: Enable- Migration- Interrupt- MSE- ARIHierarchy-
  402. # IOVSta: Migration-
  403. # Initial VFs: 8, Total VFs: 8, Number of VFs: 0, Function Dependency Link: 01
  404. # VF offset: 128, stride: 4, Device ID: 10ed
  405. # Supported Page Size: 00000553, System Page Size: 00000001
  406. # Region 0: Memory at 0000000090040000 (64-bit, prefetchable)
  407. # Region 3: Memory at 0000000090060000 (64-bit, prefetchable)
  408. # VF Migration: offset: 00000000, BIR: 0
  409. # ...
  410. def sriov_info(int, int_bus_info)
  411. sriov = DEFAULT_SRIOV.dup
  412. lspci = _get_lspci_info(int_bus_info)
  413. if lspci.match(/.*Capabilities:.*SR-IOV.*/)
  414. sriov["sriov_totalvfs"] = lspci.scan(/\s+Total\s+VFs:\s+(\d+)/).last.first.to_i
  415. unless sriov["sriov_totalvfs"] == 0
  416. sriov["available"] = true
  417. sriov["sriov_totalvfs"] -= 1
  418. end
  419. vf_vendor = File.read("/sys/class/net/#{int}/device/vendor").chomp.gsub(/^0x/, '')
  420. vf_device = lspci.scan(/VF\s+.*\s+Device\s+ID:\s+([A-Fa-f0-9]+)/).last.first
  421. sriov["pci_id"] = "#{vf_vendor}:#{vf_device}"
  422. end
  423. sriov
  424. rescue
  425. DEFAULT_SRIOV
  426. end
  427. def nic_pci_id(bus_info)
  428. vendor = File.read("/sys/bus/pci/devices/#{bus_info}/vendor").chomp.gsub(/^0x/, '')
  429. device = File.read("/sys/bus/pci/devices/#{bus_info}/device").chomp.gsub(/^0x/, '')
  430. "#{vendor}:#{device}"
  431. rescue
  432. ""
  433. end
  434. def nic_numa_node(int_bus_info)
  435. numa_node = @numa_topology[:numa_nodes].select { |node|
  436. node[:pcidevs].include?(int_bus_info)
  437. }
  438. numa_node.first[:id].to_i
  439. rescue
  440. nil
  441. end
  442. def _is_in_bond(iface_name)
  443. File.exist? "/sys/class/net/#{iface_name}/master" rescue False
  444. end
  445. def _is_in_bridge(iface_name)
  446. File.exist? "/sys/class/net/#{iface_name}/brport" rescue False
  447. end
  448. def _get_iface_bridge_name(iface_name)
  449. File.basename(File.readlink("/sys/class/net/#{iface_name}/brport/bridge"))
  450. end
  451. def _get_iface_bond_name(iface_name)
  452. File.basename(File.readlink("/sys/class/net/#{iface_name}/master"))
  453. end
  454. def _get_interface_mac(iface_name, swaddr)
  455. # Get original mac excluding case with empty EEPROM data
  456. mac = "00:00:00:00:00:00"
  457. # It is a virtual device, lets read address file in sysfs
  458. if File.exist? "/sys/devices/virtual/net/#{iface_name}"
  459. File.open("/sys/devices/virtual/net/#{iface_name}/address") do
  460. |file|
  461. mac = file.readlines[0].chomp.downcase
  462. end
  463. return mac
  464. end
  465. # It is not a virtual device, lets ask ethtool first
  466. perm_addr = `ethtool -P #{iface_name}`
  467. begin
  468. re = eval '/(?<=Permanent address: )(?!00(:00){5}).+/'
  469. rescue SyntaxError
  470. re = perm_addr.match(/(00(:00){5})+/).nil? ? /[0-9a-f]+(:[0-9a-f]+){5}$/ : nil
  471. end
  472. mac = perm_addr.match(re)[0] rescue swaddr
  473. mac.downcase
  474. end
  475. def _get_parent_interface(iface_name)
  476. if _is_in_bond(iface_name)
  477. bond_name = _get_iface_bond_name(iface_name)
  478. if _is_in_bridge(bond_name)
  479. return _get_iface_bridge_name(bond_name)
  480. else
  481. return bond_name
  482. end
  483. elsif _is_in_bridge(iface_name)
  484. return _get_iface_bridge_name(iface_name)
  485. else
  486. iface_name
  487. end
  488. end
  489. def _get_max_queues(ifname)
  490. data = `ethtool -l #{ifname}`
  491. # Example of output to parse:
  492. # Channel parameters for ens4f0:
  493. # Pre-set maximums:
  494. # RX: 0
  495. # TX: 0
  496. # Other: 1
  497. # Combined: 63
  498. # Current hardware settings:
  499. # RX: 0
  500. # TX: 0
  501. # Other: 1
  502. # Combined: 40
  503. return nil if $?.to_i != 0
  504. return data.scan(/Pre-set maximums:.*?Combined:\s*(\d+)/m).join.to_i
  505. rescue
  506. return nil
  507. end
  508. def _detailed
  509. detailed_meta = {
  510. :system => _system_info,
  511. :interfaces => [],
  512. :cpu => {
  513. :total => (@facter['processorcount'].to_i rescue nil),
  514. :real => (@facter['physicalprocessorcount'].to_i rescue nil),
  515. :spec => [],
  516. },
  517. :disks => [],
  518. :memory => (_dmi_memory or _facter_memory),
  519. :pci_devices => _get_pci_dev_list,
  520. :numa_topology => @numa_topology,
  521. }
  522. admin_mac = (_master_ip_and_mac[:mac] or @network[:mac]) rescue nil
  523. begin
  524. (@network[:interfaces] or {} rescue {}).each do |int, intinfo|
  525. #next if not intinfo.has_key?(:name)
  526. #int = intinfo[:name]
  527. # Send info about physical interfaces only
  528. next if int =~ /.*@.*/
  529. next if intinfo[:encapsulation] !~ /^Ethernet.*/
  530. # Avoid virtual devices like loopback, tunnels, bonding, vlans ...
  531. # TODO(vsharshov): replace below lines by this string after excluding Ruby 1.8
  532. # next if File.realpath("/sys/class/net/#{int}") =~ /virtual/
  533. next if Pathname.new("/sys/class/net/#{int}").realpath.to_s =~ /virtual/
  534. # Avoid wireless
  535. next if File.exist?("/sys/class/net/#{int}/phy80211") ||
  536. File.exist?("/sys/class/net/#{int}/wireless")
  537. # Skip virtual functions
  538. next if File.exists?("/sys/class/net/#{int}/device/physfn")
  539. int_meta = {:name => int}
  540. int_meta[:interface_properties] = {}
  541. int_meta[:state] = intinfo[:state]
  542. (intinfo[:addresses] or {} rescue {}).each do |addr, addrinfo|
  543. if (addrinfo[:family] rescue nil) =~ /lladdr/
  544. # Get original mac excluding case with empty EEPROM data
  545. perm_addr = `ethtool -P #{int}`
  546. int_meta[:mac] = _get_interface_mac(int_meta[:name], addr)
  547. int_meta[:pxe] = _get_interface_mac(_get_parent_interface(int_meta[:name]), addr) == admin_mac.downcase
  548. begin
  549. int_info = Rethtool::InterfaceSettings.new(int)
  550. int_meta[:driver] = int_info.driver
  551. int_meta[:bus_info] = int_info.bus_info
  552. int_meta[:max_speed] = int_info.best_mode.speed
  553. if int_info.current_mode.speed == :unknown
  554. int_meta[:current_speed] = nil
  555. else
  556. int_meta[:current_speed] = int_info.current_mode.speed
  557. end
  558. rescue
  559. int_meta[:current_speed] = nil
  560. end
  561. unless int_meta[:driver]
  562. # Rethtool::InterfaceSettings calls two ioctls: with
  563. # ETHTOOL_CMD_GSET and ETHTOOL_CMD_GDRVINFO commands.
  564. # But for virtio adapters the first is not implemented,
  565. # but the second is. So try to get driver info at least
  566. # in this fallback chain.
  567. int_meta[:driver], int_meta[:bus_info] = _get_interface_driver_info(int)
  568. end
  569. elsif (addrinfo[:family] rescue nil) =~ /^inet$/
  570. int_meta[:ip] = addr
  571. int_meta[:netmask] = addrinfo[:netmask] if addrinfo[:netmask]
  572. end
  573. end
  574. begin
  575. # this stuff will put all non-fixed offloading mode into array
  576. # collect names of non-fixed offloading modes
  577. # Example of ethtool -k ethX output:
  578. # tx-checksumming: on
  579. # tx-checksum-ipv4: on
  580. # tx-checksum-ip-generic: off [fixed]
  581. # tx-checksum-ipv6: on
  582. # tx-checksum-fcoe-crc: off [fixed]
  583. # tx-checksum-sctp: on
  584. # scatter-gather: on
  585. # tx-scatter-gather: on
  586. # tx-scatter-gather-fraglist: off [fixed]
  587. # generic-segmentation-offload: on
  588. offloading_data = `ethtool -k #{int}`.split("\n").reject { |offloading|
  589. offloading.include?("Features for") ||
  590. offloading.include?("fixed")
  591. }.map { |offloading|
  592. offloading.split(':')[0]
  593. }
  594. # transform raw data into array of objects
  595. int_meta[:offloading_modes] = _parse_offloading(offloading_data)
  596. rescue
  597. # in case if we have no `ethtool` package installed we should
  598. # return empty array to support nailgun's rest api call
  599. int_meta[:offloading_modes] = []
  600. end
  601. # Getting SR-IOV info
  602. int_meta[:interface_properties][:sriov] = sriov_info(int, int_meta[:bus_info])
  603. # Get PCI-ID
  604. int_meta[:interface_properties][:pci_id] = nic_pci_id(int_meta[:bus_info])
  605. # Get numa node
  606. int_meta[:interface_properties][:numa_node] = nic_numa_node(int_meta[:bus_info])
  607. # Get maximum queues
  608. int_meta[:interface_properties][:max_queues] = _get_max_queues(int)
  609. detailed_meta[:interfaces] << int_meta
  610. end
  611. rescue Exception => e
  612. @logger.error("Error '#{e.message}' in gathering interfaces metadata: #{e.backtrace}")
  613. end
  614. begin
  615. (_get_detailed_cpuinfo or {} rescue {}).each do |cpu, cpuinfo|
  616. if cpu =~ /^[\d]+/ and cpuinfo
  617. frequency = cpuinfo[:mhz].to_i rescue nil
  618. begin
  619. # ohai returns current frequency, try to get max if possible
  620. max_frequency = `cat /sys/devices/system/cpu/cpu#{cpu}/cpufreq/cpuinfo_max_freq 2>/dev/null`.to_i / 1000
  621. frequency = max_frequency if max_frequency > 0
  622. rescue
  623. end
  624. detailed_meta[:cpu][:spec] << {
  625. :frequency => frequency,
  626. :model => (cpuinfo[:model_name].gsub(/ +/, " ") rescue nil)
  627. }
  628. end
  629. end
  630. rescue Exception => e
  631. @logger.error("Error '#{e.message}' in gathering cpu metadata: #{e.backtrace}")
  632. end
  633. begin
  634. Timeout::timeout(30) do
  635. @logger.debug("Trying to find block devices")
  636. # ohai reports the disk size according to /sys/block/#{bname}
  637. # which is always measured in 512 bytes blocks, no matter what
  638. # the physical (minimal unit which can be atomically written)
  639. # or logical (minimal # unit which can be addressed) block sizes are, see
  640. # http://lxr.free-electrons.com/source/include/linux/types.h?v=4.4#L124
  641. # http://lxr.free-electrons.com/source/drivers/scsi/sd.c?v=4.4#L2340
  642. block_size = 512
  643. mpath_devices, skip_devices = _multipath_devices
  644. (_get_blkdev_info or {} rescue {}).each do |bname, binfo|
  645. @logger.debug("Found block device: #{bname}")
  646. @logger.debug("Block device info: #{binfo.inspect}")
  647. dname = bname.gsub(/!/, '/')
  648. next if skip_devices.include?(dname)
  649. if physical_data_storage_devices.map{|d| d[:name]}.include?(bname) && binfo
  650. @logger.debug("Block device seems to be physical data storage: #{bname}")
  651. block = physical_data_storage_devices.select{|d| d[:name] == bname}[0]
  652. if block[:removable] =~ /^1$/ && ! REMOVABLE_VENDORS.include?(binfo[:vendor])
  653. pci_vendor_id = _get_pci_vendor_id(bname)
  654. @logger.debug("Block device #{bname} is removable. PCI vendor ID: #{pci_vendor_id}")
  655. unless REMOVABLE_PCI_VENDORS.include?(pci_vendor_id)
  656. next
  657. end
  658. @logger.debug("Block device #{bname} is accepted by PCI vendor ID")
  659. end
  660. detailed_meta[:disks] << {
  661. :name => dname,
  662. :model => binfo[:model],
  663. :size => (binfo[:size].to_i * block_size),
  664. :disk => block[:disk],
  665. :extra => block[:extra],
  666. :removable => block[:removable],
  667. :paths => nil,
  668. :opt_io => get_opt_io.fetch(dname, block_size)
  669. }
  670. elsif mpath_devices.has_key?(dname)
  671. device = mpath_devices[dname]
  672. detailed_meta[:disks] << {
  673. :name => 'mapper/' + device["DM_NAME"],
  674. :model => binfo[:model],
  675. :size => (binfo[:size].to_i * block_size),
  676. :disk => dname,
  677. :extra => _disk_id_by_name(dname),
  678. :removable => 0,
  679. :paths => device["DM_BLKDEVS_USED"].map{|name| _disk_path_by_name(name)}.join(', ')
  680. }
  681. end
  682. end
  683. @logger.debug("Detailed meta disks: #{detailed_meta[:disks].inspect}")
  684. end
  685. rescue Exception => e
  686. @logger.error("Error '#{e.message}' in gathering disks metadata: #{e.backtrace}")
  687. end
  688. detailed_meta
  689. end
  690. def _get_interface_driver_info(int)
  691. cmd_driver = Rethtool::EthtoolCmdDriver.new
  692. cmd_driver.cmd = Rethtool::ETHTOOL_CMD_GDRVINFO
  693. data = Rethtool.ioctl(int, cmd_driver)
  694. [data.driver.pack('c*').delete("\000"), data.bus_info.pack('c*').delete("\000")]
  695. rescue => e
  696. @logger.error("Error '#{e.message}' while obtaining #{int} driver info.")
  697. [nil, nil]
  698. end
  699. def _multipath_devices
  700. @logger.debug("Waiting for udev to complete evaluating rules")
  701. `udevadm settle`
  702. dmsetup_command = "/sbin/dmsetup info -c --nameprefixes --noheadings -o blkdevname,subsystem,blkdevs_used,name,uuid"
  703. @logger.debug("Running command: #{dmsetup_command}")
  704. dmsetup = `#{dmsetup_command}`
  705. # Example output:
  706. # DM_BLKDEVNAME='dm-0':DM_SUBSYSTEM='mpath':DM_BLKDEVS_USED='sdb,sda':DM_NAME='31234567890abcdef':DM_UUID='mpath-31234567890abcdef'
  707. # DM_BLKDEVNAME='dm-1':DM_SUBSYSTEM='mpath':DM_BLKDEVS_USED='sdc,sdd':DM_NAME='92344567890abcdef':DM_UUID='mpath-92344567890abcdef'
  708. mpath_devices = {}
  709. mapping = []
  710. unless dmsetup.include?("No devices found")
  711. dmsetup.lines.each do |line|
  712. device = {}
  713. line.split(/:/).each do |key_value|
  714. k, v = key_value.split('=')
  715. device[k] = v.strip().gsub(/'/, '')
  716. end
  717. next unless device["DM_SUBSYSTEM"] == 'mpath'
  718. device["DM_BLKDEVS_USED"] = device["DM_BLKDEVS_USED"].split(',')
  719. device["DM_BLKDEVS_USED"].each do | name |
  720. mapping << name
  721. end
  722. mpath_devices[device["DM_BLKDEVNAME"]] = device
  723. end
  724. mapping.uniq!
  725. end
  726. @logger.debug("Multipath devices: #{mpath_devices}")
  727. @logger.debug("Physical devices that are used in multipath devices: #{mapping}")
  728. [mpath_devices, mapping]
  729. rescue => e
  730. @logger.error("Error '#{e.message}' while scanning for multipath devices.")
  731. [{}, []]
  732. end
  733. def _get_pci_vendor_id(devname)
  734. Timeout::timeout(30) do
  735. udevadm_walk = {}
  736. devpath = nil
  737. # expected output of `udevadm info --attribute-walk --name=#{devname}`:
  738. #
  739. # Udevadm info starts with the device specified by the devpath and then
  740. # walks up the chain of parent devices. It prints for every device
  741. # found, all possible attributes in the udev rules key format.
  742. # A rule to match, can be composed by the attributes of the device
  743. # and the attributes from one single parent device.
  744. #
  745. # looking at device '/devices/pci0000:00/0000:00:1e.0/0000:0d:02.0/8:0:0:1/block/sdc':
  746. # KERNEL=="sdc"
  747. # SUBSYSTEM=="block"
  748. # DRIVER==""
  749. # ATTR{ro}=="0"
  750. # ATTR{size}=="30881792"
  751. # ATTR{removable}=="1"
  752. #
  753. # looking at parent device '/devices/pci0000:00/0000:00:1e.0/0000:0d:02.0':
  754. # Disk adapter plugged into PCIe slot, we need it's PCI vendor ID
  755. # KERNELS=="0000:0d:02.0"
  756. # SUBSYSTEMS=="pci"
  757. # DRIVERS==""
  758. # ATTRS{device}=="0x9030"
  759. # ATTRS{vendor}=="0x10b5"
  760. #
  761. # looking at parent device '/devices/pci0000:00/0000:00:1e.0':
  762. # PCIe slot reported as a PCI bridge device, it's PCI vendor ID is NOT what we need
  763. # KERNELS=="0000:00:1e.0"
  764. # SUBSYSTEMS=="pci"
  765. # DRIVERS==""
  766. # ATTRS{device}=="0x244e"
  767. # ATTRS{vendor}=="0x8086"
  768. #
  769. # looking at parent device '/devices/pci0000:00':
  770. # KERNELS=="pci0000:00"
  771. # SUBSYSTEMS==""
  772. # DRIVERS==""
  773. `udevadm info --attribute-walk --name=#{devname}`.split("\n").each do |line|
  774. line.strip!
  775. next unless line.start_with?('looking', 'KERNEL', 'SUBSYSTEM', 'DRIVER', 'ATTR')
  776. if line.start_with?('looking')
  777. devpath = line.split("'")[1]
  778. udevadm_walk[devpath] = {}
  779. else
  780. key, value = line.split("==").each { |a| a.strip! }
  781. udevadm_walk[devpath][key] = value.gsub(/(^")|("$)/, '')
  782. end
  783. end
  784. # We need a vendor ID of a disk adapter rather than vendor ID of the PCIe slot where it's plugged into.
  785. # Therefore we should pick the device with SUBSYSTEMS==pci having the longest devpath.
  786. # For the example given above, vendor ID should be found as '0x10b5'.
  787. # Next ID of '0x8086' belongs to PCIe slot to which PCIe RAID disk adapter is inserted.
  788. devpath = Hash[udevadm_walk.select { |k, v| v['SUBSYSTEMS'] == 'pci' }].keys.max
  789. udevadm_walk[devpath]['ATTRS{vendor}']
  790. end
  791. rescue => e
  792. @logger.error("Error '#{e.message}' in obtaining PCI vendor ID: #{e.backtrace}")
  793. end
  794. def _disk_id_by_name(name)
  795. dn = "/dev/disk/by-id"
  796. basepath = Dir["#{dn}/**/*?"].select{|f| File.symlink?(f) and /\/#{name}$/.match(File.readlink(f))}
  797. basepath.map{|p| p.split("/")[2..-1].join("/")}
  798. end
  799. def _disk_path_by_name(name)
  800. dn = "/dev/disk/by-path"
  801. basepath = Dir["#{dn}/**/*?"].find{|f| File.symlink?(f) and /\/#{name}$/.match(File.readlink(f))}
  802. basepath.split("/")[2..-1].join("/") if basepath
  803. end
  804. # Sample mdadm --detail /dev/md127 output:
  805. # /dev/md127:
  806. # Version : 1.2
  807. # Creation Time : Thu Oct 29 16:12:00 2015
  808. # Raid Level : raid1
  809. # Array Size : 1048000 (1023.61 MiB 1073.15 MB)
  810. # Used Dev Size : 1048000 (1023.61 MiB 1073.15 MB)
  811. # Raid Devices : 2
  812. # Total Devices : 2
  813. # Persistence : Superblock is persistent
  814. #
  815. # Update Time : Sun Nov 1 00:57:31 2015
  816. # State : clean
  817. # Active Devices : 2
  818. # Working Devices : 2
  819. # Failed Devices : 0
  820. # Spare Devices : 0
  821. #
  822. # Name : agordeev:123 (local to host agordeev)
  823. # UUID : 7aa70afc:742a9fa6:45f9f5a1:25a2585f
  824. # Events : 20
  825. #
  826. # Number Major Minor RaidDevice State
  827. # 0 252 2 0 active sync /dev/dm-2
  828. # 1 252 3 1 active sync /dev/dm-3
  829. #
  830. def _parse_md(data)
  831. md = {}
  832. begin
  833. description, _, components = data.split(/Number\s+Major\s+Minor\s+RaidDevice\s+(State\s+)?/m)
  834. line_patterns = ['Version', 'Raid Level', 'Raid Devices', 'Active Devices',
  835. 'Spare Devices', 'Failed Devices', 'State', 'UUID',
  836. 'Container']
  837. for line in (description.split("\n")[1..-1] rescue [])
  838. line.strip!
  839. next if line == ""
  840. line_patterns.each { |pattern| md[pattern] = line.split(" : ").last if line.start_with?(pattern) }
  841. end
  842. md['devices'] = []
  843. for line in (components.split("\n") rescue [])
  844. line.strip!
  845. next if line == ""
  846. md['devices'] << line.split().last
  847. end
  848. rescue Exception => e
  849. @logger.error("Error '#{e.message}' in parsing MD: #{e.backtrace}")
  850. end
  851. md
  852. end
  853. def _find_fake_raid_mds()
  854. mds = []
  855. devices = []
  856. begin
  857. Dir["/sys/block/*"].each do |block_device_dir|
  858. basename_dir = File.basename(block_device_dir)
  859. devname = basename_dir.gsub(/!/, '/')
  860. next unless devname.start_with?('md')
  861. md_data = _parse_md(`mdadm --detail /dev/#{devname}`)
  862. next if md_data['Raid Level'] == 'container'
  863. if md_data.has_key?("Container")
  864. devices.concat((md_data['devices'] or []))
  865. mds << devname
  866. end
  867. end
  868. rescue Exception => e
  869. @logger.error("Error '#{e.message}' in finding fake raid MDs: #{e.backtrace}")
  870. end
  871. return mds, devices
  872. end
  873. def physical_data_storage_devices
  874. @blocks ||= []
  875. return @blocks unless @blocks.empty?
  876. @logger.debug("Trying to get list of physical devices")
  877. raise "Path /sys/block does not exist" unless File.exists?("/sys/block")
  878. mds, devices = _find_fake_raid_mds()
  879. @logger.debug("Found fake RAIDs: #{mds}")
  880. @logger.debug("Found components of fake RAIDs: #{devices}")
  881. Dir["/sys/block/*"].each do |block_device_dir|
  882. basename_dir = File.basename(block_device_dir)
  883. # Entries in /sys/block for cciss look like cciss!c0d1 while
  884. # the entries in /dev look like /dev/cciss/c0d1. udevadm uses
  885. # the entry in /dev so we need to replace the ! to get a valid
  886. # device name.
  887. devname = basename_dir.gsub(/!/, '/')
  888. # Skipping MD if it's a container. Also skipping underlying
  889. # devices from which that container is composed.
  890. next if devices.include?("/dev/#{devname}")
  891. next if devname.start_with?('md') and not mds.include?(devname)
  892. @logger.debug("Getting udev properties for device: #{devname}")
  893. properties = `udevadm info --query=property --export --name=#{devname}`.split("\n").inject({}) do |result, raw_propety|
  894. key, value = raw_propety.split(/\=/)
  895. result.update(key.strip => value.strip.chomp("'").reverse.chomp("'").reverse)
  896. end
  897. @logger.debug("Device #{devname} udev properties: #{properties.inspect}")
  898. @logger.debug("Filtering out devices that are used in multipath devices: 'DM_MULTIPATH_DEVICE_PATH' = '1'")
  899. next if properties['DM_MULTIPATH_DEVICE_PATH'] == '1'
  900. @logger.debug("Trying to find out if device #{devname} is removable or not")
  901. if File.exists?("/sys/block/#{basename_dir}/removable")
  902. removable = File.open("/sys/block/#{basename_dir}/removable"){ |f| f.read_nonblock(1024).strip }
  903. end
  904. @logger.debug("Device #{devname} removable parameter: #{removable.inspect}")
  905. if STORAGE_CODES.include?(properties['MAJOR'].to_i)
  906. # Exclude LVM volumes (in CentOS - 253, in Ubuntu - 252) using additional check
  907. # Exclude any storage device connected through USB by the default
  908. @logger.debug("Trying to exclude LVM volumes and USB devices")
  909. next if properties['DEVPATH'].include?('virtual/block/dm') ||
  910. (properties['ID_BUS'] == 'usb' &&
  911. !@settings.has_key?("report_usb_block_devices"))
  912. @logger.debug("Device #{devname} seems to be appropriate")
  913. @blocks << {
  914. :name => basename_dir,
  915. :disk => _disk_path_by_name(devname) || devname,
  916. :extra => _disk_id_by_name(devname) || [],
  917. :removable => removable,
  918. }
  919. end
  920. end
  921. @logger.debug("Final list of physical devices is: #{@blocks.inspect}")
  922. @blocks
  923. end
  924. def get_opt_io
  925. return @opt_io_res if defined?(@opt_io_res)
  926. @opt_io_res = {}
  927. # example output:
  928. # sda 4096 0
  929. # sdb 512 2048
  930. output = `lsblk --nodeps --bytes --noheadings --output NAME,MIN-IO,OPT-IO`
  931. output.split("\n").each do |line|
  932. name, min_io, opt_io = line.split()
  933. @opt_io_res[name] = opt_io.to_i != 0 ? opt_io.to_i : min_io.to_i
  934. end
  935. @opt_io_res
  936. rescue => e
  937. @logger.error("Error '#{e.message}' in obtaining optimal io size: #{e.backtrace}")
  938. @opt_io_res ||= {}
  939. end
  940. def _is_virtualbox
  941. @facter['productname'] == "VirtualBox"
  942. end
  943. def _is_virtual
  944. @facter[:is_virtual]
  945. end
  946. # JFYI: if /QEMU/ doesn't matched in /proc/cpuinfo
  947. # ohai[:virtualization] will return empty hash on kvm systems
  948. # So, this code have exactly same behavior.
  949. # But in my opinion here should be returned real value.
  950. def _manufacturer
  951. if _is_virtualbox
  952. @facter['productname']
  953. elsif (@facter.fetch('manufacturer', '').upcase != 'QEMU' && @facter['is_virtual'])
  954. @facter['virtual']
  955. else
  956. @facter.fetch('manufacturer', '')
  957. end
  958. end
  959. def _product_name
  960. unless _is_virtual
  961. @facter['productname']
  962. end
  963. end
  964. def _serial
  965. @facter['serialnumber']
  966. end
  967. # Returns unique identifier of machine
  968. # * for kvm virtual node will contain virsh UUID
  969. # * for physical HW that would be unique chassis id (from BIOS settings)
  970. # * for other hypervizors - not tested
  971. def uuid
  972. node_uuid = @facter['uuid']
  973. node_uuid && node_uuid.strip
  974. end
  975. def _system_info
  976. {
  977. :manufacturer => _manufacturer,
  978. :serial => _serial,
  979. :uuid => uuid,
  980. :runtime_uuid => @settings['runtime_uuid'],
  981. :product => _product_name,
  982. :family => (_get_dmidecode_system_info[:system][:family].strip rescue nil),
  983. :version => _get_dmi_info[:chassis_version],
  984. :fqdn => (@facter['fqdn'].strip rescue @facter['hostname'].strip rescue nil),
  985. }.delete_if { |key, value| value.nil? or value.empty? or value == "Not Specified" }
  986. end
  987. def _size(size, unit)
  988. case unit
  989. when /^kb$/i
  990. size * 1024
  991. when /^mb$/i
  992. size * 1048576
  993. when /^gb$/i
  994. size * 1073741824
  995. end
  996. end
  997. def _dmi_memory
  998. dmi = `/usr/sbin/dmidecode`
  999. info = {:devices => [], :total => 0, :maximum_capacity => 0, :slots => 0}
  1000. return nil if $?.to_i != 0
  1001. dmi.split(/\n\n/).each do |group|
  1002. if /^Physical Memory Array$/.match(group)
  1003. if /^\s*Maximum Capacity:\s+(\d+)\s+(mb|gb|kb)/i.match(group)
  1004. info[:maximum_capacity] += _size($1.to_i, $2)
  1005. end
  1006. if /^\s*Number Of Devices:\s+(\d+)/i.match(group)
  1007. info[:slots] += $1.to_i
  1008. end
  1009. elsif /^Memory Device$/.match(group)
  1010. device_info = {}
  1011. if /^\s*Size:\s+(\d+)\s+(mb|gb|kb)/i.match(group)
  1012. size = _size($1.to_i, $2)
  1013. device_info[:size] = size
  1014. info[:total] += size
  1015. else
  1016. next
  1017. end
  1018. if /^\s*Speed:\s+(\d+)\s+MHz/i.match(group)
  1019. device_info[:frequency] = $1.to_i
  1020. end
  1021. if /^\s*Type:\s+(.*?)$/i.match(group)
  1022. device_info[:type] = $1
  1023. end
  1024. #if /^\s*Locator:\s+(.*?)$/i.match(group)
  1025. # device_info[:locator] = $1
  1026. #end
  1027. info[:devices].push(device_info)
  1028. end
  1029. end
  1030. if info[:total] == 0
  1031. nil
  1032. else
  1033. info
  1034. end
  1035. end
  1036. def _facter_memory
  1037. info = {}
  1038. size = @facter['memorysize'].gsub(/(kb|mb|gb)$/i, "").to_i rescue (return nil)
  1039. info[:total] = _size(size, $1)
  1040. info
  1041. end
  1042. def _get_ip_mac_pair_for(local_addr)
  1043. @network[:interfaces].each do |int, intinfo|
  1044. next unless intinfo.has_key?(:addresses)
  1045. intinfo[:addresses].each do |k, v|
  1046. # Here we need to check family because IPAddr.new with bad
  1047. # data works very slow on some environments
  1048. # https://bugs.launchpad.net/fuel/+bug/1284571
  1049. if v[:family] == 'inet' && !(IPAddr.new(k) rescue nil).nil?
  1050. net = IPAddr.new("#{k}/#{v[:netmask]}")
  1051. if net.include? local_addr
  1052. mac = intinfo[:addresses].find { |_, info| info[:family] == 'lladdr' }[0]
  1053. return {:ip => k, :mac => mac}
  1054. end
  1055. end
  1056. end
  1057. end
  1058. {}
  1059. end
  1060. def _master_ip_and_mac_for_multirack
  1061. rv = {}
  1062. if File.exist?('/etc/astute.yaml')
  1063. conf = YAML::load_file('/etc/astute.yaml')
  1064. return {} unless conf.is_a?(Hash)
  1065. e_point_name = conf.fetch('network_scheme', {}).fetch('roles', {}).fetch('admin/pxe', nil)
  1066. e_point_ips = conf.fetch('network_scheme', {}).fetch('endpoints', {}).fetch(e_point_name, {}).fetch('IP', [])
  1067. e_point_ips.each do |admin_ip|
  1068. rv = _get_ip_mac_pair_for(admin_ip)
  1069. break unless rv.empty?
  1070. end
  1071. end
  1072. return rv
  1073. end
  1074. def _master_ip_and_mac
  1075. rv = _get_ip_mac_pair_for(@api_ip)
  1076. return (rv.empty? ? _master_ip_and_mac_for_multirack : rv)
  1077. end
  1078. def _data
  1079. res = {
  1080. :mac => (@network[:mac] rescue nil),
  1081. :ip => (@facter['ipaddress'] rescue nil),
  1082. :os_platform => (@facter['operatingsystem'].downcase rescue nil)
  1083. }
  1084. begin
  1085. detailed_data = _detailed
  1086. master_data=_master_ip_and_mac
  1087. res.merge!({
  1088. :ip => (( master_data[:ip] or @facter['ipaddress']) rescue nil),
  1089. :mac => (( master_data[:mac] or @network[:mac]) rescue nil),
  1090. :manufacturer => _manufacturer,
  1091. :platform_name => _product_name,
  1092. :meta => detailed_data
  1093. })
  1094. rescue Exception => e
  1095. @logger.error("Error '#{e.message}' in metadata calculation: #{e.backtrace}")
  1096. end
  1097. res[:status] = @node_state if @node_state
  1098. res[:is_agent] = true
  1099. res[:agent_checksum] = createsig(res)
  1100. res
  1101. end
  1102. def traverse(item, &block)
  1103. yield item
  1104. if item.is_a?(Hash)
  1105. item.each { |k,v| traverse(v, &block) }
  1106. elsif item.is_a?(Array)
  1107. item.each { |elem| traverse(elem, &block) }
  1108. end
  1109. end
  1110. #todo: move all quirks here
  1111. def fixup(data)
  1112. traverse(data) do |item|
  1113. # size for CPU means current clock frequency which constantly changes
  1114. item.delete(:size) if item.is_a?(Hash) and item.fetch(:class, nil) == 'processor'
  1115. end
  1116. end
  1117. def _get_pci_dev_list
  1118. return {} if `cat /etc/nailgun_systemtype`.chomp != 'bootstrap'
  1119. lshw_timeout = @settings['lshw_timeout'] || 60
  1120. Timeout::timeout(lshw_timeout) do
  1121. lshw_path = `which lshw`.chomp
  1122. if $?.success?
  1123. data = `#{lshw_path} -json -sanitize`
  1124. return fixup(JSON.parse(data)) if $?.success?
  1125. @logger.warn("Can't get data from lshw. Reason: lshw exited with status #{$?.exitstatus}")
  1126. else
  1127. @logger.warn("Can't find lshw. Reason: 'which lshw' returned exit status #{$?.exitstatus}")
  1128. end
  1129. end
  1130. {}
  1131. rescue => e
  1132. @logger.warn("Can't get data from lshw. Reason: #{e.message}")
  1133. {}
  1134. end
  1135. def get_numa_topology
  1136. # Output EXAMPLE:
  1137. # <distances nbobjs="2" relative_depth="1" latency_base="10.000000">
  1138. # <latency value="1.000000"/>
  1139. # <latency value="2.100000"/>
  1140. # <latency value="2.100000"/>
  1141. # <latency value="1.000000"/>
  1142. # </distances>
  1143. # <object type="NUMANode" os_index="0" cpuset="0x3ff003ff" complete_cpuset="0x3ff003ff" online_cpuset="0x3ff003ff" allowed_cpuset="0x3ff003ff" nodeset="0x00000001" complete_nodeset="0x00000001" allowed_nodeset="0x00000001" local_memory="67452473344">
  1144. # <page_type size="4096" count="14370737"/>
  1145. # <page_type size="1073741824" count="8"/>
  1146. # ...
  1147. # <object type="Bridge" os_index="0" bridge_type="0-1" depth="0" bridge_pci="0000:[00-07]">
  1148. # <object type="Bridge" os_index="51" name="Intel Corporation Xeon E7 v3/Xeon E5 v3/Core i7 PCI Express Root Port 3" bridge_type="1-1" depth="1" bridge_pci="0000:[04-04]" pci_busid="0000:00:03.3" pci_type="0604 [8086:2f0b] [0000:0000] 02" pci_link_speed="2.000000">
  1149. # <info name="PCIVendor" value="Intel Corporation"/>
  1150. # <info name="PCIDevice" value="Xeon E7 v3/Xeon E5 v3/Core i7 PCI Express Root Port 3"/>
  1151. # <object type="PCIDev" os_index="16384" name="Intel Corporation I350 Gigabit Network Connection" pci_busid="0000:04:00.0" pci_type="0200 [8086:1521] [15d9:1521] 01" pci_link_speed="2.000000">
  1152. # <info name="PCIVendor" value="Intel Corporation"/>
  1153. # <info name="PCIDevice" value="I350 Gigabit Network Connection"/>
  1154. # <object type="OSDev" name="em1" osdev_type="2">
  1155. # <info name="Address" value="0c:c4:7a:6d:06:c6"/>
  1156. doc = Document.new `lstopo --no-caches --of xml`
  1157. topology = {:numa_nodes => [], :supported_hugepages => supported_hugepages, :distances => [["1.0"]]}
  1158. doc.elements.each('//distances/') do |dist|
  1159. topology[:distances] = dist.elements.collect{|v| v.attributes['value']}
  1160. .each_slice(dist.attributes['nbobjs'].to_i).to_a
  1161. end
  1162. numa_node = "//object[@type='NUMANode']"
  1163. element = doc.elements["//object[@type='NUMANode']"] ? numa_node : "//object[@type='Machine']"
  1164. doc.elements.each(element) do |numa|
  1165. struct = {:id=> nil, :cpus => [], :memory => nil, :pcidevs => []}
  1166. struct[:id] = numa.attributes['os_index'].to_i
  1167. struct[:memory] = numa.attributes['local_memory'].to_i
  1168. numa.elements.each("#{numa.xpath}//[@type='PU']") do |pu|
  1169. struct[:cpus] << pu.attributes['os_index'].to_i
  1170. end
  1171. numa.elements.each("#{numa.xpath}//[@type='PCIDev']") do |pcidev|
  1172. struct[:pcidevs] << pcidev.attributes['pci_busid']
  1173. end
  1174. topology[:numa_nodes] << struct
  1175. end
  1176. topology
  1177. rescue => e
  1178. @logger.error "Something went wrong with parsing lstopo: #{e.backtrace}"
  1179. nil
  1180. end
  1181. def supported_hugepages
  1182. return [2048, 1048576] if _get_detailed_cpuinfo['0'][:flags].include?('pdpe1gb')
  1183. return [2048] if _get_detailed_cpuinfo['0'][:flags].include?('pse')
  1184. []
  1185. end
  1186. def _get_lspci_info(device)
  1187. lspci_path = `which lspci`.chomp
  1188. if $?.success?
  1189. data = `#{lspci_path} -vvv -s #{device}`
  1190. if $?.success?
  1191. return data
  1192. else
  1193. @logger.warn("Can't get data from lspci. Reason: lspci exited with status #{$?.exitstatus}")
  1194. ""
  1195. end
  1196. else
  1197. @logger.warn("Can't find lspci. Reason: 'which lspci' returned exit status #{$?.exitstatus}")
  1198. ""
  1199. end
  1200. rescue => e
  1201. @logger.warn("Can't get data from lspci for #{device} slot. Reason: #{e.message}")
  1202. ""
  1203. end
  1204. def update_state
  1205. @node_state = nil
  1206. if File.exist?("/etc/nailgun_systemtype")
  1207. fl = File.open("/etc/nailgun_systemtype", "r")
  1208. system_type = fl.readline.rstrip
  1209. @node_state = "discover" if system_type == "bootstrap"
  1210. end
  1211. end
  1212. def print
  1213. s = _data.to_json
  1214. @logger.info("Data collected by nailgun-agent:")
  1215. @logger.info(s)
  1216. end
  1217. end
  1218. def write_data_to_file(logger, filename, data)
  1219. if File.exist?(filename)
  1220. File.open(filename, 'r') do |fo|
  1221. text = fo.read
  1222. end
  1223. else
  1224. text = ''
  1225. end
  1226. if text != data
  1227. begin
  1228. File.open(filename, 'w') do |fo|
  1229. fo.write(data)
  1230. end
  1231. logger.info("Wrote data to file '#{filename}'. Data: #{data}")
  1232. rescue Exception => e
  1233. logger.warning("Can't write data to file '#{filename}'. Reason: #{e.message}")
  1234. end
  1235. else
  1236. logger.info("File '#{filename}' is up to date.")
  1237. end
  1238. end
  1239. dry_run = false
  1240. OptionParser.new do |opts|
  1241. opts.banner = "Usage: nailgun-agent [options]"
  1242. opts.on("-d", "--dry-run", "Only print collected information, don't send it anywhere.") do |_d|
  1243. dry_run = true
  1244. end
  1245. end.parse!
  1246. logger = Logger.new(STDOUT)
  1247. if File.exist?('/etc/nailgun_uid')
  1248. logger.level = Logger::INFO
  1249. else
  1250. logger.level = Logger::DEBUG
  1251. end
  1252. # random sleep is here to prevent target nodes
  1253. # from reporting to master node all at once
  1254. unless dry_run
  1255. sleep_time = rand(30)
  1256. logger.debug("Sleep for #{sleep_time} seconds before sending request")
  1257. sleep(sleep_time)
  1258. end
  1259. agent = NodeAgent.new(logger, dry_run)
  1260. agent.update_state
  1261. if dry_run
  1262. agent.print
  1263. exit 0
  1264. end
  1265. begin
  1266. unless File.exist?('/etc/nailgun_uid')
  1267. resp = agent.post
  1268. # We must not log 409 as error, after node is provisioned there will be no
  1269. # /etc/nailgun_uid, it will be created after put request
  1270. if [409, 403].include? resp.status
  1271. resp = agent.put
  1272. end
  1273. else
  1274. resp = agent.put
  1275. # Handle case when node was removed, but nailgun_uid exist
  1276. if resp.status == 400
  1277. resp = agent.post
  1278. end
  1279. end
  1280. unless [201, 200].include? resp.status
  1281. logger.error resp.body
  1282. exit 1
  1283. end
  1284. new_id = JSON.parse(resp.body)['id']
  1285. write_data_to_file(logger, '/etc/nailgun_uid', new_id.to_s)
  1286. rescue => ex
  1287. # NOTE(mihgen): There is no need to retry - cron will do it for us
  1288. logger.error "#{ex.message}\n#{ex.backtrace}"
  1289. end