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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.travis.yml +19 -0
- data/Gemfile +6 -0
- data/LICENSE +201 -0
- data/README.md +98 -0
- data/Rakefile +12 -0
- data/bin/falcore +78 -0
- data/falcore.gemspec +34 -0
- data/lib/falcore.rb +38 -0
- data/lib/falcore/aggregator.rb +41 -0
- data/lib/falcore/config.rb +125 -0
- data/lib/falcore/dumpers/base.rb +84 -0
- data/lib/falcore/dumpers/statsd.rb +76 -0
- data/lib/falcore/fetcher.rb +50 -0
- data/lib/falcore/nodes/base.rb +118 -0
- data/lib/falcore/nodes/master.rb +48 -0
- data/lib/falcore/nodes/slave.rb +48 -0
- data/lib/falcore/null_object.rb +56 -0
- data/lib/falcore/util.rb +39 -0
- data/lib/falcore/version.rb +21 -0
- data/spec/fixtures/computer.json +180 -0
- data/spec/functional/dumpers/statsd_spec.rb +44 -0
- data/spec/functional/fetcher_spec.rb +39 -0
- data/spec/spec_helper.rb +34 -0
- data/spec/support/server.rb +14 -0
- data/spec/support/statsd.rb +129 -0
- data/spec/unit/aggregator_spec.rb +33 -0
- data/spec/unit/config_spec.rb +55 -0
- data/spec/unit/nodes/base_spec.rb +329 -0
- data/spec/unit/nodes/master_spec.rb +37 -0
- data/spec/unit/nodes/slave_spec.rb +25 -0
- data/spec/unit/util_spec.rb +29 -0
- metadata +176 -0
data/falcore.gemspec
ADDED
|
@@ -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
|
data/lib/falcore.rb
ADDED
|
@@ -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
|