genesis_collector 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +30 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/rspec +16 -0
- data/bin/setup +8 -0
- data/exe/genesis_collector +10 -0
- data/genesis_collector.gemspec +27 -0
- data/lib/chef/handler/genesis.rb +36 -0
- data/lib/core_ext/try.rb +102 -0
- data/lib/genesis_collector/chef.rb +27 -0
- data/lib/genesis_collector/collector.rb +149 -0
- data/lib/genesis_collector/ipmi.rb +26 -0
- data/lib/genesis_collector/lshw.rb +11 -0
- data/lib/genesis_collector/lshw_parser.rb +69 -0
- data/lib/genesis_collector/network_interfaces.rb +87 -0
- data/lib/genesis_collector/simple_http.rb +39 -0
- data/lib/genesis_collector/version.rb +3 -0
- data/lib/genesis_collector.rb +6 -0
- metadata +138 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 02422af30ab1c4103629eae6d9084c27f9429352
|
4
|
+
data.tar.gz: 412e2af98f194cfc4268e00b834ff5abccfbf0b2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6c463e6cb69a4585906fe2bb5f2c02dd97eb881ed8b74ebfdfae25134079a11de76de4b05b9b7274a394fd5c2050c1e018ed3ee263e6976e756a6a0eb22e0a4c
|
7
|
+
data.tar.gz: 1942448154d46c133f09b4b283b8bbd1afb31a602852926bdb44d8839d5c7e86233b3a37a9e544deb1197076fc8d4acf388d72bfebb9371cd814e56aa09932a5
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 David Radcliffe
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# genesis_collector
|
2
|
+
|
3
|
+
This gem is a small utility that will collect information about your hardware server for the purpose of sending it back to a Genesis server.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Install it:
|
8
|
+
|
9
|
+
$ gem install genesis_collector
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
This gem ships with a command line tool to show you the data it collects. Simply run `genesis_collector` and you will see all the data. This command does not send any data anywhere.
|
14
|
+
|
15
|
+
This gem also ships with a Chef handler which will collect and send the data.
|
16
|
+
|
17
|
+
## Development
|
18
|
+
|
19
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
20
|
+
|
21
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/genesis_collector.
|
26
|
+
|
27
|
+
|
28
|
+
## License
|
29
|
+
|
30
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'genesis_collector'
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require 'irb'
|
14
|
+
IRB.start
|
data/bin/rspec
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# This file was generated by Bundler.
|
4
|
+
#
|
5
|
+
# The application 'rspec' is installed as part of a gem, and
|
6
|
+
# this file is here to facilitate running it.
|
7
|
+
#
|
8
|
+
|
9
|
+
require "pathname"
|
10
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
|
11
|
+
Pathname.new(__FILE__).realpath)
|
12
|
+
|
13
|
+
require "rubygems"
|
14
|
+
require "bundler/setup"
|
15
|
+
|
16
|
+
load Gem.bin_path("rspec-core", "rspec")
|
data/bin/setup
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'genesis_collector/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'genesis_collector'
|
8
|
+
spec.version = GenesisCollector::VERSION
|
9
|
+
spec.authors = ['David Radcliffe']
|
10
|
+
spec.email = ['david.radcliffe@shopify.com']
|
11
|
+
|
12
|
+
spec.summary = 'Agent to collect information about bare metal servers and send it to Genesis.'
|
13
|
+
spec.homepage = "https://github.com/Shopify/genesis_collector"
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = 'exe'
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_dependency 'nokogiri'
|
22
|
+
|
23
|
+
spec.add_development_dependency 'bundler', '~> 1.11'
|
24
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
25
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
26
|
+
spec.add_development_dependency 'webmock', '~> 1.22'
|
27
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'chef/handler'
|
2
|
+
require 'chef/mash'
|
3
|
+
|
4
|
+
class Chef
|
5
|
+
class Handler
|
6
|
+
class Genesis < Chef::Handler
|
7
|
+
attr_reader :config
|
8
|
+
|
9
|
+
def initialize(config = {})
|
10
|
+
@config = Mash.new(config)
|
11
|
+
end
|
12
|
+
|
13
|
+
def report
|
14
|
+
prepare_report
|
15
|
+
send_report
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def prepare_report
|
21
|
+
@collector = GenesisCollector.Collector.new(@config.merge(chef_node: run_context.node))
|
22
|
+
@collector.collect!
|
23
|
+
rescue => e
|
24
|
+
Chef::Log.error("Error collecting system information for Genesis:\n" + e.message)
|
25
|
+
Chef::Log.error(e.backtrace.join("\n"))
|
26
|
+
end
|
27
|
+
|
28
|
+
def send_report
|
29
|
+
@collector.submit!
|
30
|
+
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT => e
|
31
|
+
Chef::Log.error("Could not connect to Genesis. Connection error:\n" + e.message)
|
32
|
+
Chef::Log.error(e.backtrace.join("\n"))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/core_ext/try.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
# https://github.com/rails/rails/blob/ff7d37d5884be3939833fb52b58a95017369eda5/activesupport/lib/active_support/core_ext/object/try.rb
|
2
|
+
|
3
|
+
class Object
|
4
|
+
# Invokes the public method whose name goes as first argument just like
|
5
|
+
# +public_send+ does, except that if the receiver does not respond to it the
|
6
|
+
# call returns +nil+ rather than raising an exception.
|
7
|
+
#
|
8
|
+
# This method is defined to be able to write
|
9
|
+
#
|
10
|
+
# @person.try(:name)
|
11
|
+
#
|
12
|
+
# instead of
|
13
|
+
#
|
14
|
+
# @person.name if @person
|
15
|
+
#
|
16
|
+
# +try+ calls can be chained:
|
17
|
+
#
|
18
|
+
# @person.try(:spouse).try(:name)
|
19
|
+
#
|
20
|
+
# instead of
|
21
|
+
#
|
22
|
+
# @person.spouse.name if @person && @person.spouse
|
23
|
+
#
|
24
|
+
# +try+ will also return +nil+ if the receiver does not respond to the method:
|
25
|
+
#
|
26
|
+
# @person.try(:non_existing_method) #=> nil
|
27
|
+
#
|
28
|
+
# instead of
|
29
|
+
#
|
30
|
+
# @person.non_existing_method if @person.respond_to?(:non_existing_method) #=> nil
|
31
|
+
#
|
32
|
+
# +try+ returns +nil+ when called on +nil+ regardless of whether it responds
|
33
|
+
# to the method:
|
34
|
+
#
|
35
|
+
# nil.try(:to_i) # => nil, rather than 0
|
36
|
+
#
|
37
|
+
# Arguments and blocks are forwarded to the method if invoked:
|
38
|
+
#
|
39
|
+
# @posts.try(:each_slice, 2) do |a, b|
|
40
|
+
# ...
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# The number of arguments in the signature must match. If the object responds
|
44
|
+
# to the method the call is attempted and +ArgumentError+ is still raised
|
45
|
+
# in case of argument mismatch.
|
46
|
+
#
|
47
|
+
# If +try+ is called without arguments it yields the receiver to a given
|
48
|
+
# block unless it is +nil+:
|
49
|
+
#
|
50
|
+
# @person.try do |p|
|
51
|
+
# ...
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# You can also call try with a block without accepting an argument, and the block
|
55
|
+
# will be instance_eval'ed instead:
|
56
|
+
#
|
57
|
+
# @person.try { upcase.truncate(50) }
|
58
|
+
#
|
59
|
+
# Please also note that +try+ is defined on +Object+. Therefore, it won't work
|
60
|
+
# with instances of classes that do not have +Object+ among their ancestors,
|
61
|
+
# like direct subclasses of +BasicObject+. For example, using +try+ with
|
62
|
+
# +SimpleDelegator+ will delegate +try+ to the target instead of calling it on
|
63
|
+
# the delegator itself.
|
64
|
+
def try(*a, &b)
|
65
|
+
try!(*a, &b) if a.empty? || respond_to?(a.first)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Same as #try, but will raise a NoMethodError exception if the receiver is not +nil+ and
|
69
|
+
# does not implement the tried method.
|
70
|
+
|
71
|
+
def try!(*a, &b)
|
72
|
+
if a.empty? && block_given?
|
73
|
+
if b.arity == 0
|
74
|
+
instance_eval(&b)
|
75
|
+
else
|
76
|
+
yield self
|
77
|
+
end
|
78
|
+
else
|
79
|
+
public_send(*a, &b)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
class NilClass
|
85
|
+
# Calling +try+ on +nil+ always returns +nil+.
|
86
|
+
# It becomes especially helpful when navigating through associations that may return +nil+.
|
87
|
+
#
|
88
|
+
# nil.try(:name) # => nil
|
89
|
+
#
|
90
|
+
# Without +try+
|
91
|
+
# @person && @person.children.any? && @person.children.first.name
|
92
|
+
#
|
93
|
+
# With +try+
|
94
|
+
# @person.try(:children).try(:first).try(:name)
|
95
|
+
def try(*args)
|
96
|
+
nil
|
97
|
+
end
|
98
|
+
|
99
|
+
def try!(*args)
|
100
|
+
nil
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module GenesisCollector
|
2
|
+
module Chef
|
3
|
+
|
4
|
+
def collect_chef
|
5
|
+
@payload[:chef] = {
|
6
|
+
environment: get_chef_environment,
|
7
|
+
roles: (@chef_node.respond_to?(:[]) ? @chef_node['roles'] : []),
|
8
|
+
run_list: (@chef_node.respond_to?(:[]) ? @chef_node['run_list'] : ''),
|
9
|
+
tags: get_chef_tags
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
def get_chef_environment
|
14
|
+
env = nil
|
15
|
+
env = File.read('/etc/chef/current_environment').gsub(/\s+/, '') if File.exist? '/etc/chef/current_environment'
|
16
|
+
env || 'unknown'
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_chef_tags
|
20
|
+
node_show_output = shellout_with_timeout('knife node show `hostname` -c /etc/chef/client.rb')
|
21
|
+
node_show_output.match(/Tags:(.*)/)[0].delete(' ').gsub('Tags:', '').split(',')
|
22
|
+
rescue
|
23
|
+
[]
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'resolv'
|
3
|
+
require 'socket'
|
4
|
+
require 'genesis_collector/simple_http'
|
5
|
+
require 'genesis_collector/network_interfaces'
|
6
|
+
require 'genesis_collector/chef'
|
7
|
+
require 'genesis_collector/ipmi'
|
8
|
+
require 'genesis_collector/lshw'
|
9
|
+
require 'English'
|
10
|
+
|
11
|
+
module GenesisCollector
|
12
|
+
class Collector
|
13
|
+
attr_reader :payload
|
14
|
+
|
15
|
+
include GenesisCollector::NetworkInterfaces
|
16
|
+
include GenesisCollector::Chef
|
17
|
+
include GenesisCollector::IPMI
|
18
|
+
include GenesisCollector::Lshw
|
19
|
+
|
20
|
+
def initialize(config = {})
|
21
|
+
@chef_node = config.delete(:chef_node)
|
22
|
+
@config = config
|
23
|
+
@payload = {}
|
24
|
+
end
|
25
|
+
|
26
|
+
def collect!
|
27
|
+
@sku = get_sku
|
28
|
+
collect_basic_data
|
29
|
+
collect_chef
|
30
|
+
collect_ipmi
|
31
|
+
collect_network_interfaces
|
32
|
+
collect_disks
|
33
|
+
collect_cpus
|
34
|
+
collect_memories
|
35
|
+
@payload
|
36
|
+
end
|
37
|
+
|
38
|
+
def submit!
|
39
|
+
fail 'Must collect data first!' unless @payload
|
40
|
+
headers = {
|
41
|
+
'Authorization' => "Token token=\"#{@config[:api_token]}\"",
|
42
|
+
'Content-Type' => 'application/json'
|
43
|
+
}
|
44
|
+
http = SimpleHTTP.new(@config[:endpoint], headers: headers)
|
45
|
+
http.patch("/api/devices/#{@sku}", @payload)
|
46
|
+
end
|
47
|
+
|
48
|
+
def collect_basic_data
|
49
|
+
@payload = {
|
50
|
+
type: 'Server',
|
51
|
+
hostname: get_hostname,
|
52
|
+
os: {
|
53
|
+
distribution: get_distribution,
|
54
|
+
release: get_release,
|
55
|
+
codename: get_codename,
|
56
|
+
description: get_description
|
57
|
+
},
|
58
|
+
product: read_dmi('system-product-name'),
|
59
|
+
vendor: read_dmi('system-manufacturer'),
|
60
|
+
properties: {
|
61
|
+
'SYSTEM_SERIAL_NUMBER' => read_dmi('system-serial-number'),
|
62
|
+
'BASEBOARD_VENDOR' => read_dmi('baseboard-manufacturer'),
|
63
|
+
'BASEBOARD_PRODUCT_NAME' => read_dmi('baseboard-product-name'),
|
64
|
+
'BASEBOARD_SERIAL_NUMBER' => read_dmi('baseboard-serial-number'),
|
65
|
+
'CHASSIS_VENDOR' => read_dmi('chassis-manufacturer'),
|
66
|
+
'CHASSIS_SERIAL_NUMBER' => read_dmi('chassis-serial-number')
|
67
|
+
}
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
def collect_disks
|
72
|
+
@payload[:disks] = get_lshw_data.disks
|
73
|
+
end
|
74
|
+
|
75
|
+
def collect_cpus
|
76
|
+
@payload[:cpus] = get_lshw_data.cpus
|
77
|
+
end
|
78
|
+
|
79
|
+
def collect_memories
|
80
|
+
@payload[:memories] = get_lshw_data.memories
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def shellout_with_timeout(command, timeout = 2)
|
86
|
+
response = `timeout #{timeout} #{command}`
|
87
|
+
unless $CHILD_STATUS.success?
|
88
|
+
puts "Call to #{command} timed out after #{timeout} seconds"
|
89
|
+
return ''
|
90
|
+
end
|
91
|
+
response
|
92
|
+
end
|
93
|
+
|
94
|
+
def get_distribution
|
95
|
+
read_lsb_key('DISTRIB_ID')
|
96
|
+
end
|
97
|
+
|
98
|
+
def get_release
|
99
|
+
read_lsb_key('DISTRIB_RELEASE')
|
100
|
+
end
|
101
|
+
|
102
|
+
def get_codename
|
103
|
+
read_lsb_key('DISTRIB_CODENAME')
|
104
|
+
end
|
105
|
+
|
106
|
+
def get_description
|
107
|
+
read_lsb_key('DISTRIB_DESCRIPTION')
|
108
|
+
end
|
109
|
+
|
110
|
+
def get_hostname
|
111
|
+
Socket.gethostname
|
112
|
+
end
|
113
|
+
|
114
|
+
def read_lsb_key(key)
|
115
|
+
@lsb_data ||= File.read('/etc/lsb-release')
|
116
|
+
@lsb_data.match(/^#{key}=["']?(.+?)["']?$/)[1] || 'unknown'
|
117
|
+
end
|
118
|
+
|
119
|
+
def get_sku
|
120
|
+
vendor = nil
|
121
|
+
serial = nil
|
122
|
+
vendor ||= read_dmi 'baseboard-manufacturer'
|
123
|
+
serial ||= read_dmi 'baseboard-serial-number'
|
124
|
+
serial = nil if serial == '0123456789'
|
125
|
+
|
126
|
+
vendor ||= read_dmi 'system-manufacturer'
|
127
|
+
serial ||= read_dmi 'system-serial-number'
|
128
|
+
serial = nil if serial == '0123456789'
|
129
|
+
|
130
|
+
serial ||= read_ipmi_fru('Board Serial')
|
131
|
+
|
132
|
+
vendor ||= 'Unknown'
|
133
|
+
manufacturer = case vendor
|
134
|
+
when 'DellInc'
|
135
|
+
'DEL'
|
136
|
+
when 'Supermicro'
|
137
|
+
'SPM'
|
138
|
+
else
|
139
|
+
'UKN'
|
140
|
+
end
|
141
|
+
"#{manufacturer}-#{serial}"
|
142
|
+
end
|
143
|
+
|
144
|
+
def read_dmi(key)
|
145
|
+
value = shellout_with_timeout("dmidecode -s #{key}").gsub(/\s+|\./, '')
|
146
|
+
value.empty? ? nil : value
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module GenesisCollector
|
2
|
+
module IPMI
|
3
|
+
|
4
|
+
def collect_ipmi
|
5
|
+
@payload[:ipmi] = {
|
6
|
+
address: read_ipmi_attribute('IP Address'),
|
7
|
+
netmask: read_ipmi_attribute('Subnet Mask'),
|
8
|
+
mac: read_ipmi_attribute('MAC Address'),
|
9
|
+
gateway: read_ipmi_attribute('Default Gateway IP')
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def read_ipmi_attribute(key)
|
16
|
+
data = shellout_with_timeout('ipmitool lan print')
|
17
|
+
data.match(/#{key}\s*:\s*(\S+)$/)[1] || 'unknown'
|
18
|
+
end
|
19
|
+
|
20
|
+
def read_ipmi_fru(key)
|
21
|
+
data = shellout_with_timeout('ipmitool fru')
|
22
|
+
data.match(/#{key}\s*:\s*(\S+)$/)[1] || 'unknown'
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'core_ext/try'
|
3
|
+
|
4
|
+
module GenesisCollector
|
5
|
+
class LshwParser
|
6
|
+
attr_reader :doc
|
7
|
+
|
8
|
+
def initialize(doc)
|
9
|
+
@doc = Nokogiri::XML(doc)
|
10
|
+
end
|
11
|
+
|
12
|
+
def disks
|
13
|
+
@disks ||= doc.xpath("//node[@class='disk']").map do |disk|
|
14
|
+
disk_size = disk.at_xpath('.//size')
|
15
|
+
{
|
16
|
+
size: disk_size.nil? ? 0 : disk_size.text.to_i,
|
17
|
+
serial_number: disk.at_xpath('.//serial').try(:text),
|
18
|
+
kind: /(?<kind>[a-zA-Z]+)/.match(disk.at_xpath('.//businfo').text)[:kind],
|
19
|
+
description: disk.at_xpath('.//description').text,
|
20
|
+
product: disk.at_xpath('.//product').try(:text),
|
21
|
+
vendor_name: nil
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def cpus
|
27
|
+
@cpus ||= doc.xpath("//node[@class='processor']").map do |cpu|
|
28
|
+
{
|
29
|
+
description: cpu.at_xpath('.//product').try(:text) || cpu.at_xpath('.//description').try(:text),
|
30
|
+
cores: cpu.at_xpath(".//configuration/setting[@id='cores']/@value").try(:value).try(:to_i),
|
31
|
+
threads: cpu.at_xpath(".//configuration/setting[@id='threads']/@value").try(:value).try(:to_i),
|
32
|
+
speed: cpu.at_xpath('.//size').try(:text).try(:to_i),
|
33
|
+
vendor_name: cpu.at_xpath('.//vendor').try(:text),
|
34
|
+
physid: cpu.at_xpath('.//physid').try(:text).try(:to_i)
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def memories
|
40
|
+
@memories ||= doc.xpath("//node[@class='memory']/*[@id]").map do |memory|
|
41
|
+
mem_size = memory.at_xpath('.//size')
|
42
|
+
{
|
43
|
+
size: mem_size.nil? ? 0 : mem_size.text.to_i,
|
44
|
+
description: memory.at_xpath('.//description').text,
|
45
|
+
bank: memory.at_xpath('.//physid').text.to_i,
|
46
|
+
slot: memory.at_xpath('.//slot').try(:text),
|
47
|
+
product: memory.at_xpath('.//product').try(:text),
|
48
|
+
vendor_name: memory.at_xpath('.//vendor').try(:text)
|
49
|
+
}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def network_interfaces
|
54
|
+
@network_interfaces ||= doc.xpath("//node[@class='network']").map do |network_interface|
|
55
|
+
{
|
56
|
+
name: network_interface.at_xpath('.//logicalname').try(:text),
|
57
|
+
description: network_interface.at_xpath('.//description').try(:text),
|
58
|
+
mac_address: network_interface.at_xpath('.//serial').try(:text),
|
59
|
+
product: network_interface.at_xpath('.//product').try(:text),
|
60
|
+
vendor_name: network_interface.at_xpath('.//vendor').try(:text),
|
61
|
+
driver: network_interface.at_xpath(".//configuration//setting[@id='driver']/@value").try(:text),
|
62
|
+
driver_version: network_interface.at_xpath(".//configuration//setting[@id='driverversion']/@value").try(:text),
|
63
|
+
duplex: network_interface.at_xpath(".//configuration//setting[@id='duplex']/@value").try(:text),
|
64
|
+
link_type: network_interface.at_xpath(".//configuration//setting[@id='port']/@value").try(:text)
|
65
|
+
}
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module GenesisCollector
|
4
|
+
module NetworkInterfaces
|
5
|
+
|
6
|
+
def collect_network_interfaces
|
7
|
+
interfaces = {}
|
8
|
+
Socket.getifaddrs.each do |ifaddr|
|
9
|
+
next if ifaddr.name.start_with?('lo')
|
10
|
+
interfaces[ifaddr.name] ||= {}
|
11
|
+
interfaces[ifaddr.name][:addresses] ||= []
|
12
|
+
next unless ifaddr.addr.ipv4?
|
13
|
+
interfaces[ifaddr.name][:addresses] << {
|
14
|
+
address: ifaddr.addr.ip_address,
|
15
|
+
netmask: ifaddr.netmask.ip_address
|
16
|
+
}
|
17
|
+
end
|
18
|
+
@payload[:network_interfaces] = interfaces.reduce([]) { |memo, (k, v)| memo << v.merge(name: k) }
|
19
|
+
@payload[:network_interfaces].each do |i|
|
20
|
+
lshw_interface = get_lshw_data.network_interfaces.select { |lshw_i| lshw_i[:name] == i[:name] }[0]
|
21
|
+
i[:mac_address] = read_mac_address(i[:name])
|
22
|
+
i[:product] = lshw_interface.try(:[], :product)
|
23
|
+
i[:speed] = read_interface_info(i[:name], 'speed')
|
24
|
+
i[:vendor_name] = lshw_interface.try(:[], :vendor_name)
|
25
|
+
i[:duplex] = read_interface_info(i[:name], 'duplex')
|
26
|
+
i[:link_type] = lshw_interface.try(:[], :link_type)
|
27
|
+
i[:neighbor] = get_network_neighbor(i[:name])
|
28
|
+
i.merge!(get_interface_driver(i[:name]))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def read_mac_address(interface)
|
35
|
+
if !interface.start_with?('bond') && File.exist?("/sys/class/net/#{interface}/bonding_slave/perm_hwaddr")
|
36
|
+
read_interface_info(interface, 'bonding_slave/perm_hwaddr')
|
37
|
+
else
|
38
|
+
read_interface_info(interface, 'address')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def read_interface_info(interface, key)
|
43
|
+
File.read("/sys/class/net/#{interface}/#{key}").strip
|
44
|
+
end
|
45
|
+
|
46
|
+
def get_interface_driver(interface)
|
47
|
+
value = shellout_with_timeout("ethtool --driver #{interface}")
|
48
|
+
{ driver: value.match(/^driver: (.*)/)[1], driver_version: value.match(/^version: (.*)/)[1] }
|
49
|
+
end
|
50
|
+
|
51
|
+
def get_network_neighbor(interface_name)
|
52
|
+
@lldp_data ||= parse_lldp
|
53
|
+
@lldp_data[interface_name]
|
54
|
+
end
|
55
|
+
|
56
|
+
def parse_lldp
|
57
|
+
@raw_lldp_output ||= shellout_with_timeout('lldpctl -f keyvalue')
|
58
|
+
data = {}
|
59
|
+
@raw_lldp_output.split("\n").each do |kvp|
|
60
|
+
key, value = kvp.split('=')
|
61
|
+
fields = key.split('.')
|
62
|
+
name = fields[1]
|
63
|
+
data[name] ||= { name: name }
|
64
|
+
case fields[2..-1].join('.')
|
65
|
+
when 'chassis.name'
|
66
|
+
data[name][:chassis_name] = value
|
67
|
+
when 'chassis.descr'
|
68
|
+
data[name][:chassis_desc] = value
|
69
|
+
when 'chassis.mac'
|
70
|
+
data[name][:chassis_id_type] = 'mac'
|
71
|
+
data[name][:chassis_id_value] = value
|
72
|
+
when 'port.ifname'
|
73
|
+
data[name][:port_id_type] = 'ifname'
|
74
|
+
data[name][:port_id_value] = value
|
75
|
+
when 'port.descr'
|
76
|
+
data[name][:port_desc] = value
|
77
|
+
when 'vlan.vlan-id'
|
78
|
+
data[name][:vlan_id] = value
|
79
|
+
when 'vlan'
|
80
|
+
data[name][:vlan_name] = value
|
81
|
+
end
|
82
|
+
end
|
83
|
+
data
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'net/http'
|
3
|
+
require 'uri'
|
4
|
+
require 'genesis_collector'
|
5
|
+
|
6
|
+
module GenesisCollector
|
7
|
+
class SimpleHTTP
|
8
|
+
def initialize(host, headers: {}, timeout: 2)
|
9
|
+
@host = host
|
10
|
+
@headers = {'User-Agent' => GenesisCollector::USER_AGENT}.merge! headers
|
11
|
+
@timeout = timeout
|
12
|
+
end
|
13
|
+
|
14
|
+
def patch(endpoint, payload = nil, headers = {})
|
15
|
+
verb(endpoint, payload, headers) { |uri| Net::HTTP::Patch.new(uri) }
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def verb(endpoint, payload, headers)
|
21
|
+
uri = URI.parse("#{@host}#{endpoint}")
|
22
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
23
|
+
http.open_timeout = http.read_timeout = http.ssl_timeout = @timeout
|
24
|
+
http.use_ssl = true if @host.start_with?('https')
|
25
|
+
request = yield uri.request_uri
|
26
|
+
request.body = JSON.dump(payload) unless payload.nil?
|
27
|
+
request = add_headers(request, headers)
|
28
|
+
http.request(request)
|
29
|
+
end
|
30
|
+
|
31
|
+
def add_headers(request, headers)
|
32
|
+
headers.merge!(@headers)
|
33
|
+
headers.each do |k, v|
|
34
|
+
request[k] = v
|
35
|
+
end
|
36
|
+
request
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
metadata
ADDED
@@ -0,0 +1,138 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: genesis_collector
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- David Radcliffe
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-02-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: nokogiri
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.11'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.11'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: webmock
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.22'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.22'
|
83
|
+
description:
|
84
|
+
email:
|
85
|
+
- david.radcliffe@shopify.com
|
86
|
+
executables:
|
87
|
+
- genesis_collector
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- ".gitignore"
|
92
|
+
- ".rspec"
|
93
|
+
- Gemfile
|
94
|
+
- LICENSE.txt
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- bin/console
|
98
|
+
- bin/rspec
|
99
|
+
- bin/setup
|
100
|
+
- exe/genesis_collector
|
101
|
+
- genesis_collector.gemspec
|
102
|
+
- lib/chef/handler/genesis.rb
|
103
|
+
- lib/core_ext/try.rb
|
104
|
+
- lib/genesis_collector.rb
|
105
|
+
- lib/genesis_collector/chef.rb
|
106
|
+
- lib/genesis_collector/collector.rb
|
107
|
+
- lib/genesis_collector/ipmi.rb
|
108
|
+
- lib/genesis_collector/lshw.rb
|
109
|
+
- lib/genesis_collector/lshw_parser.rb
|
110
|
+
- lib/genesis_collector/network_interfaces.rb
|
111
|
+
- lib/genesis_collector/simple_http.rb
|
112
|
+
- lib/genesis_collector/version.rb
|
113
|
+
homepage: https://github.com/Shopify/genesis_collector
|
114
|
+
licenses:
|
115
|
+
- MIT
|
116
|
+
metadata: {}
|
117
|
+
post_install_message:
|
118
|
+
rdoc_options: []
|
119
|
+
require_paths:
|
120
|
+
- lib
|
121
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
requirements: []
|
132
|
+
rubyforge_project:
|
133
|
+
rubygems_version: 2.6.1
|
134
|
+
signing_key:
|
135
|
+
specification_version: 4
|
136
|
+
summary: Agent to collect information about bare metal servers and send it to Genesis.
|
137
|
+
test_files: []
|
138
|
+
has_rdoc:
|