falcore 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.
@@ -0,0 +1,34 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'falcore/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'falcore'
8
+ spec.version = Falcore::VERSION
9
+ spec.authors = ['Seth Vargo']
10
+ spec.email = ['sethvargo@gmail.com']
11
+ spec.summary = 'A Ruby application for collecting Jenkins node slave ' \
12
+ 'status and aggregating to multiple output formats.'
13
+ spec.description = 'Falcore is a Ruby library and CLI for collecting ' \
14
+ 'Jenkins slave information from a Jenkins master and ' \
15
+ 'aggregating to multiple output formats and checks ' \
16
+ 'as StatsD or Nagios.'
17
+ spec.homepage = 'https://github.com/opscode/falcore'
18
+ spec.license = 'Apache 2.0'
19
+
20
+ spec.required_ruby_version = '>= 2.0'
21
+
22
+ spec.files = `git ls-files -z`.split("\x0")
23
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
24
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
25
+ spec.require_paths = ['lib']
26
+
27
+ spec.add_runtime_dependency 'statsd-ruby', '~> 1.2'
28
+
29
+ spec.add_development_dependency 'bundler', '~> 1.5'
30
+ spec.add_development_dependency 'rspec', '~> 2.14'
31
+ spec.add_development_dependency 'rake'
32
+ spec.add_development_dependency 'sinatra', '~> 1.4'
33
+ spec.add_development_dependency 'webmock', '~> 1.17'
34
+ end
@@ -0,0 +1,38 @@
1
+ #
2
+ # Author: Seth Vargo <sethvargo@gmail.com>
3
+ #
4
+ # Copyright 2014 Chef Software, Inc.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'falcore/version'
20
+
21
+ module Falcore
22
+ autoload :Aggregator, 'falcore/aggregator'
23
+ autoload :Config, 'falcore/config'
24
+ autoload :Fetcher, 'falcore/fetcher'
25
+ autoload :NullObject, 'falcore/null_object'
26
+ autoload :Util, 'falcore/util'
27
+
28
+ module Dumper
29
+ autoload :Base, 'falcore/dumpers/base'
30
+ autoload :Statsd, 'falcore/dumpers/statsd'
31
+ end
32
+
33
+ module Node
34
+ autoload :Base, 'falcore/nodes/base'
35
+ autoload :Master, 'falcore/nodes/master'
36
+ autoload :Slave, 'falcore/nodes/slave'
37
+ end
38
+ end
@@ -0,0 +1,41 @@
1
+ #
2
+ # Author: Seth Vargo <sethvargo@gmail.com>
3
+ #
4
+ # Copyright 2014 Chef Software, Inc.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module Falcore
20
+ class Aggregator
21
+ attr_reader :config
22
+
23
+ #
24
+ #
25
+ #
26
+ def initialize(config)
27
+ @config = config
28
+ end
29
+
30
+ def run
31
+ hash = Fetcher.get("#{config.jenkins.endpoint}/computer/api/json")
32
+
33
+ master = Node::Master.new(hash['computer'][0])
34
+ slaves = hash['computer'][1..-1].map do |slave|
35
+ Node::Slave.new(master, slave)
36
+ end
37
+
38
+ master
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,125 @@
1
+ #
2
+ # Author: Seth Vargo <sethvargo@gmail.com>
3
+ #
4
+ # Copyright 2014 Chef Software, Inc.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module Falcore
20
+ class Config < Hash
21
+ class << self
22
+ def from_file(path)
23
+ contents = File.read(File.expand_path(path))
24
+ parse(contents)
25
+ rescue Errno::ENOENT
26
+ raise "No config found at '#{path}'!"
27
+ end
28
+
29
+ def parse(contents)
30
+ Parser.new(contents).parse
31
+ end
32
+
33
+ private
34
+
35
+ class Parser
36
+ CONFIG_SECTION = /\[(.+)\]/.freeze
37
+ CONFIG_OPTION = /[[:space:]]{2,}(.+) = (.+)/.freeze
38
+
39
+ def initialize(contents)
40
+ @contents = contents
41
+ @result = Config.new
42
+ end
43
+
44
+ def parse
45
+ @contents.split(/\r?\n/).each do |line|
46
+ next if line.strip.empty?
47
+ next if line.strip.start_with?('#')
48
+
49
+ if line =~ CONFIG_SECTION
50
+ @current_key = $1
51
+ elsif line =~ CONFIG_OPTION
52
+ match = line.match(CONFIG_OPTION)
53
+ key = match[1]
54
+ value = match[2]
55
+
56
+ # Make sure we are keyed
57
+ @result[@current_key] ||= Config.new
58
+ @result[@current_key][key] = coerce(value)
59
+ else
60
+ raise "Could not parse line '#{line}'"
61
+ end
62
+ end
63
+
64
+ @result
65
+ end
66
+
67
+ private
68
+
69
+ #
70
+ # Coerce the value into it's appropiate Ruby type.
71
+ #
72
+ # @param [Object] value
73
+ # @return [Object]
74
+ # the coerced value
75
+ #
76
+ def coerce(value)
77
+ case value
78
+ when /^[[:digit:]]+$/
79
+ value.to_i
80
+ when /^[[:digit:]]+\.[[:digit:]]+$/
81
+ value.to_f
82
+ else
83
+ value
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ # @private
90
+ def method_missing(m, *args, &block)
91
+ key = m.to_s
92
+
93
+ if key.include?('=')
94
+ set(key.delete('='), args.first)
95
+ else
96
+ if has_key?(key)
97
+ get(key)
98
+ else
99
+ NullObject.new
100
+ end
101
+ end
102
+ end
103
+
104
+ # @private
105
+ def respond_to_missing?(m, include_private = false)
106
+ key = m.to_s
107
+
108
+ if key.include?('=')
109
+ true
110
+ else
111
+ has_key?(key) || super
112
+ end
113
+ end
114
+
115
+ private
116
+
117
+ def get(key)
118
+ self[key]
119
+ end
120
+
121
+ def set(key, value)
122
+ self[key] = value
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,84 @@
1
+ #
2
+ # Author: Seth Vargo <sethvargo@gmail.com>
3
+ #
4
+ # Copyright 2014 Chef Software, Inc.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module Falcore
20
+ class Dumper::Base
21
+ class << self
22
+ def run(&block)
23
+ block ? @run = block : @run || Proc.new
24
+ end
25
+
26
+ def validate(&block)
27
+ block ? @validate = block : @validate || Proc.new
28
+ end
29
+ end
30
+
31
+ # @return [Falcore::Config]
32
+ attr_reader :config
33
+
34
+ # @return [Falcore::Node::Master]
35
+ attr_reader :master
36
+
37
+ #
38
+ # @param [Config] config
39
+ # the config object
40
+ # @param [Node::Master] master
41
+ # the master node
42
+ #
43
+ def initialize(config, master)
44
+ unless config.is_a?(Config)
45
+ raise ArgumentError, "#{config.class} is not an Falcore::Config"
46
+ end
47
+
48
+ unless master.is_a?(Node::Master)
49
+ raise ArgumenError, "#{config.class} is not an Falcore::Node::Master"
50
+ end
51
+
52
+ @config = config
53
+ @master = master
54
+ end
55
+
56
+ #
57
+ # Run this dumper. This method should be overridden in subclasses.
58
+ #
59
+ def run
60
+ instance_eval(&self.class.validate)
61
+ instance_eval(&self.class.run)
62
+ end
63
+
64
+ private
65
+
66
+ #
67
+ # Ensure the thing called in the block is not +nil+.
68
+ #
69
+ # @param [String] thing
70
+ # the thing to check (used for the error message)
71
+ # @param [Proc] block
72
+ # the block to call
73
+ #
74
+ # @return [true]
75
+ #
76
+ def presence!(thing, &block)
77
+ if block.call(self).nil?
78
+ raise "Expected '#{thing}' to be set!"
79
+ end
80
+
81
+ true
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,76 @@
1
+ #
2
+ # Author: Seth Vargo <sethvargo@gmail.com>
3
+ #
4
+ # Copyright 2014 Chef Software, Inc.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'statsd'
20
+
21
+ module Falcore
22
+ class Dumper::Statsd < Dumper::Base
23
+ validate do
24
+ presence!('Statsd host') { config.statsd.host }
25
+ presence!('Statsd port') { config.statsd.port }
26
+ end
27
+
28
+ run do
29
+ stat(master)
30
+ master.slaves.each(&method(:stat))
31
+ end
32
+
33
+ private
34
+
35
+ def statsd
36
+ @statsd ||= Statsd.new(config.statsd.host, config.statsd.port)
37
+ end
38
+
39
+ #
40
+ # @private
41
+ #
42
+ # Push stats to statsd for this particular +node+.
43
+ #
44
+ # @param [Node::Base] node
45
+ # the node to examine
46
+ #
47
+ def stat(node)
48
+ # Up/down
49
+ statsd.gauge("#{node.id}.offline", node.offline? ? 1 : 0)
50
+
51
+ # Idle
52
+ statsd.gauge("#{node.id}.idle", node.idle? ? 1 : 0)
53
+
54
+ # Response time
55
+ statsd.timing("#{node.id}.response_time", node.response_time)
56
+
57
+ # Temporary space
58
+ statsd.gauge("#{node.id}.temporary_space", node.temporary_space)
59
+
60
+ # Disk space
61
+ statsd.gauge("#{node.id}.disk_space", node.disk_space)
62
+
63
+ # Free memory
64
+ statsd.gauge("#{node.id}.free_memory", node.free_memory)
65
+
66
+ # Total memory
67
+ statsd.gauge("#{node.id}.total_memory", node.total_memory)
68
+
69
+ # Free swap
70
+ statsd.gauge("#{node.id}.free_swap", node.free_swap)
71
+
72
+ # Total swap
73
+ statsd.gauge("#{node.id}.total_swap", node.total_swap)
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,50 @@
1
+ #
2
+ # Author: Seth Vargo <sethvargo@gmail.com>
3
+ #
4
+ # Copyright 2014 Chef Software, Inc.
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'json'
20
+ require 'open-uri'
21
+ require 'socket'
22
+
23
+ module Falcore
24
+ class Fetcher
25
+ class << self
26
+ #
27
+ # Get the JSON at the given URL and parse it as such. This method is just
28
+ # a very thin wrapper around Ruby's native +OpenURI+ with some error-
29
+ # handling magic.
30
+ #
31
+ # @raise [RuntimeError]
32
+ # if the request fails (40X, 50X, bad-URL) or if the JSON is invalid
33
+ #
34
+ # @param [String] url
35
+ # the url to get
36
+ #
37
+ # @return [Hash]
38
+ # the parsed JSON hash
39
+ #
40
+ def get(url)
41
+ response = open(url)
42
+ JSON.parse(response.read)
43
+ rescue Errno::ENOENT, OpenURI::HTTPError, SocketError => e
44
+ raise "Failed to GET '#{url}': #{e.class} - #{e.message}"
45
+ rescue JSON::ParserError
46
+ raise 'Invalid JSON!'
47
+ end
48
+ end
49
+ end
50
+ end