1456 lines
49 KiB
Ruby
Executable File
1456 lines
49 KiB
Ruby
Executable File
#!/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
|
|
@logger.debug("Waiting for udev to complete evaluating rules")
|
|
`udevadm settle`
|
|
|
|
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"/>
|
|
|
|
doc = Document.new `lstopo --no-caches --of xml`
|
|
|
|
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
|
|
topology
|
|
rescue => e
|
|
@logger.error "Something went wrong with parsing lstopo: #{e.backtrace}"
|
|
nil
|
|
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
|