Advertisement
Guest User

Untitled

a guest
Dec 11th, 2013
110
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 15.60 KB | None | 0 0
  1. #!/usr/bin/env ruby
  2.  
  3. # Copyright 2013 Mirantis, Inc.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  6. # not use this file except in compliance with the License. You may obtain
  7. # a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  13. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  14. # License for the specific language governing permissions and limitations
  15. # under the License.
  16.  
  17. begin
  18. require 'rubygems'
  19. rescue LoadError
  20. end
  21. require 'ohai/system'
  22. require 'json'
  23. require 'httpclient'
  24. require 'logger'
  25. require 'optparse'
  26. require 'yaml'
  27. require 'ipaddr'
  28. require 'rethtool'
  29.  
  30. unless Process.euid == 0
  31. puts "You must be root"
  32. exit 1
  33. end
  34.  
  35. ENV['PATH'] = "/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin"
  36.  
  37. AGENT_CONFIG = "/etc/nailgun-agent/config.yaml"
  38.  
  39. class McollectiveConfig
  40. def initialize(logger)
  41. @logger = logger
  42. @configfile = '/etc/mcollective/server.cfg'
  43. end
  44.  
  45. def get_config_by_key(find_key)
  46. found_key = nil
  47. found_value = nil
  48. # This code is from mcollective's sources
  49. File.open(@configfile, "r").each do |line|
  50. # strip blank spaces, tabs etc off the end of all lines
  51. line.gsub!(/\s*$/, "")
  52. unless line =~ /^#|^$/
  53. if line =~ /(.+?)\s*=\s*(.+)/
  54. key = $1
  55. val = $2
  56. if key == find_key
  57. found_key = key
  58. found_value = val
  59. end
  60. end
  61. end
  62. end
  63.  
  64. found_value if found_key
  65. end
  66.  
  67. def replace_identity(new_id)
  68. # check if id complies reqs
  69. raise 'Identities can only match /\w\.\-/' unless new_id.to_s.match(/^[\w\.\-]+$/)
  70.  
  71. value_from_config = get_config_by_key('identity')
  72.  
  73. if value_from_config == new_id.to_s
  74. @logger.info "MCollective is up to date with identity = #{new_id}"
  75. else
  76. config = File.open(@configfile, "rb").read
  77. if value_from_config
  78. # Key found, but it has other value
  79. @logger.info "Replacing identity in mcollective server.cfg to new value = '#{new_id}'"
  80. config.gsub!(/^identity[ =].*$/, "identity = #{new_id}")
  81. File.open(@configfile, "w") { |f| f.write(config) }
  82. else # if key was not found
  83. config += "\nidentity = #{new_id}\n"
  84. @logger.info "Identity in mcollective server.cfg has not been found. Setting to '#{new_id}'"
  85. File.open(@configfile, "w") { |f| f.write(config) }
  86. end
  87. # Generally because generic init script for
  88. # mcollective is broken at least in Ubuntu and
  89. # altering restart in such way seems better
  90. # than shipping our own package.
  91. puts `/etc/init.d/mcollective stop; /etc/init.d/mcollective start`
  92. end
  93. end
  94. end
  95.  
  96.  
  97. class NodeAgent
  98. def initialize(logger, url=nil)
  99. @logger = logger
  100.  
  101. @api_default_address = "localhost"
  102. @api_default_port = "8000"
  103.  
  104. @api_url = url
  105.  
  106. if @api_url
  107. @api_url.chomp!('/')
  108. @api_ip = @api_url.match(/\bhttp:\/\/((\d{1,3}\.){3}\d{1,3})/)[1]
  109. else
  110. begin
  111. cmdline = ::File.read("/proc/cmdline")
  112. @api_ip = cmdline.match(/\burl=http:\/\/((\d{1,3}\.){3}\d{1,3})/)[1]
  113. @logger.info("Found admin node IP address in kernel cmdline: #{@api_ip}")
  114. rescue
  115. @logger.info("Can't get API url from /proc/cmdline. Will use localhost.")
  116. @api_ip = "127.0.0.1"
  117. end
  118. @api_url = "http://#{@api_ip}:#{@api_default_port}/api"
  119. end
  120.  
  121. @os = Ohai::System.new()
  122. @os.all_plugins
  123. end
  124.  
  125. def put
  126. headers = {"Content-Type" => "application/json"}
  127. @logger.debug("Trying to put host info into #{@api_url}")
  128. res = htclient.put("#{@api_url}/nodes/", [_data].to_json, headers)
  129. if res.status < 200 or res.status >= 300
  130. @logger.error("HTTP PUT failed: #{res.inspect}")
  131. end
  132. res
  133. end
  134.  
  135. def post
  136. headers = {"Content-Type" => "application/json"}
  137. @logger.debug("Trying to create host using #{@api_url}")
  138. res = htclient.post("#{@api_url}/nodes/", _data.to_json, headers)
  139. res
  140. end
  141.  
  142. def htclient
  143. client = HTTPClient.new
  144. client.connect_timeout = 10
  145. client.send_timeout = 10
  146. client.receive_timeout = 10 # (mihgen): Nailgun may hang for a while, but 10sec should be enough for him to respond
  147. client
  148. end
  149.  
  150. def _interfaces
  151. interfaces = @os[:network][:interfaces].inject([]) do |result, elm|
  152. result << { :name => elm[0], :addresses => elm[1]["addresses"] }
  153. end
  154. interfaces << { "default_interface" => @os["network"]["default_interface"] }
  155. interfaces << { "default_gateway" => @os["network"]["default_gateway"] }
  156. interfaces
  157. end
  158.  
  159. def _detailed
  160. detailed_meta = {
  161. :system => _system_info,
  162. :interfaces => [],
  163. :cpu => {
  164. :total => (@os[:cpu][:total].to_i rescue nil),
  165. :real => (@os[:cpu][:real].to_i rescue nil),
  166. :spec => [],
  167. },
  168. :disks => [],
  169. :memory => (_dmi_memory or _ohai_memory),
  170. }
  171.  
  172. begin
  173. (@os[:network][:interfaces] or {} rescue {}).each do |int, intinfo|
  174. # Send info about physical interfaces only
  175. next if intinfo[:type] != "eth"
  176. # Exception: eth0.0(example) have "type" => "eth" but it is not physical interface
  177. next if int =~ /\d+\.\d+$/
  178. int_meta = {:name => int}
  179. (intinfo[:addresses] or {} rescue {}).each do |addr, addrinfo|
  180. if (addrinfo[:family] rescue nil) =~ /lladdr/
  181. int_meta[:mac] = addr
  182. begin
  183. int_info = Rethtool::InterfaceSettings.new(int)
  184. int_meta[:max_speed] = int_info.best_mode.speed
  185. if int_info.current_mode.speed == :unknown
  186. int_meta[:current_speed] = nil
  187. else
  188. int_meta[:current_speed] = int_info.current_mode.speed
  189. end
  190. rescue
  191. int_meta[:current_speed] = nil
  192. end
  193. elsif (addrinfo[:family] rescue nil) =~ /^inet$/
  194. int_meta[:ip] = addr
  195. int_meta[:netmask] = addrinfo[:netmask] if addrinfo[:netmask]
  196. end
  197. end
  198. detailed_meta[:interfaces] << int_meta
  199. end
  200. rescue Exception => e
  201. @logger.error("Error '#{e.message}' in gathering interfaces metadata: #{e.backtrace}")
  202. end
  203.  
  204. begin
  205. (@os[:cpu] or {} rescue {}).each do |cpu, cpuinfo|
  206. if cpu =~ /^[\d]+/ and cpuinfo
  207. frequency = cpuinfo[:mhz].to_i rescue nil
  208. begin
  209. # ohai returns current frequency, try to get max if possible
  210. max_frequency = `cat /sys/devices/system/cpu/cpu#{cpu}/cpufreq/cpuinfo_max_freq 2>/dev/null`.to_i / 1000
  211. frequency = max_frequency if max_frequency > 0
  212. rescue
  213. end
  214. detailed_meta[:cpu][:spec] << {
  215. :frequency => frequency,
  216. :model => (cpuinfo[:model_name].gsub(/ +/, " ") rescue nil)
  217. }
  218. end
  219. end
  220. rescue Exception => e
  221. @logger.error("Error '#{e.message}' in gathering cpu metadata: #{e.backtrace}")
  222. end
  223.  
  224. begin
  225. (@os[:block_device] or {} rescue {}).each do |bname, binfo|
  226. if physical_data_storage_devices.include?(bname) && binfo
  227. dname = bname.gsub(/!/, '/')
  228. # 512 bytes is the size of one sector by default
  229. block_size = 512
  230. fn = "/sys/block/#{bname}/queue/logical_block_size"
  231. block_size = File.read(fn).to_i if File.exist? fn
  232. block_size = 512 if block_size == 0
  233. detailed_meta[:disks] << {
  234. :name => dname,
  235. :model => binfo[:model],
  236. :size => (binfo[:size].to_i * block_size),
  237. :disk => _disk_path_by_name(dname) || dname
  238. }
  239. end
  240. end
  241. rescue Exception => e
  242. @logger.error("Error '#{e.message}' in gathering disks metadata: #{e.backtrace}")
  243. end
  244.  
  245. detailed_meta
  246. end
  247.  
  248. def _disk_path_by_name(name)
  249. dn = "/dev/disk/by-path"
  250. basepath = Dir["#{dn}/**?"].find{|f| /\/#{name}$/.match(File.readlink(f))}
  251. basepath.split("/")[2..-1].join("/") if basepath
  252. end
  253.  
  254. def physical_data_storage_devices
  255. @blocks ||= []
  256. return @blocks unless @blocks.empty?
  257.  
  258. raise "Path /sys/block does not exist" unless File.exists?("/sys/block")
  259.  
  260. Dir["/sys/block/*"].each do |block_device_dir|
  261. basename_dir = File.basename(block_device_dir)
  262.  
  263. #properties = `udevadm info --query=property --export --name=#{basename_dir}`.inject({}) do |result, raw_propety|
  264. properties = `udevadm info --query=property --export --name=#{basename_dir.gsub(/!/, '/')}`.inject({}) do |result, raw_propety|
  265. key, value = raw_propety.split(/\=/)
  266. result.update(key.strip => value.strip.chomp("'").reverse.chomp("'").reverse)
  267. end
  268.  
  269. if File.exists?("/sys/block/#{basename_dir}/removable")
  270. removable = File.open("/sys/block/#{basename_dir}/removable"){ |f| f.read_nonblock(1024).strip }
  271. end
  272. # look at https://github.com/torvalds/linux/blob/master/Documentation/devices.txt
  273. # KVM virtio volumes has code 252 in CentOS, but 253 in Ubuntu
  274. if [3, 8, 104, 105, 106, 107, 108, 109, 110, 111, 202, 252, 253].include?(properties['MAJOR'].to_i) && removable =~ /^0$/
  275. # Exclude LVM volumes (in CentOS - 253, in Ubuntu - 252) using additional check
  276. @blocks << basename_dir unless properties['DEVPATH'].include?('virtual')
  277. end
  278. end
  279. @blocks
  280. end
  281.  
  282. def _is_virtualbox
  283. @os[:dmi][:system][:product_name] == "VirtualBox" rescue false
  284. end
  285.  
  286. def _is_virtual
  287. _is_virtualbox or @os[:virtualization][:role] == "guest" rescue false
  288. end
  289.  
  290. def _manufacturer
  291. if _is_virtualbox
  292. @os[:dmi][:system][:product_name] rescue nil
  293. elsif _is_virtual
  294. @os[:virtualization][:system].upcase.strip rescue nil
  295. else
  296. @os[:dmi][:system][:manufacturer].strip rescue nil
  297. end
  298. end
  299.  
  300. def _product_name
  301. unless _is_virtual
  302. @os[:dmi][:system][:product_name].strip rescue nil
  303. end
  304. end
  305.  
  306. def _serial
  307. @os[:dmi][:system][:serial_number].strip rescue nil
  308. end
  309.  
  310. def _system_info
  311. {
  312. :manufacturer => _manufacturer,
  313. :serial => _serial,
  314. :product => _product_name,
  315. :family => (@os[:dmi][:system][:family].strip rescue nil),
  316. :version => (@os[:dmi][:system][:version].strip rescue nil),
  317. :fqdn => (@os[:fqdn].strip rescue @os[:hostname].strip rescue nil),
  318. }.delete_if { |key, value| value.nil? or value.empty? or value == "Not Specified" }
  319. end
  320.  
  321. def _size(size, unit)
  322. case unit
  323. when /^kb$/i
  324. size * 1024
  325. when /^mb$/i
  326. size * 1048576
  327. when /^gb$/i
  328. size * 1073741824
  329. end
  330. end
  331.  
  332. def _dmi_memory
  333. dmi = `/usr/sbin/dmidecode`
  334. info = {:devices => [], :total => 0, :maximum_capacity => 0, :slots => 0}
  335. return nil if $?.to_i != 0
  336. dmi.split(/\n\n/).each do |group|
  337. if /^Physical Memory Array$/.match(group)
  338. if /^\s*Maximum Capacity:\s+(\d+)\s+(mb|gb|kb)/i.match(group)
  339. info[:maximum_capacity] += _size($1.to_i, $2)
  340. end
  341. if /^\s*Number Of Devices:\s+(\d+)/i.match(group)
  342. info[:slots] += $1.to_i
  343. end
  344. elsif /^Memory Device$/.match(group)
  345. device_info = {}
  346. if /^\s*Size:\s+(\d+)\s+(mb|gb|kb)/i.match(group)
  347. size = _size($1.to_i, $2)
  348. device_info[:size] = size
  349. info[:total] += size
  350. else
  351. next
  352. end
  353. if /^\s*Speed:\s+(\d+)\s+MHz/i.match(group)
  354. device_info[:frequency] = $1.to_i
  355. end
  356. if /^\s*Type:\s+(.*?)$/i.match(group)
  357. device_info[:type] = $1
  358. end
  359. #if /^\s*Locator:\s+(.*?)$/i.match(group)
  360. # device_info[:locator] = $1
  361. #end
  362. info[:devices].push(device_info)
  363. end
  364. end
  365. if info[:total] == 0
  366. nil
  367. else
  368. info
  369. end
  370. end
  371.  
  372. def _ohai_memory
  373. info = {}
  374. size = @os['memory']['total'].gsub(/(kb|mb|gb)$/i, "").to_i rescue (return nil)
  375. info[:total] = _size(size, $1)
  376. info
  377. end
  378.  
  379. def _master_ip detailed_data
  380. detailed_data.each do |k, v|
  381. if k.to_s =~ /^interfaces$/
  382. detailed_data[k].each do |i|
  383. begin
  384. net = IPAddr.new "#{i[:ip]}/#{i[:netmask]}"
  385. return i[:ip] if net.include? @api_ip
  386. rescue
  387. end
  388. end
  389. end
  390. end
  391. nil
  392. end
  393.  
  394. def _master_mac detailed_data
  395. detailed_data.each do |k, v|
  396. if k.to_s =~ /^interfaces$/
  397. detailed_data[k].each do |i|
  398. begin
  399. net = IPAddr.new "#{i[:ip]}/#{i[:netmask]}"
  400. return i[:mac] if net.include? @api_ip
  401. rescue
  402. end
  403. end
  404. end
  405. end
  406. nil
  407. end
  408.  
  409. def _data
  410. res = {
  411. :mac => (@os[:macaddress] rescue nil),
  412. :ip => (@os[:ipaddress] rescue nil),
  413. :os_platform => (@os[:platform] rescue nil)
  414. }
  415. begin
  416. detailed_data = _detailed
  417. res.merge!({
  418. :ip => ((_master_ip detailed_data or @os[:ipaddress]) rescue nil),
  419. :mac => ((_master_mac detailed_data or @os[:mac]) rescue nil),
  420. :manufacturer => _manufacturer,
  421. :platform_name => _product_name,
  422. :meta => detailed_data
  423. })
  424. rescue Exception => e
  425. @logger.error("Error '#{e.message}' in metadata calculation: #{e.backtrace}")
  426. end
  427.  
  428. res[:status] = @node_state if @node_state
  429. res[:is_agent] = true
  430. res
  431. end
  432.  
  433. def update_state
  434. @node_state = nil
  435. if File.exist?("/etc/nailgun_systemtype")
  436. fl = File.open("/etc/nailgun_systemtype", "r")
  437. system_type = fl.readline.rstrip
  438. @node_state = "discover" if system_type == "bootstrap"
  439. end
  440. end
  441. end
  442.  
  443. def write_data_to_file(logger, filename, data)
  444. if File.exist?(filename)
  445. File.open(filename, 'r') do |fo|
  446. text = fo.read
  447. end
  448. else
  449. text = ''
  450. end
  451.  
  452. if text != data
  453. begin
  454. File.open(filename, 'w') do |fo|
  455. fo.write(data)
  456. end
  457. logger.info("Wrote data to file '#{filename}'. Data: #{data}")
  458. rescue Exception => e
  459. logger.warning("Can't write data to file '#{filename}'. Reason: #{e.message}")
  460. end
  461. else
  462. logger.info("File '#{filename}' is up to date.")
  463. end
  464. end
  465.  
  466.  
  467. logger = Logger.new(STDOUT)
  468. logger.level = Logger::DEBUG
  469.  
  470. if File.exist?('/var/run/nodiscover')
  471. logger.info("Discover prevented by /var/run/nodiscover presence.")
  472. exit 1
  473. end
  474.  
  475. begin
  476. logger.info("Trying to load agent config #{AGENT_CONFIG}")
  477. url = YAML.load_file(AGENT_CONFIG)['url']
  478. logger.info("Obtained service url from config file: '#{url}'")
  479. rescue Exception => e
  480. logger.info("Could not get url from configuration file: #{e.message}, trying other ways..")
  481. end
  482.  
  483. sleep_time = rand(30)
  484. logger.debug("Sleep for #{sleep_time} seconds before sending request")
  485. sleep(sleep_time)
  486.  
  487. agent = NodeAgent.new(logger, url)
  488. agent.update_state
  489.  
  490. begin
  491. post_res = agent.post
  492. if post_res.status == 409
  493. put_res = agent.put
  494. new_id = JSON.parse(put_res.body)[0]['id']
  495. elsif post_res.status == 201
  496. new_id = JSON.parse(post_res.body)['id']
  497. else
  498. logger.error post_res.body
  499. exit 1
  500. end
  501. mc_config = McollectiveConfig.new(logger)
  502. mc_config.replace_identity(new_id)
  503. write_data_to_file(logger, '/etc/nailgun_uid', new_id.to_s)
  504. rescue => ex
  505. # NOTE(mihgen): There is no need to retry - cron will do it for us
  506. logger.error "#{ex.message}\n#{ex.backtrace}"
  507. end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement