genesis_collector 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: