#!/usr/bin/env ruby

#    Copyright 2013 Mirantis, Inc.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

begin
  require 'rubygems'
rescue LoadError
end
require 'facter'
require 'json'
require 'httpclient'
require 'logger'
require 'optparse'
require 'yaml'
require 'ipaddr'
require 'rethtool'
require 'digest'
require 'timeout'
require 'uri'
require 'optparse'
# TODO(vsharshov): replace below lines by this string after excluding Ruby 1.8
require 'pathname'
require 'rexml/document'
require 'socket'
include REXML

unless Process.euid == 0
  puts "You must be root"
  exit 1
end

ENV['PATH'] = "/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin"

AGENT_CONFIG = "/etc/nailgun-agent/config.yaml"

# look at https://github.com/torvalds/linux/blob/master/Documentation/devices.txt
# KVM virtio volumes has code 252 in CentOS, but 253 in Ubuntu
# Please also update the device codes here
# https://github.com/stackforge/fuel-astute/blob/master/mcagents/erase_node.rb#L81
# NVMe has code 259
STORAGE_CODES = [3, 8, 9, 65, 66, 67, 68, 69, 70, 71, 104, 105, 106, 107, 108, 109, 110, 111, 202, 252, 253, 259]
REMOVABLE_VENDORS = [
  "Adaptec", "IBM", "ServeRA",
]
# PCI vendor IDs for Adaptec
REMOVABLE_PCI_VENDORS = [
  "0x1044", "0x9004", "0x9005",
]
# Set default data structure for SR-IOV
DEFAULT_SRIOV = {
  "sriov_totalvfs" => 0,
  "available"      => false,
  "pci_id"         => ""
}

def digest(body)
  if body.is_a? Hash
    digest body.map { |k,v| [digest(k),digest(v)].join("=>") }.sort
  elsif body.is_a? Array
    body.map{ |v| digest v }.join('|')
  else
    [body.class.to_s, body.to_s].join(":")
  end
end

def createsig(body)
  Digest::SHA1.hexdigest( digest body )
end

class McollectiveConfig
  def initialize(logger)
    @logger = logger
    @configfile = '/etc/mcollective/server.cfg'
  end

  def get_config_by_key(find_key)
    found_key = nil
    found_value = nil
    # This code is from mcollective's sources
    File.open(@configfile, "r").each do |line|
      # strip blank spaces, tabs etc off the end of all lines
      line.gsub!(/\s*$/, "")
      unless line =~ /^#|^$/
        if line =~ /(.+?)\s*=\s*(.+)/
          key = $1
          val = $2
          if key == find_key
            found_key = key
            found_value = val
          end
        end
      end
    end

    found_value if found_key
  end

  def replace_identity(new_id)
    # check if id complies reqs
    raise 'Identities can only match /\w\.\-/' unless new_id.to_s.match(/^[\w\.\-]+$/)

    value_from_config = get_config_by_key('identity')

    if value_from_config == new_id.to_s
      @logger.info "MCollective is up to date with identity = #{new_id}"
    else
      config = File.open(@configfile, "rb").read
      if value_from_config
        # Key found, but it has other value
        @logger.info "Replacing identity in mcollective server.cfg to new value = '#{new_id}'"
        config.gsub!(/^identity[ =].*$/, "identity = #{new_id}")
        File.open(@configfile, "w") { |f| f.write(config) }
      else # if key was not found
        config += "\nidentity = #{new_id}\n"
        @logger.info "Identity in mcollective server.cfg has not been found. Setting to '#{new_id}'"
        File.open(@configfile, "w") { |f| f.write(config) }
      end
      puts `/usr/bin/fix-configs-on-startup`
    end
  end
end

class Offloading
  def initialize(name, sub)
    @name, @sub = name, sub
  end

  def to_json(options = {})
    {'name' => @name, 'state' => nil, 'sub' => @sub}.to_json()
  end

  def to_s
    "#{@name}: #{@sub}"
  end
end

class NodeAgent
  API_DEFAULT_ADDRESS = "localhost"
  API_DEFAULT_PORT = "8443"
  API_LEGACY_PORT = "8000"

  def initialize(logger, dry_run)
    @logger = logger
    @settings = get_settings()

    unless dry_run
      @api_ip = URI(@settings['url']).host || API_DEFAULT_ADDRESS

      scheme, api_port = get_scheme_and_port

      @api_url = "#{scheme}://#{@api_ip}:#{api_port}/api"
      @logger.info("API URL is #{@api_url}")
    end

    @facter = facter_system_info
    @network = _network
    @numa_topology = get_numa_topology
  end

  def get_scheme_and_port
    scheme, api_port = nil
    begin
      res = htclient.get("https://#{@api_ip}:#{API_DEFAULT_PORT}/")
      scheme, api_port = "https", API_DEFAULT_PORT
    rescue Errno::ECONNREFUSED
      @logger.warn("Connection Refused catched when trying connect to HTTPS port. Use plain HTTP")
      scheme, api_port = "http", API_LEGACY_PORT
    end
    return scheme, api_port
  end

  # transform string into Dictionary
  # For example, line: "initrd=/images/bootstrap/initramfs.img ksdevice=bootif lang="
  # will be transformed into: {"mco_user"=>"mcollective", "initrd"=>"/images/bootstrap/initramfs.img", "lang"=>nil}
  def string_to_hash(string)
    hash = Hash.new
    string.split(' ').each do |pair|
      key,value = pair.split(/=/, 2)
      hash[key] = value
    end
    hash
  end

  def get_settings
    agent_settings = YAML.load_file(AGENT_CONFIG) rescue {}
    cmdline_settings = string_to_hash(File.read("/proc/cmdline")) rescue {}
    agent_settings.merge(cmdline_settings)
  end

  def facter_system_info
    Facter.loadfacts
    Facter.to_hash
  end

  def put
    headers = {"Content-Type" => "application/json"}
    @logger.debug("Trying to put host info into #{@api_url}")
    res = htclient.put("#{@api_url}/nodes/agent/", _data.to_json, headers)
    @logger.debug("Response: status: #{res.status} body: #{res.body}")
    if res.status < 200 or res.status >= 400
      @logger.error("HTTP PUT failed: #{res.inspect}")
    end
    res
  end

  def post
    headers = {"Content-Type" => "application/json"}
    @logger.debug("Trying to create host using #{@api_url}")
    res = htclient.post("#{@api_url}/nodes/", _data.to_json, headers)
    @logger.debug("Response: status: #{res.status} body: #{res.body}")
    res
  end

  def htclient
    client = HTTPClient.new
    client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
    client.ssl_config.ssl_version = :TLSv1
    client.connect_timeout = 10
    client.send_timeout    = 10
    client.receive_timeout = 10  # (mihgen): Nailgun may hang for a while, but 10sec should be enough for him to respond
    client
  end

  def _get_iface_info(ifname)
    info = {}
    info[:name] = ifname
    info[:addresses] = {}
    if ifname =~ /^(\D+)(\d+.*)/ # enp0s11, enp0, eth0
      info[:type] = $1 # enp, enp, eth
      info[:number] = $2 # 0s11, 0, 0
    end
    data = `ip a s dev #{ifname}`
    #2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast master br-fw-admin state UP group default qlen 1000
    #    link/ether 64:de:13:ab:f4:1d brd ff:ff:ff:ff:ff:ff
    #    inet6 fe80::66de:13ff:feab:f41d/64 scope link
    #       valid_lft forever preferred_lft forever
    data.each_line do |line|
      case line.strip
        when /(\d+): #{ifname}: <([^>]*)> mtu (\d+) (.+) state (\w+)/
          info[:flags] = $2.split(',')
          info[:mtu] = $3
          info[:state] = $5.downcase
        when /link\/(\w+) ([\da-f\:]+) brd ([\da-f\:]+)/
          info[:addresses][$2.upcase] = { :family => "lladdr" } if $2 != "00:00:00:00:00:00"
          info[:encapsulation] = case $1
            when /loopback/i then 'Loopback'
            when /IPIP Tunnel/ then 'IPIP'
            when /Point-to-Point Protocol/ then 'PPP'
            when /IPv6-in-IPv4/ then '6to4'
            when /ether/ then'Ethernet'
            else nil
          end
        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+)?/
          info[:addresses][$1] = { :family => "inet", "prefixlen" => $3 ||32 }
          info[:addresses][$1][:scope] = ($6.eql?("host") ? "Node" : $6.capitalize)
          info[:addresses][$1][:netmask] = IPAddr.new("255.255.255.255").mask(($3 ||32).to_i).to_s
          info[:addresses][$1][:broadcast] = $5
        when /inet6 ([a-f0-9\:]+)\/(\d+) scope (\w+)/
          info[:addresses][$1] = { :family => "inet6", "prefixlen" => $2, "scope" => ($3.eql?("host") ? "Node" : $3.capitalize) }
      end
    end
    data = `ip -d link show dev #{ifname}`
    #2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast master br-fw-admin state UP mode DEFAULT group default qlen 1000
    #    link/ether 64:de:13:ab:f4:1d brd ff:ff:ff:ff:ff:ff promiscuity 1
    #    bridge_slave state forwarding priority 32 cost 4 hairpin off guard off root_block off fastleave off learning on flood on addrgenmode eui64
    data.each_line do |line|
      next if line =~ /^\d+/
      if line =~ /state (\w+)/
        info[:state] = $1.downcase
      end
      if line =~ /vlan id (\d+)/
        vid = $1
        info[:state][:vlan] = {}
        info[:state][:vlan][:id] = vid
      end
    end
    info
  end

  def _get_all_interfaces_info
    res = {}
    res[:interfaces] = {}
    Facter::Util::IP.get_interfaces().each do |ifname|
      res[:interfaces][ifname] = _get_iface_info(ifname)
    end
    %w[inet inet6].each do |family|
      #default via 10.109.3.1 dev br-ex
      #10.109.0.0/24 dev br-fw-admin  proto kernel  scope link  src 10.109.0.4
      #10.109.1.0/24 dev br-mgmt  proto kernel  scope link  src 10.109.1.3
      #10.109.2.0/24 dev br-storage  proto kernel  scope link  src 10.109.2.3
      #10.109.3.0/24 dev br-ex  proto kernel  scope link  src 10.109.3.3
      #240.0.0.0/30 dev hapr-host  proto kernel  scope link  src 240.0.0.1
      #240.0.0.4/30 dev vr-host-base  proto kernel  scope link  src 240.0.0.5
      `ip -f #{family} route show`.each_line do |line|
        if line =~ /^([^\s]+)\s(.*)$/
          rdest = $1
          rend = $2
          next if not rend =~ /\bdev\s+([^\s]+)\b/
          rint = $1
          next if not res[:interfaces].has_key?(rint)
          rent = {:destination => rdest, :family => family}
          %w[via scope metric proto src].each do |k|
            rent[k] = $1 if rend =~ /\b#{k}\s+([^\s]+)\b/
          end
          next if rent[:src] and not res[:interfaces][rint].has_key?(rent[:src])
          res[:interfaces][rint][:routes] = [] if not res[:interfaces][rint][:routes]
          res[:interfaces][rint][:routes] << rent
        end
      end
    end
    res
  end

  def _network
    iface = nil
    gw = nil
    route = `ip r list 0/0`.strip # 'default via 10.21.5.1 dev eth0'
    if route =~ /^default via ?([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+) dev ([a-zA-Z0-9_-]+)/
      gw = $1
      iface = $2
    end
    result = _get_all_interfaces_info
    if gw and iface
      result[:default_gateway] = gw
      result[:default_interface] = iface
      result[:mac] = @facter["macaddress_#{iface.gsub('-', '_')}"].upcase
    end
    result
  end

  def _get_detailed_cpuinfo
    real = {}
    info = {}
    info[:total] = 0
    curr_proc = nil
    File.open('/proc/cpuinfo').each do |l|
      case l.strip
        when /processor\s+:\s(.+)/
          info[:total] += 1
          curr_proc = $1
          info[curr_proc] = {}
        when /^cpu MHz\s+:\s(.+)/
          info[curr_proc][:mhz] = $1
        when /^physical id\s+:\s(.+)/
          info[curr_proc][:physical_id] = $1
          real[$1] = true
        when /^flags\s+:\s(.+)/
          info[curr_proc][:flags] = $1.split
        when /^address sizes\s+:\s(\d+) bits (\w+), (\d+) bits (\w+)/
          info[curr_proc][:address_sizes] = {}
          info[curr_proc][:address_sizes][$2.to_sym] = $1
          info[curr_proc][:address_sizes][$4.to_sym] = $3
        when /(.+)\s+:\s(.+)/
          value = $2
          key = $1.strip.downcase.gsub(/ /, '_')
          info[curr_proc][key.to_sym] = value
      end
    end
    info[:real] = real.keys.size
    info
  end

  def _get_blkdev_info
    info = {}
    if File.directory?('/sys/block/')
      begin
      Timeout::timeout(10) do
        Dir['/sys/block/*'].each do |blkdir|
          blkdev = File.basename(blkdir)
          info[blkdev] = Hash.new
          Dir.glob("/sys/block/#{blkdev}/{size,removable}").each do |g|
            File.open(g) { |f| info[blkdev][File.basename(g).to_sym] = f.read_nonblock(1024).strip }
          end
          Dir.glob("/sys/block/#{blkdev}/device/{model,rev,state,timeout,vendor}").each do |g|
            File.open(g) { |f| info[blkdev][File.basename(g).to_sym] = f.read_nonblock(1024).strip }
          end
        end # of blkdir
      end # of timeout
      rescue => e
        @logger.error("Error '#{e.message}' in gathering disks metadata: #{e.backtrace}")
      end
    end # File.directory
    info
  end

  def _get_dmi_info
    info = {}
    Dir['/sys/class/dmi/id/*'].each do |key|
      if File.file?(key)
        case File.basename(key)
          when /product_uuid/
            File.open(key) {|f| info[:uuid] = f.read_nonblock(1024).strip}
          when /sys_vendor/
            File.open(key) {|f| info[:sys_vendor] = info[:manufacturer] = f.read_nonblock(1024).strip}
          else
            File.open(key) {|f| info[File.basename(key).to_sym] = f.read_nonblock(1024).strip}
        end
      end
    end
    info
  end

  def _get_dmidecode_system_info
    info = {}
    info[:system] = {}
    # dmidecode -t system
    ## dmidecode 3.0
    #Getting SMBIOS data from sysfs.
    #SMBIOS 2.8 present.
    #
    #Handle 0x0100, DMI type 1, 27 bytes
    #System Information
    #  Manufacturer: QEMU
    #  Product Name: Standard PC (i440FX + PIIX, 1996)
    #  Version: pc-i440fx-2.4
    #  Serial Number: Not Specified
    #  UUID: 0C2041CE-94E4-453D-95DD-1682D5D8E487
    #  Wake-up Type: Power Switch
    #  SKU Number: Not Specified
    #  Family: Not Specified
    #
    #Handle 0x2000, DMI type 32, 11 bytes
    #System Boot Information
    #  Status: No errors detected
    `dmidecode -t system`.each_line do |l|
      case l.strip
        when /^SMBIOS (\S+)/
          info[:dmidecode_version] = $1.strip
        when /(.+):(.+)/
          k = $1
          v = $2
          info[:system][k.downcase.gsub(/([ -])/, '_').to_sym] = v.strip
      end
    end
    info
  end


  # transform input array into array of the objects
  # Example:
  # [{
  #     "state":null,
  #     "sub":[
  #         {
  #             "state":null,
  #             "sub":[],
  #             "name":"tx-checksum-ipv6"
  #         },
  #         ...........
  #     ],
  #     "name":"tx-checksumming"
  # },
  # {
  #     "state":null,
  #     "sub":[],
  #     "name":"generic-segmentation-offload"
  # },
  # .............
  # ]
  def _parse_offloading(offloading_arr)
    return [] if offloading_arr.empty?
    inner = []
    current = offloading_arr.shift()
    while offloading_arr.any? && offloading_arr.first().start_with?("\t") do
      inner << offloading_arr.shift()[1..-1]
    end
    res = _parse_offloading(offloading_arr)
    res << Offloading.new(current, _parse_offloading(inner))
  end

  # Gets information about SR-IOV for specified pci slot
  # using 'lspci' utility. Example of output to parse:
  #  ...
  #  Capabilities: [160 v1] Single Root I/O Virtualization (SR-IOV)
  #          IOVCap: Migration-, Interrupt Message Number: 000
  #          IOVCtl: Enable- Migration- Interrupt- MSE- ARIHierarchy-
  #          IOVSta: Migration-
  #          Initial VFs: 8, Total VFs: 8, Number of VFs: 0, Function Dependency Link: 01
  #          VF offset: 128, stride: 4, Device ID: 10ed
  #          Supported Page Size: 00000553, System Page Size: 00000001
  #          Region 0: Memory at 0000000090040000 (64-bit, prefetchable)
  #          Region 3: Memory at 0000000090060000 (64-bit, prefetchable)
  #          VF Migration: offset: 00000000, BIR: 0
  #  ...
  def sriov_info(int, int_bus_info)
    sriov = DEFAULT_SRIOV.dup
    lspci = _get_lspci_info(int_bus_info)
    if lspci.match(/.*Capabilities:.*SR-IOV.*/)
      sriov["sriov_totalvfs"] = lspci.scan(/\s+Total\s+VFs:\s+(\d+)/).last.first.to_i
      unless sriov["sriov_totalvfs"] == 0
        sriov["available"] = true
        sriov["sriov_totalvfs"] -= 1
      end
      vf_vendor = File.read("/sys/class/net/#{int}/device/vendor").chomp.gsub(/^0x/, '')
      vf_device = lspci.scan(/VF\s+.*\s+Device\s+ID:\s+([A-Fa-f0-9]+)/).last.first
      sriov["pci_id"] = "#{vf_vendor}:#{vf_device}"
    end
    sriov
  rescue
    DEFAULT_SRIOV
  end

  def nic_pci_id(bus_info)
    vendor = File.read("/sys/bus/pci/devices/#{bus_info}/vendor").chomp.gsub(/^0x/, '')
    device = File.read("/sys/bus/pci/devices/#{bus_info}/device").chomp.gsub(/^0x/, '')
    "#{vendor}:#{device}"
  rescue
    ""
  end

  def nic_numa_node(int_bus_info)
    numa_node = @numa_topology[:numa_nodes].select { |node|
      node[:pcidevs].include?(int_bus_info)
    }
    numa_node.first[:id].to_i
  rescue
    nil
  end

  def _is_in_bond(iface_name)
    File.exist? "/sys/class/net/#{iface_name}/master" rescue False
  end

  def _is_in_bridge(iface_name)
    File.exist? "/sys/class/net/#{iface_name}/brport" rescue False
  end

  def _get_iface_bridge_name(iface_name)
    File.basename(File.readlink("/sys/class/net/#{iface_name}/brport/bridge"))
  end

  def _get_iface_bond_name(iface_name)
    File.basename(File.readlink("/sys/class/net/#{iface_name}/master"))
  end

  def _get_interface_mac(iface_name, swaddr)
    # Get original mac excluding case with empty EEPROM data
    mac = "00:00:00:00:00:00"
    # It is a virtual device, lets read address file in sysfs
    if File.exist? "/sys/devices/virtual/net/#{iface_name}"
      File.open("/sys/devices/virtual/net/#{iface_name}/address") do
      |file|
        mac = file.readlines[0].chomp.downcase
      end
      return mac
    end
    # It is not a virtual device, lets ask ethtool first
    perm_addr = `ethtool -P #{iface_name}`
    begin
      re = eval '/(?<=Permanent address: )(?!00(:00){5}).+/'
    rescue SyntaxError
      re = perm_addr.match(/(00(:00){5})+/).nil? ? /[0-9a-f]+(:[0-9a-f]+){5}$/ : nil
    end
    mac = perm_addr.match(re)[0] rescue swaddr
    mac.downcase
  end

  def _get_parent_interface(iface_name)
    if _is_in_bond(iface_name)
      bond_name = _get_iface_bond_name(iface_name)
      if _is_in_bridge(bond_name)
        return _get_iface_bridge_name(bond_name)
      else
        return bond_name
      end
    elsif _is_in_bridge(iface_name)
      return _get_iface_bridge_name(iface_name)
    else
      iface_name
    end
  end

  def _get_max_queues(ifname)
    data = `ethtool -l #{ifname}`
    # Example of output to parse:
    # Channel parameters for ens4f0:
    #  Pre-set maximums:
    #  RX:		0
    #  TX:		0
    #  Other:		1
    #  Combined:	63
    # Current hardware settings:
    #  RX:		0
    #  TX:		0
    #  Other:		1
    #  Combined:	40
    return nil if $?.to_i != 0
    return data.scan(/Pre-set maximums:.*?Combined:\s*(\d+)/m).join.to_i
  rescue
    return nil
  end

  def _detailed
    detailed_meta = {
      :system => _system_info,
      :interfaces => [],
      :cpu => {
        :total => (@facter['processorcount'].to_i rescue nil),
        :real  => (@facter['physicalprocessorcount'].to_i rescue nil),
        :spec  => [],
      },
      :disks => [],
      :memory => (_dmi_memory or _facter_memory),
      :pci_devices => _get_pci_dev_list,
      :numa_topology => @numa_topology,
    }

    admin_mac = (_master_ip_and_mac[:mac] or @network[:mac]) rescue nil
    begin
      (@network[:interfaces] or {} rescue {}).each do |int, intinfo|
        #next if not intinfo.has_key?(:name)
        #int = intinfo[:name]
        # Send info about physical interfaces only

        next if int =~ /.*@.*/
        next if intinfo[:encapsulation] !~ /^Ethernet.*/
        # Avoid virtual devices like loopback, tunnels, bonding, vlans ...
        # TODO(vsharshov): replace below lines by this string after excluding Ruby 1.8
        # next if File.realpath("/sys/class/net/#{int}") =~ /virtual/
        next if Pathname.new("/sys/class/net/#{int}").realpath.to_s  =~ /virtual/
        # Avoid wireless
        next if File.exist?("/sys/class/net/#{int}/phy80211") ||
                File.exist?("/sys/class/net/#{int}/wireless")
        # Skip virtual functions
        next if File.exists?("/sys/class/net/#{int}/device/physfn")
        int_meta = {:name => int}
        int_meta[:interface_properties] = {}
        int_meta[:state] = intinfo[:state]
        (intinfo[:addresses] or {} rescue {}).each do |addr, addrinfo|
          if (addrinfo[:family] rescue nil) =~ /lladdr/
            # Get original mac excluding case with empty EEPROM data
            perm_addr = `ethtool -P #{int}`
            int_meta[:mac] = _get_interface_mac(int_meta[:name], addr)
            int_meta[:pxe] = _get_interface_mac(_get_parent_interface(int_meta[:name]), addr) == admin_mac.downcase
            begin
              int_info = Rethtool::InterfaceSettings.new(int)
              int_meta[:driver] = int_info.driver
              int_meta[:bus_info] = int_info.bus_info
              int_meta[:max_speed] = int_info.best_mode.speed
              if int_info.current_mode.speed == :unknown
                int_meta[:current_speed] = nil
              else
                int_meta[:current_speed] = int_info.current_mode.speed
              end
            rescue
              int_meta[:current_speed] = nil
            end

            unless int_meta[:driver]
              # Rethtool::InterfaceSettings calls two ioctls: with
              # ETHTOOL_CMD_GSET and ETHTOOL_CMD_GDRVINFO commands.
              # But for virtio adapters the first is not implemented,
              # but the second is. So try to get driver info at least
              # in this fallback chain.
              int_meta[:driver], int_meta[:bus_info] = _get_interface_driver_info(int)
            end

          elsif (addrinfo[:family] rescue nil) =~ /^inet$/
            int_meta[:ip] = addr
            int_meta[:netmask] = addrinfo[:netmask] if addrinfo[:netmask]
          end
        end
        begin
          # this stuff will put all non-fixed offloading mode into array
          # collect names of non-fixed offloading modes
          # Example of ethtool -k ethX output:
          # tx-checksumming: on
          # 	tx-checksum-ipv4: on
          # 	tx-checksum-ip-generic: off [fixed]
          # 	tx-checksum-ipv6: on
          # 	tx-checksum-fcoe-crc: off [fixed]
          # 	tx-checksum-sctp: on
          # scatter-gather: on
          # 	tx-scatter-gather: on
          # 	tx-scatter-gather-fraglist: off [fixed]
          # generic-segmentation-offload: on
          offloading_data = `ethtool -k #{int}`.split("\n").reject { |offloading|
                                 offloading.include?("Features for") ||
                                 offloading.include?("fixed")
                            }.map { |offloading|
                                 offloading.split(':')[0]
                            }
          # transform raw data into array of objects
          int_meta[:offloading_modes] = _parse_offloading(offloading_data)
        rescue
          # in case if we have no `ethtool` package installed we should
          # return empty array to support nailgun's rest api call
          int_meta[:offloading_modes] = []
        end
        # Getting SR-IOV info
        int_meta[:interface_properties][:sriov] = sriov_info(int, int_meta[:bus_info])
        # Get PCI-ID
        int_meta[:interface_properties][:pci_id] = nic_pci_id(int_meta[:bus_info])
        # Get numa node
        int_meta[:interface_properties][:numa_node] = nic_numa_node(int_meta[:bus_info])
        # Get maximum queues
        int_meta[:interface_properties][:max_queues] = _get_max_queues(int)
        detailed_meta[:interfaces] << int_meta
      end
    rescue Exception => e
      @logger.error("Error '#{e.message}' in gathering interfaces metadata: #{e.backtrace}")
    end

    begin
      (_get_detailed_cpuinfo or {} rescue {}).each do |cpu, cpuinfo|
        if cpu =~ /^[\d]+/ and cpuinfo
          frequency = cpuinfo[:mhz].to_i rescue nil
          begin
            # ohai returns current frequency, try to get max if possible
            max_frequency = `cat /sys/devices/system/cpu/cpu#{cpu}/cpufreq/cpuinfo_max_freq 2>/dev/null`.to_i / 1000
            frequency = max_frequency if max_frequency > 0
          rescue
          end
          detailed_meta[:cpu][:spec] << {
            :frequency => frequency,
            :model => (cpuinfo[:model_name].gsub(/ +/, " ") rescue nil)
          }
        end
      end
    rescue Exception => e
      @logger.error("Error '#{e.message}' in gathering cpu metadata: #{e.backtrace}")
    end

    begin
      Timeout::timeout(30) do
        @logger.debug("Trying to find block devices")

        # ohai reports the disk size according to /sys/block/#{bname}
        # which is always measured in 512 bytes blocks, no matter what
        # the physical (minimal unit which can be atomically written)
        # or logical (minimal # unit which can be addressed) block sizes are, see
        # http://lxr.free-electrons.com/source/include/linux/types.h?v=4.4#L124
        # http://lxr.free-electrons.com/source/drivers/scsi/sd.c?v=4.4#L2340
        block_size = 512

        mpath_devices, skip_devices = _multipath_devices

        (_get_blkdev_info or {} rescue {}).each do |bname, binfo|
          @logger.debug("Found block device: #{bname}")
          @logger.debug("Block device info: #{binfo.inspect}")
          dname = bname.gsub(/!/, '/')
          next if skip_devices.include?(dname)

          if physical_data_storage_devices.map{|d| d[:name]}.include?(bname) && binfo
            @logger.debug("Block device seems to be physical data storage: #{bname}")
            block = physical_data_storage_devices.select{|d| d[:name] == bname}[0]
            if block[:removable] =~ /^1$/ && ! REMOVABLE_VENDORS.include?(binfo[:vendor])
              pci_vendor_id = _get_pci_vendor_id(bname)
              @logger.debug("Block device #{bname} is removable. PCI vendor ID: #{pci_vendor_id}")
              unless REMOVABLE_PCI_VENDORS.include?(pci_vendor_id)
                next
              end
              @logger.debug("Block device #{bname} is accepted by PCI vendor ID")
            end

            detailed_meta[:disks] << {
              :name => dname,
              :model => binfo[:model],
              :size => (binfo[:size].to_i * block_size),
              :disk => block[:disk],
              :extra => block[:extra],
              :removable => block[:removable],
              :paths => nil
            }

          elsif mpath_devices.has_key?(dname)
            device = mpath_devices[dname]
            detailed_meta[:disks] << {
              :name => 'mapper/' + device["DM_NAME"],
              :model => binfo[:model],
              :size => (binfo[:size].to_i * block_size),
              :disk => dname,
              :extra => _disk_id_by_name(dname),
              :removable => 0,
              :paths => device["DM_BLKDEVS_USED"].map{|name| _disk_path_by_name(name)}.join(', ')
            }
          end
        end
        @logger.debug("Detailed meta disks: #{detailed_meta[:disks].inspect}")
      end
    rescue Exception => e
      @logger.error("Error '#{e.message}' in gathering disks metadata: #{e.backtrace}")
    end

    detailed_meta
  end

  def _get_interface_driver_info(int)
    cmd_driver = Rethtool::EthtoolCmdDriver.new
    cmd_driver.cmd = Rethtool::ETHTOOL_CMD_GDRVINFO
    data = Rethtool.ioctl(int, cmd_driver)
    [data.driver.pack('c*').delete("\000"), data.bus_info.pack('c*').delete("\000")]
  rescue => e
    @logger.error("Error '#{e.message}' while obtaining #{int} driver info.")
    [nil, nil]
  end

  def _multipath_devices
    dmsetup_command = "/sbin/dmsetup info -c --nameprefixes --noheadings -o blkdevname,subsystem,blkdevs_used,name,uuid"
    @logger.debug("Running command: #{dmsetup_command}")
    dmsetup = `#{dmsetup_command}`
    # Example output:
    # DM_BLKDEVNAME='dm-0':DM_SUBSYSTEM='mpath':DM_BLKDEVS_USED='sdb,sda':DM_NAME='31234567890abcdef':DM_UUID='mpath-31234567890abcdef'
    # DM_BLKDEVNAME='dm-1':DM_SUBSYSTEM='mpath':DM_BLKDEVS_USED='sdc,sdd':DM_NAME='92344567890abcdef':DM_UUID='mpath-92344567890abcdef'

    mpath_devices = {}
    mapping = []
    unless dmsetup.include?("No devices found")
      dmsetup.lines.each do |line|
        device = {}
        line.split(/:/).each do |key_value|
          k, v = key_value.split('=')
          device[k] = v.strip().gsub(/'/, '')
        end
        next unless device["DM_SUBSYSTEM"] == 'mpath'
        device["DM_BLKDEVS_USED"] = device["DM_BLKDEVS_USED"].split(',')
        device["DM_BLKDEVS_USED"].each do | name |
          mapping << name
        end
        mpath_devices[device["DM_BLKDEVNAME"]] = device
      end
      mapping.uniq!
    end
    @logger.debug("Multipath devices: #{mpath_devices}")
    @logger.debug("Physical devices that are used in multipath devices: #{mapping}")
    [mpath_devices, mapping]
  rescue => e
    @logger.error("Error '#{e.message}' while scanning for multipath devices.")
    [{}, []]
  end

  def _get_pci_vendor_id(devname)
    Timeout::timeout(30) do
      udevadm_walk = {}
      devpath = nil
      # expected output of `udevadm info --attribute-walk --name=#{devname}`:
      #
      #  Udevadm info starts with the device specified by the devpath and then
      #  walks up the chain of parent devices. It prints for every device
      #  found, all possible attributes in the udev rules key format.
      #  A rule to match, can be composed by the attributes of the device
      #  and the attributes from one single parent device.
      #
      #    looking at device '/devices/pci0000:00/0000:00:1e.0/0000:0d:02.0/8:0:0:1/block/sdc':
      #      KERNEL=="sdc"
      #      SUBSYSTEM=="block"
      #      DRIVER==""
      #      ATTR{ro}=="0"
      #      ATTR{size}=="30881792"
      #      ATTR{removable}=="1"
      #
      #    looking at parent device '/devices/pci0000:00/0000:00:1e.0/0000:0d:02.0':
      # Disk adapter plugged into PCIe slot, we need it's PCI vendor ID
      #      KERNELS=="0000:0d:02.0"
      #      SUBSYSTEMS=="pci"
      #      DRIVERS==""
      #      ATTRS{device}=="0x9030"
      #      ATTRS{vendor}=="0x10b5"
      #
      #    looking at parent device '/devices/pci0000:00/0000:00:1e.0':
      # PCIe slot reported as a PCI bridge device, it's PCI vendor ID is NOT what we need
      #      KERNELS=="0000:00:1e.0"
      #      SUBSYSTEMS=="pci"
      #      DRIVERS==""
      #      ATTRS{device}=="0x244e"
      #      ATTRS{vendor}=="0x8086"
      #
      #    looking at parent device '/devices/pci0000:00':
      #      KERNELS=="pci0000:00"
      #      SUBSYSTEMS==""
      #      DRIVERS==""
      `udevadm info --attribute-walk --name=#{devname}`.split("\n").each do |line|
        line.strip!
        next unless line.start_with?('looking', 'KERNEL', 'SUBSYSTEM', 'DRIVER', 'ATTR')
        if line.start_with?('looking')
          devpath = line.split("'")[1]
          udevadm_walk[devpath] = {}
        else
          key, value = line.split("==").each { |a| a.strip! }
          udevadm_walk[devpath][key] = value.gsub(/(^")|("$)/, '')
        end
      end
      # We need a vendor ID of a disk adapter rather than vendor ID of the PCIe slot where it's plugged into.
      # Therefore we should pick the device with SUBSYSTEMS==pci having the longest devpath.
      # For the example given above, vendor ID should be found as '0x10b5'.
      # Next ID of '0x8086' belongs to PCIe slot to which PCIe RAID disk adapter is inserted.
      devpath = Hash[udevadm_walk.select { |k, v| v['SUBSYSTEMS'] == 'pci' }].keys.max
      udevadm_walk[devpath]['ATTRS{vendor}']
    end
  rescue => e
    @logger.error("Error '#{e.message}' in obtaining PCI vendor ID: #{e.backtrace}")
  end

  def _disk_id_by_name(name)
    dn = "/dev/disk/by-id"
    basepath = Dir["#{dn}/**?"].select{|f| File.symlink?(f) and /\/#{name}$/.match(File.readlink(f))}
    basepath.map{|p| p.split("/")[2..-1].join("/")}
  end

  def _disk_path_by_name(name)
    dn = "/dev/disk/by-path"
    basepath = Dir["#{dn}/**?"].find{|f| File.symlink?(f) and /\/#{name}$/.match(File.readlink(f))}
    basepath.split("/")[2..-1].join("/") if basepath
  end

  # Sample mdadm --detail /dev/md127 output:
  # /dev/md127:
  #         Version : 1.2
  #   Creation Time : Thu Oct 29 16:12:00 2015
  #      Raid Level : raid1
  #      Array Size : 1048000 (1023.61 MiB 1073.15 MB)
  #   Used Dev Size : 1048000 (1023.61 MiB 1073.15 MB)
  #    Raid Devices : 2
  #   Total Devices : 2
  #     Persistence : Superblock is persistent
  #
  #     Update Time : Sun Nov  1 00:57:31 2015
  #           State : clean
  #  Active Devices : 2
  # Working Devices : 2
  #  Failed Devices : 0
  #   Spare Devices : 0
  #
  #            Name : agordeev:123  (local to host agordeev)
  #            UUID : 7aa70afc:742a9fa6:45f9f5a1:25a2585f
  #          Events : 20
  #
  #     Number   Major   Minor   RaidDevice State
  #        0     252        2        0      active sync   /dev/dm-2
  #        1     252        3        1      active sync   /dev/dm-3
  #
  def _parse_md(data)
    md = {}
    begin
      description, _, components = data.split(/Number\s+Major\s+Minor\s+RaidDevice\s+(State\s+)?/m)
      line_patterns = ['Version', 'Raid Level', 'Raid Devices', 'Active Devices',
                       'Spare Devices', 'Failed Devices', 'State', 'UUID',
                       'Container']
      for line in (description.split("\n")[1..-1] rescue [])
        line.strip!
        next if line == ""
        line_patterns.each { |pattern| md[pattern] = line.split(" : ").last if line.start_with?(pattern) }
      end
      md['devices'] = []
      for line in (components.split("\n") rescue [])
        line.strip!
        next if line == ""
        md['devices'] << line.split().last
      end
    rescue Exception => e
      @logger.error("Error '#{e.message}' in parsing MD: #{e.backtrace}")
    end
    md
  end

  def _find_fake_raid_mds()
    mds = []
    devices = []
    begin
      Dir["/sys/block/*"].each do |block_device_dir|
        basename_dir = File.basename(block_device_dir)
        devname = basename_dir.gsub(/!/, '/')
        next unless devname.start_with?('md')
        md_data = _parse_md(`mdadm --detail /dev/#{devname}`)
        next if md_data['Raid Level'] == 'container'
        if md_data.has_key?("Container")
          devices.concat((md_data['devices'] or []))
          mds << devname
        end
      end
    rescue Exception => e
      @logger.error("Error '#{e.message}' in finding fake raid MDs: #{e.backtrace}")
    end
    return mds, devices
  end

  def physical_data_storage_devices
    @blocks ||= []
    return @blocks unless @blocks.empty?

    @logger.debug("Trying to get list of physical devices")
    raise "Path /sys/block does not exist" unless File.exists?("/sys/block")

    mds, devices = _find_fake_raid_mds()

    @logger.debug("Found fake RAIDs: #{mds}")
    @logger.debug("Found components of fake RAIDs: #{devices}")

    Dir["/sys/block/*"].each do |block_device_dir|
      basename_dir = File.basename(block_device_dir)
      # Entries in /sys/block for cciss look like cciss!c0d1 while
      # the entries in /dev look like /dev/cciss/c0d1. udevadm uses
      # the entry in /dev so we need to replace the ! to get a valid
      # device name.
      devname = basename_dir.gsub(/!/, '/')

      # Skipping MD if it's a container. Also skipping underlying
      # devices from which that container is composed.
      next if devices.include?("/dev/#{devname}")
      next if devname.start_with?('md') and not mds.include?(devname)

      @logger.debug("Getting udev properties for device: #{devname}")
      properties = `udevadm info --query=property --export --name=#{devname}`.split("\n").inject({}) do |result, raw_propety|
       key, value = raw_propety.split(/\=/)
       result.update(key.strip => value.strip.chomp("'").reverse.chomp("'").reverse)
      end
      @logger.debug("Device #{devname} udev properties: #{properties.inspect}")

      @logger.debug("Filtering out devices that are used in multipath devices: 'DM_MULTIPATH_DEVICE_PATH' = '1'")
      next if properties['DM_MULTIPATH_DEVICE_PATH'] == '1'

      @logger.debug("Trying to find out if device #{devname} is removable or not")
      if File.exists?("/sys/block/#{basename_dir}/removable")
        removable = File.open("/sys/block/#{basename_dir}/removable"){ |f| f.read_nonblock(1024).strip }
      end
      @logger.debug("Device #{devname} removable parameter: #{removable.inspect}")

      if STORAGE_CODES.include?(properties['MAJOR'].to_i)
        # Exclude LVM volumes (in CentOS - 253, in Ubuntu - 252) using additional check
        # Exclude any storage device connected through USB by the default
        @logger.debug("Trying to exclude LVM volumes and USB devices")
        next if properties['DEVPATH'].include?('virtual/block/dm') ||
                (properties['ID_BUS'] == 'usb' &&
                 !@settings.has_key?("report_usb_block_devices"))

        @logger.debug("Device #{devname} seems to be appropriate")
        @blocks << {
          :name => basename_dir,
          :disk => _disk_path_by_name(devname) || devname,
          :extra => _disk_id_by_name(devname) || [],
          :removable => removable,
        }
      end
    end
    @logger.debug("Final list of physical devices is: #{@blocks.inspect}")
    @blocks
  end

  def _is_virtualbox
    @facter['productname'] == "VirtualBox"
  end

  def _is_virtual
    @facter[:is_virtual]
  end

  # JFYI: if /QEMU/ doesn't matched in /proc/cpuinfo
  # ohai[:virtualization] will return empty hash on kvm systems
  # So, this code have exactly same behavior.
  # But in my opinion here should be returned real value.
  def _manufacturer
    if _is_virtualbox
      @facter['productname']
    elsif (@facter.fetch('manufacturer', '').upcase != 'QEMU' && @facter['is_virtual'])
      @facter['virtual']
    else
      @facter.fetch('manufacturer', '')
    end
  end

  def _product_name
    unless _is_virtual
      @facter['productname']
    end
  end

  def _serial
    @facter['serialnumber']
  end

  # Returns unique identifier of machine
  # * for kvm virtual node will contain virsh UUID
  # * for physical HW that would be unique chassis id (from BIOS settings)
  # * for other hypervizors - not tested
  def uuid
    node_uuid = @facter['uuid']
    node_uuid && node_uuid.strip
  end

  def _system_info
    {
      :manufacturer => _manufacturer,
      :serial => _serial,
      :uuid => uuid,
      :runtime_uuid => @settings['runtime_uuid'],
      :product => _product_name,
      :family => (_get_dmidecode_system_info[:system][:family].strip rescue nil),
      :version => _get_dmi_info[:chassis_version],
      :fqdn => (@facter['fqdn'].strip rescue @facter['hostname'].strip rescue nil),
    }.delete_if { |key, value| value.nil? or value.empty? or value == "Not Specified" }
  end

  def _size(size, unit)
    case unit
      when /^kb$/i
        size * 1024
      when /^mb$/i
        size * 1048576
      when /^gb$/i
        size * 1073741824
    end
  end

  def _dmi_memory
    dmi = `/usr/sbin/dmidecode`
    info = {:devices => [], :total => 0, :maximum_capacity => 0, :slots => 0}
    return nil if $?.to_i != 0
    dmi.split(/\n\n/).each do |group|
      if /^Physical Memory Array$/.match(group)
        if /^\s*Maximum Capacity:\s+(\d+)\s+(mb|gb|kb)/i.match(group)
          info[:maximum_capacity] += _size($1.to_i, $2)
        end
        if /^\s*Number Of Devices:\s+(\d+)/i.match(group)
          info[:slots] += $1.to_i
        end
      elsif /^Memory Device$/.match(group)
        device_info = {}
        if /^\s*Size:\s+(\d+)\s+(mb|gb|kb)/i.match(group)
          size = _size($1.to_i, $2)
          device_info[:size] = size
          info[:total] += size
        else
          next
        end
        if /^\s*Speed:\s+(\d+)\s+MHz/i.match(group)
          device_info[:frequency] = $1.to_i
        end
        if /^\s*Type:\s+(.*?)$/i.match(group)
          device_info[:type] = $1
        end
        #if /^\s*Locator:\s+(.*?)$/i.match(group)
        #  device_info[:locator] = $1
        #end
        info[:devices].push(device_info)
      end
    end
    if info[:total] == 0
      nil
    else
      info
    end
  end

  def _facter_memory
    info = {}
    size = @facter['memorysize'].gsub(/(kb|mb|gb)$/i, "").to_i rescue (return nil)
    info[:total] = _size(size, $1)
    info
  end

  def _get_ip_mac_pair_for(local_addr)
    @network[:interfaces].each do |int, intinfo|
      next unless intinfo.has_key?(:addresses)
      intinfo[:addresses].each do |k, v|
        # Here we need to check family because IPAddr.new with bad
        # data works very slow on some environments
        # https://bugs.launchpad.net/fuel/+bug/1284571
        if v[:family] == 'inet' && !(IPAddr.new(k) rescue nil).nil?
          net = IPAddr.new("#{k}/#{v[:netmask]}")
          if net.include? local_addr
            mac = intinfo[:addresses].find { |_, info| info[:family] == 'lladdr' }[0]
            return {:ip => k, :mac => mac}
          end
        end
      end
    end
    {}
  end

  def _master_ip_and_mac_for_multirack
    rv = {}
    if File.exist?('/etc/astute.yaml')
      conf = YAML::load_file('/etc/astute.yaml')
      return {} unless conf.is_a?(Hash)
      e_point_name = conf.fetch('network_scheme', {}).fetch('roles', {}).fetch('admin/pxe', nil)
      e_point_ips = conf.fetch('network_scheme', {}).fetch('endpoints', {}).fetch(e_point_name, {}).fetch('IP', [])
      e_point_ips.each do |admin_ip|
        rv = _get_ip_mac_pair_for(admin_ip)
        break unless rv.empty?
      end
    end
    return rv
  end

  def _master_ip_and_mac
    rv = _get_ip_mac_pair_for(@api_ip)
    return (rv.empty?  ?  _master_ip_and_mac_for_multirack  :  rv)
  end

  def _data
    res = {
      :mac => (@network[:mac] rescue nil),
      :ip  => (@facter['ipaddress'] rescue nil),
      :os_platform => (@facter['operatingsystem'].downcase rescue nil)
    }
    begin
      detailed_data = _detailed
      master_data=_master_ip_and_mac
      res.merge!({
        :ip => (( master_data[:ip] or @facter['ipaddress']) rescue nil),
        :mac => (( master_data[:mac] or @network[:mac]) rescue nil),
        :manufacturer => _manufacturer,
        :platform_name => _product_name,
        :meta => detailed_data
      })
    rescue Exception => e
      @logger.error("Error '#{e.message}' in metadata calculation: #{e.backtrace}")
    end

    res[:status] = @node_state if @node_state
    res[:is_agent] = true
    res[:agent_checksum] = createsig(res)
    res
  end

  def traverse(item, &block)
    yield item
    if item.is_a?(Hash)
      item.each { |k,v| traverse(v, &block) }
    elsif item.is_a?(Array)
      item.each { |elem| traverse(elem, &block) }
    end
  end
 
  #todo: move all quirks here
  def fixup(data)
    traverse(data) do |item|
      # size for CPU means current clock frequency which constantly changes
      item.delete(:size) if item.is_a?(Hash) and item.fetch(:class, nil) == 'processor'
    end
  end

  def _get_pci_dev_list
    return {} if `cat /etc/nailgun_systemtype`.chomp != 'bootstrap'
    lshw_timeout = @settings['lshw_timeout'] || 60
    Timeout::timeout(lshw_timeout) do
      lshw_path = `which lshw`.chomp
      if $?.success?
        data = `#{lshw_path} -json -sanitize`
        return fixup(JSON.parse(data)) if $?.success?
        @logger.warn("Can't get data from lshw. Reason: lshw exited with status #{$?.exitstatus}")
      else
        @logger.warn("Can't find lshw. Reason: 'which lshw' returned exit status #{$?.exitstatus}")
      end
    end
    {}
  rescue => e
    @logger.warn("Can't get data from lshw. Reason: #{e.message}")
    {}
  end

  def get_numa_topology
    # Output EXAMPLE:
    # <distances nbobjs="2" relative_depth="1" latency_base="10.000000">
    #   <latency value="1.000000"/>
    #   <latency value="2.100000"/>
    #   <latency value="2.100000"/>
    #   <latency value="1.000000"/>
    # </distances>
    # <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">
    #   <page_type size="4096" count="14370737"/>
    #   <page_type size="1073741824" count="8"/>
    # ...
    # <object type="Bridge" os_index="0" bridge_type="0-1" depth="0" bridge_pci="0000:[00-07]">
    #   <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">
    #     <info name="PCIVendor" value="Intel Corporation"/>
    #     <info name="PCIDevice" value="Xeon E7 v3/Xeon E5 v3/Core i7 PCI Express Root Port 3"/>
    #     <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">
    #       <info name="PCIVendor" value="Intel Corporation"/>
    #       <info name="PCIDevice" value="I350 Gigabit Network Connection"/>
    #       <object type="OSDev" name="em1" osdev_type="2">
    #         <info name="Address" value="0c:c4:7a:6d:06:c6"/>

    if File.file?('/tmp/lstopo-output')
      lstopo_out = File.read('/tmp/lstopo-output')
      doc = Document.new lstopo_out
    else
      doc = Document.new `lstopo --no-caches --of xml | tee /tmp/lstopo-output`
    end

    if doc.size != 0
      topology = {:numa_nodes => [], :supported_hugepages => supported_hugepages, :distances => [["1.0"]]}

      doc.elements.each('//distances/') do |dist|
        topology[:distances] = dist.elements.collect{|v| v.attributes['value']}
          .each_slice(dist.attributes['nbobjs'].to_i).to_a
      end

      numa_node = "//object[@type='NUMANode']"
      element = doc.elements["//object[@type='NUMANode']"] ? numa_node : "//object[@type='Machine']"

      doc.elements.each(element) do |numa|

        struct = {:id=> nil, :cpus => [], :memory => nil, :pcidevs => []}
        struct[:id] = numa.attributes['os_index'].to_i
        struct[:memory] = numa.attributes['local_memory'].to_i

        numa.elements.each("#{numa.xpath}//[@type='PU']") do |pu|
          struct[:cpus] << pu.attributes['os_index'].to_i
        end
        numa.elements.each("#{numa.xpath}//[@type='PCIDev']") do |pcidev|
          struct[:pcidevs] << pcidev.attributes['pci_busid']
        end
        topology[:numa_nodes] << struct
      end
    else
      # mock dummy topology
      topology = {:numa_nodes=>[{:id=>0, :cpus=>[0], :memory=>0, :pcidevs=>[]}], :supported_hugepages=>[2048, 1048576], :distances=>[["1.0"]]}
    end
    topology
  end

  def supported_hugepages
    return [2048, 1048576] if _get_detailed_cpuinfo['0'][:flags].include?('pdpe1gb')
    return [2048] if _get_detailed_cpuinfo['0'][:flags].include?('pse')
    []
  end

  def _get_lspci_info(device)
    lspci_path = `which lspci`.chomp
    if $?.success?
      data = `#{lspci_path} -vvv -s #{device}`
      if $?.success?
        return data
      else
        @logger.warn("Can't get data from lspci. Reason: lspci exited with status #{$?.exitstatus}")
        ""
      end
    else
      @logger.warn("Can't find lspci. Reason: 'which lspci' returned exit status #{$?.exitstatus}")
      ""
    end
  rescue => e
    @logger.warn("Can't get data from lspci for #{device} slot. Reason: #{e.message}")
    ""
  end

  def update_state
    @node_state = nil
    if File.exist?("/etc/nailgun_systemtype")
      fl = File.open("/etc/nailgun_systemtype", "r")
      system_type = fl.readline.rstrip
      @node_state = "discover" if system_type == "bootstrap"
    end
  end

  def print
    s = _data.to_json
    @logger.info("Data collected by nailgun-agent:")
    @logger.info(s)
  end
end

def write_data_to_file(logger, filename, data)
  if File.exist?(filename)
    File.open(filename, 'r') do |fo|
      text = fo.read
    end
  else
    text = ''
  end

  if text != data
    begin
      File.open(filename, 'w') do |fo|
        fo.write(data)
      end
      logger.info("Wrote data to file '#{filename}'. Data: #{data}")
    rescue Exception => e
      logger.warning("Can't write data to file '#{filename}'. Reason: #{e.message}")
    end
  else
    logger.info("File '#{filename}' is up to date.")
  end
end

def provisioned?
  Socket.gethostname != 'bootstrap'
end

dry_run = false
OptionParser.new do |opts|
  opts.banner = "Usage: nailgun-agent [options]"

  opts.on("-d", "--dry-run", "Only print collected information, don't send it anywhere.") do |_d|
    dry_run = true
  end
end.parse!

logger = Logger.new(STDOUT)

if File.exist?('/etc/nailgun_uid')
  logger.level = Logger::INFO
else
  logger.level = Logger::DEBUG
end

# random sleep is here to prevent target nodes
# from reporting to master node all at once
unless dry_run
  sleep_time = rand(30)
  logger.debug("Sleep for #{sleep_time} seconds before sending request")
  sleep(sleep_time)
end

if File.exist?('/etc/nailgun-agent/nodiscover')
  logger.info("Discover prevented by /etc/nailgun-agent/nodiscover presence.")
  exit 1
end

agent = NodeAgent.new(logger, dry_run)
agent.update_state

if dry_run
  agent.print
  exit 0
end

begin
  unless File.exist?('/etc/nailgun_uid')
    resp = agent.post
    # We must not log 409 as error, after node is provisioned there will be no
    # /etc/nailgun_uid, it will be created after put request
    if [409, 403].include? resp.status
      resp = agent.put
    end
  else
    resp = agent.put
    # Handle case when node was removed, but nailgun_uid exist
    if resp.status == 400
      resp = agent.post
    end
  end
  unless [201, 200].include? resp.status
    logger.error resp.body
    exit 1
  end
  new_id = JSON.parse(resp.body)['id']
  mc_config = McollectiveConfig.new(logger)
  unless provisioned?
    mc_config.replace_identity(new_id)
  end
  write_data_to_file(logger, '/etc/nailgun_uid', new_id.to_s)
rescue => ex
  # NOTE(mihgen): There is no need to retry - cron will do it for us
  logger.error "#{ex.message}\n#{ex.backtrace}"
end
