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 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
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --require spec_helper
3
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in genesis_collector.gemspec
4
+ gemspec
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
@@ -0,0 +1,2 @@
1
+ require 'bundler/gem_tasks'
2
+ task default: :spec
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,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'genesis_collector'
5
+ require 'pp'
6
+
7
+ collector = GenesisCollector::Collector.new
8
+ collector.collect!
9
+
10
+ pp collector.payload
@@ -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
@@ -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,11 @@
1
+ require 'genesis_collector/lshw_parser'
2
+
3
+ module GenesisCollector
4
+ module Lshw
5
+
6
+ def get_lshw_data
7
+ @lshw_data ||= GenesisCollector::LshwParser.new(shellout_with_timeout('lshw -xml', 40).strip)
8
+ end
9
+
10
+ end
11
+ 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
@@ -0,0 +1,3 @@
1
+ module GenesisCollector
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,6 @@
1
+ require 'genesis_collector/version'
2
+ require 'genesis_collector/collector'
3
+
4
+ module GenesisCollector
5
+ USER_AGENT = "GenesisCollector/#{GenesisCollector::VERSION}".freeze
6
+ 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: