es-scout 5.3.0.es1
Sign up to get free protection for your applications and to get access to all the features.
- data/AUTHORS +4 -0
- data/CHANGELOG +225 -0
- data/COPYING +340 -0
- data/INSTALL +18 -0
- data/LICENSE +6 -0
- data/README +66 -0
- data/Rakefile +74 -0
- data/TODO +6 -0
- data/bin/es-scout +10 -0
- data/data/cacert.pem +3154 -0
- data/data/code_key.pub +13 -0
- data/data/gpl-2.0.txt +339 -0
- data/data/lgpl-2.1.txt +504 -0
- data/lib/es-scout/command/install.rb +68 -0
- data/lib/es-scout/command/run.rb +56 -0
- data/lib/es-scout/command/test.rb +62 -0
- data/lib/es-scout/command/troubleshoot.rb +142 -0
- data/lib/es-scout/command.rb +258 -0
- data/lib/es-scout/plugin.rb +237 -0
- data/lib/es-scout/plugin_options.rb +80 -0
- data/lib/es-scout/scout_logger.rb +19 -0
- data/lib/es-scout/server.rb +578 -0
- data/lib/es-scout.rb +11 -0
- data/vendor/json_pure/CHANGES +162 -0
- data/vendor/json_pure/COPYING +58 -0
- data/vendor/json_pure/GPL +340 -0
- data/vendor/json_pure/README +358 -0
- data/vendor/json_pure/Rakefile +292 -0
- data/vendor/json_pure/TODO +1 -0
- data/vendor/json_pure/VERSION +1 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkComparison.log +52 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_fast-autocorrelation.dat +1000 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_fast.dat +1001 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_pretty-autocorrelation.dat +900 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_pretty.dat +901 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_safe-autocorrelation.dat +1000 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_safe.dat +1001 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt.log +261 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_fast-autocorrelation.dat +1000 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_fast.dat +1001 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_pretty-autocorrelation.dat +1000 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_pretty.dat +1001 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_safe-autocorrelation.dat +1000 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_safe.dat +1001 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure.log +262 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkRails#generator-autocorrelation.dat +1000 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkRails#generator.dat +1001 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkRails.log +82 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkComparison.log +34 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkExt#parser-autocorrelation.dat +900 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkExt#parser.dat +901 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkExt.log +81 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkPure#parser-autocorrelation.dat +1000 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkPure#parser.dat +1001 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkPure.log +82 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkRails#parser-autocorrelation.dat +1000 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkRails#parser.dat +1001 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkRails.log +82 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkYAML#parser-autocorrelation.dat +1000 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkYAML#parser.dat +1001 -0
- data/vendor/json_pure/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkYAML.log +82 -0
- data/vendor/json_pure/benchmarks/generator2_benchmark.rb +222 -0
- data/vendor/json_pure/benchmarks/generator_benchmark.rb +224 -0
- data/vendor/json_pure/benchmarks/ohai.json +1216 -0
- data/vendor/json_pure/benchmarks/ohai.ruby +1 -0
- data/vendor/json_pure/benchmarks/parser2_benchmark.rb +251 -0
- data/vendor/json_pure/benchmarks/parser_benchmark.rb +259 -0
- data/vendor/json_pure/bin/edit_json.rb +9 -0
- data/vendor/json_pure/bin/prettify_json.rb +75 -0
- data/vendor/json_pure/data/example.json +1 -0
- data/vendor/json_pure/data/index.html +38 -0
- data/vendor/json_pure/data/prototype.js +4184 -0
- data/vendor/json_pure/ext/json/ext/generator/extconf.rb +16 -0
- data/vendor/json_pure/ext/json/ext/generator/generator.c +1341 -0
- data/vendor/json_pure/ext/json/ext/generator/generator.h +170 -0
- data/vendor/json_pure/ext/json/ext/parser/extconf.rb +15 -0
- data/vendor/json_pure/ext/json/ext/parser/parser.c +1935 -0
- data/vendor/json_pure/ext/json/ext/parser/parser.h +71 -0
- data/vendor/json_pure/ext/json/ext/parser/parser.rl +792 -0
- data/vendor/json_pure/install.rb +26 -0
- data/vendor/json_pure/lib/json/Array.xpm +21 -0
- data/vendor/json_pure/lib/json/FalseClass.xpm +21 -0
- data/vendor/json_pure/lib/json/Hash.xpm +21 -0
- data/vendor/json_pure/lib/json/Key.xpm +73 -0
- data/vendor/json_pure/lib/json/NilClass.xpm +21 -0
- data/vendor/json_pure/lib/json/Numeric.xpm +28 -0
- data/vendor/json_pure/lib/json/String.xpm +96 -0
- data/vendor/json_pure/lib/json/TrueClass.xpm +21 -0
- data/vendor/json_pure/lib/json/add/core.rb +148 -0
- data/vendor/json_pure/lib/json/add/rails.rb +58 -0
- data/vendor/json_pure/lib/json/common.rb +397 -0
- data/vendor/json_pure/lib/json/editor.rb +1371 -0
- data/vendor/json_pure/lib/json/ext.rb +15 -0
- data/vendor/json_pure/lib/json/json.xpm +1499 -0
- data/vendor/json_pure/lib/json/pure/generator.rb +452 -0
- data/vendor/json_pure/lib/json/pure/parser.rb +307 -0
- data/vendor/json_pure/lib/json/pure.rb +77 -0
- data/vendor/json_pure/lib/json/version.rb +8 -0
- data/vendor/json_pure/lib/json.rb +10 -0
- data/vendor/json_pure/tests/fixtures/fail1.json +1 -0
- data/vendor/json_pure/tests/fixtures/fail10.json +1 -0
- data/vendor/json_pure/tests/fixtures/fail11.json +1 -0
- data/vendor/json_pure/tests/fixtures/fail12.json +1 -0
- data/vendor/json_pure/tests/fixtures/fail13.json +1 -0
- data/vendor/json_pure/tests/fixtures/fail14.json +1 -0
- data/vendor/json_pure/tests/fixtures/fail18.json +1 -0
- data/vendor/json_pure/tests/fixtures/fail19.json +1 -0
- data/vendor/json_pure/tests/fixtures/fail2.json +1 -0
- data/vendor/json_pure/tests/fixtures/fail20.json +1 -0
- data/vendor/json_pure/tests/fixtures/fail21.json +1 -0
- data/vendor/json_pure/tests/fixtures/fail22.json +1 -0
- data/vendor/json_pure/tests/fixtures/fail23.json +1 -0
- data/vendor/json_pure/tests/fixtures/fail24.json +1 -0
- data/vendor/json_pure/tests/fixtures/fail25.json +1 -0
- data/vendor/json_pure/tests/fixtures/fail27.json +2 -0
- data/vendor/json_pure/tests/fixtures/fail28.json +2 -0
- data/vendor/json_pure/tests/fixtures/fail3.json +1 -0
- data/vendor/json_pure/tests/fixtures/fail4.json +1 -0
- data/vendor/json_pure/tests/fixtures/fail5.json +1 -0
- data/vendor/json_pure/tests/fixtures/fail6.json +1 -0
- data/vendor/json_pure/tests/fixtures/fail7.json +1 -0
- data/vendor/json_pure/tests/fixtures/fail8.json +1 -0
- data/vendor/json_pure/tests/fixtures/fail9.json +1 -0
- data/vendor/json_pure/tests/fixtures/pass1.json +56 -0
- data/vendor/json_pure/tests/fixtures/pass15.json +1 -0
- data/vendor/json_pure/tests/fixtures/pass16.json +1 -0
- data/vendor/json_pure/tests/fixtures/pass17.json +1 -0
- data/vendor/json_pure/tests/fixtures/pass2.json +1 -0
- data/vendor/json_pure/tests/fixtures/pass26.json +1 -0
- data/vendor/json_pure/tests/fixtures/pass3.json +6 -0
- data/vendor/json_pure/tests/test_json.rb +340 -0
- data/vendor/json_pure/tests/test_json_addition.rb +162 -0
- data/vendor/json_pure/tests/test_json_encoding.rb +68 -0
- data/vendor/json_pure/tests/test_json_fixtures.rb +34 -0
- data/vendor/json_pure/tests/test_json_generate.rb +122 -0
- data/vendor/json_pure/tests/test_json_rails.rb +144 -0
- data/vendor/json_pure/tests/test_json_unicode.rb +76 -0
- data/vendor/json_pure/tools/fuzz.rb +139 -0
- data/vendor/json_pure/tools/server.rb +61 -0
- metadata +233 -0
@@ -0,0 +1,80 @@
|
|
1
|
+
#!/usr/bin/env ruby -wKU
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module Scout
|
6
|
+
# a data structure of an individual plugin option
|
7
|
+
class PluginOption
|
8
|
+
attr_reader :name, :notes, :default, :advanced, :password, :required
|
9
|
+
def initialize(name, h)
|
10
|
+
@name=name
|
11
|
+
@notes=h['notes'] || ''
|
12
|
+
@default=h['default'] || ''
|
13
|
+
@attributes=h['attributes'] || ''
|
14
|
+
@advanced = @attributes.include?('advanced')
|
15
|
+
@password = @attributes.include?('password')
|
16
|
+
@required = @attributes.include?('required')
|
17
|
+
end
|
18
|
+
|
19
|
+
# convenience -- for nicer syntax
|
20
|
+
def advanced?; @advanced; end
|
21
|
+
def password?; @password; end
|
22
|
+
def required?; @required; end
|
23
|
+
def has_default?; default != '';end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
required_string = required? ? " (required). " : ""
|
27
|
+
default_string = default == '' ? '' : " Default: #{default}. "
|
28
|
+
"'#{name}'#{required_string}#{default_string}#{notes}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# A collection of pluginOption
|
33
|
+
# Create: opts=PluginOptions.from_yaml(yaml_string)
|
34
|
+
# Check if there were any problems -- opts.error -- should be nil.
|
35
|
+
#
|
36
|
+
# A valid options yaml looks like this:
|
37
|
+
# max_swap_used:
|
38
|
+
# notes: If swap is larger than this amount, an alert is generated. Amount should be in MB.
|
39
|
+
# default: 2048 # 2 GB
|
40
|
+
# max_swap_ratio:
|
41
|
+
# notes: If swap used over memory used is larger than this amount, an alert is generated
|
42
|
+
# default: 3
|
43
|
+
# attributes: required advanced
|
44
|
+
class PluginOptions < Array
|
45
|
+
|
46
|
+
attr_accessor :error
|
47
|
+
|
48
|
+
# Should be valid YAML, a hash of hashes ... if not, will be caught in the rescue below
|
49
|
+
def self.from_yaml(string)
|
50
|
+
options_array=[]
|
51
|
+
error=nil
|
52
|
+
|
53
|
+
items=YAML.load(string)
|
54
|
+
items.each_pair {|name, hash| options_array.push(PluginOption.new(name,hash)) }
|
55
|
+
rescue
|
56
|
+
error="Invalid Plugin Options"
|
57
|
+
ensure
|
58
|
+
res=PluginOptions.new(options_array)
|
59
|
+
res.error=error
|
60
|
+
return res
|
61
|
+
end
|
62
|
+
|
63
|
+
def advanced
|
64
|
+
select{|o|o.advanced? }
|
65
|
+
end
|
66
|
+
|
67
|
+
def regular
|
68
|
+
select{|o|!o.advanced? }
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_s
|
72
|
+
res=[]
|
73
|
+
each_with_index do |opt,i|
|
74
|
+
res.push "#{i+1}. #{opt.to_s}"
|
75
|
+
end
|
76
|
+
res.join("\n")
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# We use this subclass of Logger in the Scout Agent so we can retrieve all the logged messages at the end of the run.
|
2
|
+
# This works well only because the Agent is not a long-running process.
|
3
|
+
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
class ScoutLogger < Logger
|
7
|
+
attr_reader :messages
|
8
|
+
|
9
|
+
def initialize(*args)
|
10
|
+
@messages=[]
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
# this is the method that ultimately gets called whenever you invoke info, debug, etc.
|
15
|
+
def add(severity, message=nil, progname = nil, &block)
|
16
|
+
@messages << "[#{Time.now.strftime('%Y-%m-%d %H:%M:%S ')} ##{$$}] : #{progname}"
|
17
|
+
super
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,578 @@
|
|
1
|
+
#!/usr/bin/env ruby -wKU
|
2
|
+
|
3
|
+
require "net/https"
|
4
|
+
require "uri"
|
5
|
+
require "yaml"
|
6
|
+
require "timeout"
|
7
|
+
require "stringio"
|
8
|
+
require "zlib"
|
9
|
+
require "socket"
|
10
|
+
require "base64"
|
11
|
+
|
12
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__), *%w[.. .. vendor json_pure lib])
|
13
|
+
require "json"
|
14
|
+
|
15
|
+
module Scout
|
16
|
+
class Server
|
17
|
+
# A new class for plugin Timeout errors.
|
18
|
+
class PluginTimeoutError < RuntimeError; end
|
19
|
+
# A new class for API Timeout errors.
|
20
|
+
class APITimeoutError < RuntimeError; end
|
21
|
+
|
22
|
+
# Headers passed up with all API requests.
|
23
|
+
HTTP_HEADERS = { "Client-Version" => Scout::VERSION,
|
24
|
+
"Client-Hostname" => Socket.gethostname,
|
25
|
+
"Accept-Encoding" => "gzip" }
|
26
|
+
|
27
|
+
#
|
28
|
+
# A plugin cannot take more than DEFAULT_PLUGIN_TIMEOUT seconds to execute,
|
29
|
+
# otherwise, a timeout error is generated. This can be overriden by
|
30
|
+
# individual plugins.
|
31
|
+
#
|
32
|
+
DEFAULT_PLUGIN_TIMEOUT = 60
|
33
|
+
#
|
34
|
+
# A fuzzy range of seconds in which it is okay to rerun a plugin.
|
35
|
+
# We consider the interval close enough at this point.
|
36
|
+
#
|
37
|
+
RUN_DELTA = 30
|
38
|
+
|
39
|
+
attr_reader :new_plan
|
40
|
+
attr_reader :directives
|
41
|
+
attr_reader :plugin_config
|
42
|
+
|
43
|
+
# Creates a new Scout Server connection.
|
44
|
+
def initialize(server, client_key, history_file, logger = nil)
|
45
|
+
@server = server
|
46
|
+
@client_key = client_key
|
47
|
+
@history_file = history_file
|
48
|
+
@history = Hash.new
|
49
|
+
@logger = logger
|
50
|
+
@plugin_plan = []
|
51
|
+
@directives = {} # take_snapshots, interval, sleep_interval
|
52
|
+
@new_plan = false
|
53
|
+
@local_plugin_path = File.dirname(history_file) # just put overrides and ad-hoc plugins in same directory as history file.
|
54
|
+
@plugin_config_path = File.join(@local_plugin_path, "plugins.properties")
|
55
|
+
@plugin_config = load_plugin_configs(@plugin_config_path)
|
56
|
+
|
57
|
+
# the block is only passed for install and test, since we split plan retrieval outside the lockfile for run
|
58
|
+
if block_given?
|
59
|
+
load_history
|
60
|
+
yield self
|
61
|
+
save_history
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def refresh?
|
66
|
+
return true if !ping_key
|
67
|
+
|
68
|
+
url=URI.join( @server.sub("https://","http://"), "/clients/#{ping_key}/ping.scout")
|
69
|
+
|
70
|
+
headers = {"x-scout-tty" => ($stdin.tty? ? 'true' : 'false')}
|
71
|
+
if @history["plan_last_modified"] and @history["old_plugins"]
|
72
|
+
headers["If-Modified-Since"] = @history["plan_last_modified"]
|
73
|
+
end
|
74
|
+
get(url, "Could not ping #{url} for refresh info", headers) do |res|
|
75
|
+
if res.is_a?(Net::HTTPNotModified)
|
76
|
+
return false
|
77
|
+
else
|
78
|
+
info "Plan has been modified!"
|
79
|
+
return true
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
#
|
86
|
+
# Retrieves the Plugin Plan from the server. This is the list of plugins
|
87
|
+
# to execute, along with all options.
|
88
|
+
#
|
89
|
+
# This method has a couple of side effects:
|
90
|
+
# 1) it sets the @plugin_plan with either A) whatever is in history, B) the results of the /plan retrieval
|
91
|
+
# 2) it sets @checkin_to = true IF so directed by the scout server
|
92
|
+
def fetch_plan
|
93
|
+
if refresh?
|
94
|
+
|
95
|
+
url = urlify(:plan)
|
96
|
+
info "Fetching plan from server at #{url}..."
|
97
|
+
headers = {"x-scout-tty" => ($stdin.tty? ? 'true' : 'false')}
|
98
|
+
|
99
|
+
get(url, "Could not retrieve plan from server.", headers) do |res|
|
100
|
+
begin
|
101
|
+
body = res.body
|
102
|
+
if res["Content-Encoding"] == "gzip" and body and not body.empty?
|
103
|
+
body = Zlib::GzipReader.new(StringIO.new(body)).read
|
104
|
+
end
|
105
|
+
|
106
|
+
body_as_hash = JSON.parse(body)
|
107
|
+
|
108
|
+
# Ensure all the plugins in the new plan are properly signed. Load the public key for this.
|
109
|
+
public_key_text = File.read(File.join( File.dirname(__FILE__), *%w[.. .. data code_key.pub] ))
|
110
|
+
debug "Loaded public key used for verifying code signatures (#{public_key_text.size} bytes)"
|
111
|
+
code_public_key = OpenSSL::PKey::RSA.new(public_key_text)
|
112
|
+
|
113
|
+
temp_plugins=Array(body_as_hash["plugins"])
|
114
|
+
plugin_signature_error = false
|
115
|
+
temp_plugins.each do |plugin|
|
116
|
+
signature=plugin['signature']
|
117
|
+
id_and_name = "#{plugin['id']}-#{plugin['name']}".sub(/\A-/, "")
|
118
|
+
if signature
|
119
|
+
code=plugin['code'].gsub(/ +$/,'') # we strip trailing whitespace before calculating signatures. Same here.
|
120
|
+
decoded_signature=Base64.decode64(signature)
|
121
|
+
if !code_public_key.verify(OpenSSL::Digest::SHA1.new, decoded_signature, code)
|
122
|
+
info "#{id_and_name} signature doesn't match!"
|
123
|
+
plugin_signature_error=true
|
124
|
+
end
|
125
|
+
else
|
126
|
+
info "#{id_and_name} has no signature!"
|
127
|
+
plugin_signature_error=true
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
if(!plugin_signature_error)
|
133
|
+
@plugin_plan = temp_plugins
|
134
|
+
@directives = body_as_hash["directives"].is_a?(Hash) ? body_as_hash["directives"] : Hash.new
|
135
|
+
@history["plan_last_modified"] = res["last-modified"]
|
136
|
+
@history["old_plugins"] = @plugin_plan.clone # important that the plan is cloned -- we're going to add local plugins, and they shouldn't go into history
|
137
|
+
@history["directives"] = @directives
|
138
|
+
|
139
|
+
info "Plan loaded. (#{@plugin_plan.size} plugins: " +
|
140
|
+
"#{@plugin_plan.map { |p| p['name'] }.join(', ')})" +
|
141
|
+
". Directives: #{@directives.to_a.map{|a| "#{a.first}:#{a.last}"}.join(", ")}"
|
142
|
+
|
143
|
+
@new_plan = true # used in determination if we should checkin this time or not
|
144
|
+
else
|
145
|
+
info "There was a problem with plugin signatures. Reusing old plan."
|
146
|
+
@plugin_plan = Array(@history["old_plugins"])
|
147
|
+
@directives = @history["directives"] || Hash.new
|
148
|
+
end
|
149
|
+
|
150
|
+
# Add local plugins to the plan. Note that local plugins are NOT saved to history file
|
151
|
+
@plugin_plan += get_local_plugins
|
152
|
+
rescue Exception =>e
|
153
|
+
fatal "Plan from server was malformed: #{e.message} - #{e.backtrace}"
|
154
|
+
exit
|
155
|
+
end
|
156
|
+
end
|
157
|
+
else
|
158
|
+
info "Plan not modified."
|
159
|
+
@plugin_plan = Array(@history["old_plugins"])
|
160
|
+
@plugin_plan += get_local_plugins
|
161
|
+
@directives = @history["directives"] || Hash.new
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# returns an array of hashes representing local plugins found on the filesystem
|
166
|
+
# The glob pattern requires that filenames begin with a letter,
|
167
|
+
# which excludes plugin overrides (like 12345.rb)
|
168
|
+
def get_local_plugins
|
169
|
+
local_plugin_paths=Dir.glob(File.join(@local_plugin_path,"[a-zA-Z]*.rb"))
|
170
|
+
local_plugin_paths.map do |plugin_path|
|
171
|
+
begin
|
172
|
+
{
|
173
|
+
'name' => File.basename(plugin_path),
|
174
|
+
'local_filename' => File.basename(plugin_path),
|
175
|
+
'origin' => 'LOCAL',
|
176
|
+
'code' => File.read(plugin_path),
|
177
|
+
'interval' => 0
|
178
|
+
}
|
179
|
+
rescue => e
|
180
|
+
info "Error trying to read local plugin: #{plugin_path} -- #{e.backtrace.join('\n')}"
|
181
|
+
nil
|
182
|
+
end
|
183
|
+
end.compact
|
184
|
+
end
|
185
|
+
|
186
|
+
# To distribute pings across a longer timeframe, the agent will sleep for a given
|
187
|
+
# amount of time. When using the --force option the sleep_interval is ignored.
|
188
|
+
def sleep_interval
|
189
|
+
(@history['directives'] || {})['sleep_interval'].to_f
|
190
|
+
end
|
191
|
+
|
192
|
+
def ping_key
|
193
|
+
(@history['directives'] || {})['ping_key']
|
194
|
+
end
|
195
|
+
|
196
|
+
# uses values from history and current time to determine if we should checkin at this time
|
197
|
+
def time_to_checkin?
|
198
|
+
@history['last_checkin'] == nil ||
|
199
|
+
@directives['interval'] == nil ||
|
200
|
+
(Time.now.to_i - Time.at(@history['last_checkin']).to_i).abs+15 > @directives['interval'].to_i*60
|
201
|
+
rescue
|
202
|
+
debug "Failed to calculate time_to_checkin. @history['last_checkin']=#{@history['last_checkin']}. "+
|
203
|
+
"@directives['interval']=#{@directives['interval']}. Time.now.to_i=#{Time.now.to_i}"
|
204
|
+
return true
|
205
|
+
end
|
206
|
+
|
207
|
+
# uses values from history and current time to determine if we should ping the server at this time
|
208
|
+
def time_to_ping?
|
209
|
+
return true if
|
210
|
+
@history['last_ping'] == nil ||
|
211
|
+
@directives['ping_interval'] == nil ||
|
212
|
+
(Time.now.to_i - Time.at(@history['last_ping']).to_i).abs+15 > @directives['ping_interval'].to_i*60
|
213
|
+
rescue
|
214
|
+
debug "Failed to calculate time_to_ping. @history['last_ping']=#{@history['last_ping']}. "+
|
215
|
+
"@directives['ping_interval']=#{@directives['ping_interval']}. Time.now.to_i=#{Time.now.to_i}"
|
216
|
+
return true
|
217
|
+
end
|
218
|
+
|
219
|
+
# returns a human-readable representation of the next checkin, i.e., 5min 30sec
|
220
|
+
def next_checkin
|
221
|
+
secs= @directives['interval'].to_i*60 - (Time.now.to_i - Time.at(@history['last_checkin']).to_i).abs
|
222
|
+
minutes=(secs.to_f/60).floor
|
223
|
+
secs=secs%60
|
224
|
+
"#{minutes}min #{secs} sec"
|
225
|
+
rescue
|
226
|
+
"[next scout invocation]"
|
227
|
+
end
|
228
|
+
|
229
|
+
# Runs all plugins from the given plan. Calls process_plugin on each plugin.
|
230
|
+
# @plugin_execution_plan is populated by calling fetch_plan
|
231
|
+
def run_plugins_by_plan
|
232
|
+
prepare_checkin
|
233
|
+
@plugin_plan.each do |plugin|
|
234
|
+
begin
|
235
|
+
process_plugin(plugin)
|
236
|
+
rescue Exception
|
237
|
+
@checkin[:errors] << build_report(
|
238
|
+
plugin,
|
239
|
+
:subject => "Exception: #{$!.message}.",
|
240
|
+
:body => $!.backtrace
|
241
|
+
)
|
242
|
+
error("Encountered an error: #{$!.message}")
|
243
|
+
puts $!.backtrace.join('\n')
|
244
|
+
end
|
245
|
+
end
|
246
|
+
take_snapshot if @directives['take_snapshots']
|
247
|
+
checkin
|
248
|
+
end
|
249
|
+
|
250
|
+
#
|
251
|
+
# This is the heart of Scout.
|
252
|
+
#
|
253
|
+
# First, it determines if a plugin is past interval and needs to be run.
|
254
|
+
# If it is, it simply evals the code, compiling it.
|
255
|
+
# It then loads the plugin and runs it with a PLUGIN_TIMEOUT time limit.
|
256
|
+
# The plugin generates data, alerts, and errors. In addition, it will
|
257
|
+
# set memory and last_run information in the history file.
|
258
|
+
#
|
259
|
+
# The plugin argument is a hash with keys: id, name, code, timeout, options, signature.
|
260
|
+
def process_plugin(plugin)
|
261
|
+
info "Processing the '#{plugin['name']}' plugin:"
|
262
|
+
id_and_name = "#{plugin['id']}-#{plugin['name']}".sub(/\A-/, "")
|
263
|
+
plugin_id = plugin['id']
|
264
|
+
last_run = @history["last_runs"][id_and_name] ||
|
265
|
+
@history["last_runs"][plugin['name']]
|
266
|
+
memory = @history["memory"][id_and_name] ||
|
267
|
+
@history["memory"][plugin['name']]
|
268
|
+
run_time = Time.now
|
269
|
+
delta = last_run.nil? ? nil : run_time -
|
270
|
+
(last_run + plugin['interval'] * 60)
|
271
|
+
if last_run.nil? or delta.between?(-RUN_DELTA, 0) or delta >= 0
|
272
|
+
debug "Plugin is past interval and needs to be run. " +
|
273
|
+
"(last run: #{last_run || 'nil'})"
|
274
|
+
code_to_run = plugin['code']
|
275
|
+
if plugin_id && plugin_id != ""
|
276
|
+
override_path=File.join(@local_plugin_path, "#{plugin_id}.rb")
|
277
|
+
# debug "Checking for local plugin override file at #{override_path}"
|
278
|
+
if File.exist?(override_path)
|
279
|
+
code_to_run = File.read(override_path)
|
280
|
+
debug "Override file found - Using #{code_to_run.size} chars of code in #{override_path} for plugin id=#{plugin_id}"
|
281
|
+
plugin['origin'] = "OVERRIDE"
|
282
|
+
else
|
283
|
+
plugin['origin'] = nil
|
284
|
+
end
|
285
|
+
end
|
286
|
+
debug "Compiling plugin..."
|
287
|
+
begin
|
288
|
+
eval( code_to_run,
|
289
|
+
TOPLEVEL_BINDING,
|
290
|
+
plugin['path'] || plugin['name'] )
|
291
|
+
info "Plugin compiled."
|
292
|
+
rescue Exception
|
293
|
+
raise if $!.is_a? SystemExit
|
294
|
+
error "Plugin would not compile: #{$!.message}"
|
295
|
+
@checkin[:errors] << build_report(plugin,:subject => "Plugin would not compile", :body=>"#{$!.message}\n\n#{$!.backtrace}")
|
296
|
+
return
|
297
|
+
end
|
298
|
+
|
299
|
+
# Lookup any local options in plugin_config.properies as needed
|
300
|
+
options=(plugin['options'] || Hash.new)
|
301
|
+
options.each_pair do |k,v|
|
302
|
+
if v=~/^lookup:(.+)$/
|
303
|
+
lookup_key = $1.strip
|
304
|
+
if plugin_config[lookup_key]
|
305
|
+
options[k]=plugin_config[lookup_key]
|
306
|
+
else
|
307
|
+
info "Plugin #{id_and_name}: option #{k} appears to be a lookup, but we can't find #{lookup_key} in #{@plugin_config_path}"
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
|
313
|
+
debug "Loading plugin..."
|
314
|
+
if job = Plugin.last_defined.load( last_run, (memory || Hash.new), options)
|
315
|
+
info "Plugin loaded."
|
316
|
+
debug "Running plugin..."
|
317
|
+
begin
|
318
|
+
data = {}
|
319
|
+
timeout = plugin['timeout'].to_i
|
320
|
+
timeout = DEFAULT_PLUGIN_TIMEOUT unless timeout > 0
|
321
|
+
Timeout.timeout(timeout, PluginTimeoutError) do
|
322
|
+
data = job.run
|
323
|
+
end
|
324
|
+
rescue Timeout::Error, PluginTimeoutError
|
325
|
+
error "Plugin took too long to run."
|
326
|
+
@checkin[:errors] << build_report(plugin,
|
327
|
+
:subject => "Plugin took too long to run",
|
328
|
+
:body=>"Execution timed out.")
|
329
|
+
return
|
330
|
+
rescue Exception
|
331
|
+
raise if $!.is_a? SystemExit
|
332
|
+
error "Plugin failed to run: #{$!.class}: #{$!.message}\n" +
|
333
|
+
"#{$!.backtrace.join("\n")}"
|
334
|
+
@checkin[:errors] << build_report(plugin,
|
335
|
+
:subject => "Plugin failed to run",
|
336
|
+
:body=>"#{$!.class}: #{$!.message}\n#{$!.backtrace.join("\n")}")
|
337
|
+
end
|
338
|
+
info "Plugin completed its run."
|
339
|
+
|
340
|
+
%w[report alert error summary].each do |type|
|
341
|
+
plural = "#{type}s".sub(/ys\z/, "ies").to_sym
|
342
|
+
reports = data[plural].is_a?(Array) ? data[plural] :
|
343
|
+
[data[plural]].compact
|
344
|
+
if report = data[type.to_sym]
|
345
|
+
reports << report
|
346
|
+
end
|
347
|
+
reports.each do |fields|
|
348
|
+
@checkin[plural] << build_report(plugin, fields)
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
@history["last_runs"].delete(plugin['name'])
|
353
|
+
@history["memory"].delete(plugin['name'])
|
354
|
+
@history["last_runs"][id_and_name] = run_time
|
355
|
+
@history["memory"][id_and_name] = data[:memory]
|
356
|
+
else
|
357
|
+
@checkin[:errors] << build_report(
|
358
|
+
plugin,
|
359
|
+
:subject => "Plugin would not load."
|
360
|
+
)
|
361
|
+
end
|
362
|
+
else
|
363
|
+
debug "Plugin does not need to be run at this time. " +
|
364
|
+
"(last run: #{last_run || 'nil'})"
|
365
|
+
end
|
366
|
+
data
|
367
|
+
ensure
|
368
|
+
if Plugin.last_defined
|
369
|
+
debug "Removing plugin code..."
|
370
|
+
begin
|
371
|
+
Object.send(:remove_const, Plugin.last_defined.to_s.split("::").first)
|
372
|
+
Plugin.last_defined = nil
|
373
|
+
info "Plugin Removed."
|
374
|
+
rescue
|
375
|
+
raise if $!.is_a? SystemExit
|
376
|
+
error "Unable to remove plugin."
|
377
|
+
end
|
378
|
+
end
|
379
|
+
info "Plugin '#{plugin['name']}' processing complete."
|
380
|
+
end
|
381
|
+
|
382
|
+
|
383
|
+
# captures a list of processes running at this moment
|
384
|
+
def take_snapshot
|
385
|
+
info "Taking a process snapshot"
|
386
|
+
ps=%x(ps aux).split("\n")[1..-1].join("\n") # get rid of the header line
|
387
|
+
@checkin[:snapshot]=ps
|
388
|
+
rescue Exception
|
389
|
+
error "unable to capture processes on this server. #{$!.message}"
|
390
|
+
return nil
|
391
|
+
end
|
392
|
+
|
393
|
+
# Prepares a check-in data structure to hold Plugin generated data.
|
394
|
+
def prepare_checkin
|
395
|
+
@checkin = { :reports => Array.new,
|
396
|
+
:alerts => Array.new,
|
397
|
+
:errors => Array.new,
|
398
|
+
:summaries => Array.new,
|
399
|
+
:snapshot => '',
|
400
|
+
:config_path => File.expand_path(File.dirname(@history_file))}
|
401
|
+
end
|
402
|
+
|
403
|
+
def show_checkin(printer = :p)
|
404
|
+
send(printer, @checkin)
|
405
|
+
end
|
406
|
+
|
407
|
+
#
|
408
|
+
# Loads the history file from disk. If the file does not exist,
|
409
|
+
# it creates one.
|
410
|
+
#
|
411
|
+
def load_history
|
412
|
+
if !File.exist?(@history_file) || File.zero?(@history_file)
|
413
|
+
create_blank_history
|
414
|
+
end
|
415
|
+
debug "Loading history file..."
|
416
|
+
contents=File.read(@history_file)
|
417
|
+
begin
|
418
|
+
@history = YAML.load(contents)
|
419
|
+
rescue => e
|
420
|
+
backup_path=File.join(File.dirname(@history_file), "history.corrupt")
|
421
|
+
info "Couldn't parse the history file. Deleting it and resetting to an empty history file. Keeping a backup at #{backup_path}"
|
422
|
+
File.open(backup_path,"w"){|f|f.write contents}
|
423
|
+
File.delete(@history_file)
|
424
|
+
create_blank_history
|
425
|
+
@history = File.open(@history_file) { |file| YAML.load(file) }
|
426
|
+
end
|
427
|
+
|
428
|
+
info "History file loaded."
|
429
|
+
end
|
430
|
+
|
431
|
+
# creates a blank history file
|
432
|
+
def create_blank_history
|
433
|
+
debug "Creating empty history file..."
|
434
|
+
File.open(@history_file, "w") do |file|
|
435
|
+
YAML.dump({"last_runs" => Hash.new, "memory" => Hash.new}, file)
|
436
|
+
end
|
437
|
+
info "History file created."
|
438
|
+
end
|
439
|
+
|
440
|
+
# Saves the history file to disk.
|
441
|
+
def save_history
|
442
|
+
debug "Saving history file..."
|
443
|
+
File.open(@history_file, "w") { |file| YAML.dump(@history, file) }
|
444
|
+
info "History file saved."
|
445
|
+
end
|
446
|
+
|
447
|
+
private
|
448
|
+
|
449
|
+
def build_report(plugin_hash, fields)
|
450
|
+
{ :plugin_id => plugin_hash['id'],
|
451
|
+
:created_at => Time.now.utc.strftime("%Y-%m-%d %H:%M:%S"),
|
452
|
+
:fields => fields,
|
453
|
+
:local_filename => plugin_hash['local_filename'], # this will be nil unless it's an ad-hoc plugin
|
454
|
+
:origin => plugin_hash['origin'] # [LOCAL|OVERRIDE|nil]
|
455
|
+
}
|
456
|
+
end
|
457
|
+
|
458
|
+
def urlify(url_name, options = Hash.new)
|
459
|
+
return unless @server
|
460
|
+
options.merge!(:client_version => Scout::VERSION)
|
461
|
+
URI.join( @server,
|
462
|
+
"/clients/CLIENT_KEY/#{url_name}.scout".
|
463
|
+
gsub(/\bCLIENT_KEY\b/, @client_key).
|
464
|
+
gsub(/\b[A-Z_]+\b/) { |k| options[k.downcase.to_sym] || k } )
|
465
|
+
end
|
466
|
+
|
467
|
+
def post(url, error, body, headers = Hash.new, &response_handler)
|
468
|
+
return unless url
|
469
|
+
request(url, response_handler, error) do |connection|
|
470
|
+
post = Net::HTTP::Post.new( url.path +
|
471
|
+
(url.query ? ('?' + url.query) : ''),
|
472
|
+
HTTP_HEADERS.merge(headers) )
|
473
|
+
post.body = body
|
474
|
+
connection.request(post)
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
def get(url, error, headers = Hash.new, &response_handler)
|
479
|
+
return unless url
|
480
|
+
request(url, response_handler, error) do |connection|
|
481
|
+
connection.get( url.path + (url.query ? ('?' + url.query) : ''),
|
482
|
+
HTTP_HEADERS.merge(headers) )
|
483
|
+
end
|
484
|
+
end
|
485
|
+
|
486
|
+
def request(url, response_handler, error, &connector)
|
487
|
+
response = nil
|
488
|
+
Timeout.timeout(5 * 60, APITimeoutError) do
|
489
|
+
http = Net::HTTP.new(url.host, url.port)
|
490
|
+
if url.is_a? URI::HTTPS
|
491
|
+
http.use_ssl = true
|
492
|
+
http.ca_file = File.join( File.dirname(__FILE__),
|
493
|
+
*%w[.. .. data cacert.pem] )
|
494
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER |
|
495
|
+
OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
|
496
|
+
end
|
497
|
+
response = no_warnings { http.start(&connector) }
|
498
|
+
end
|
499
|
+
case response
|
500
|
+
when Net::HTTPSuccess, Net::HTTPNotModified
|
501
|
+
response_handler[response] unless response_handler.nil?
|
502
|
+
else
|
503
|
+
error = "Server says: #{response['x-scout-msg']}" if response['x-scout-msg']
|
504
|
+
fatal error
|
505
|
+
raise SystemExit.new(error)
|
506
|
+
end
|
507
|
+
rescue Timeout::Error
|
508
|
+
fatal "Request timed out."
|
509
|
+
exit
|
510
|
+
rescue Exception
|
511
|
+
raise if $!.is_a? SystemExit
|
512
|
+
fatal "An HTTP error occurred: #{$!.message}"
|
513
|
+
exit
|
514
|
+
end
|
515
|
+
|
516
|
+
def checkin
|
517
|
+
@history['last_checkin'] = Time.now.to_i # might have to save the time of invocation and use here to prevent drift
|
518
|
+
io = StringIO.new
|
519
|
+
gzip = Zlib::GzipWriter.new(io)
|
520
|
+
gzip << @checkin.to_json
|
521
|
+
gzip.close
|
522
|
+
post( urlify(:checkin),
|
523
|
+
"Unable to check in with the server.",
|
524
|
+
io.string,
|
525
|
+
"Content-Type" => "application/json",
|
526
|
+
"Content-Encoding" => "gzip" )
|
527
|
+
rescue Exception
|
528
|
+
error "Unable to check in with the server."
|
529
|
+
debug $!.class.to_s
|
530
|
+
debug $!.message
|
531
|
+
debug $!.backtrace
|
532
|
+
end
|
533
|
+
|
534
|
+
|
535
|
+
def no_warnings
|
536
|
+
old_verbose = $VERBOSE
|
537
|
+
$VERBOSE = false
|
538
|
+
yield
|
539
|
+
ensure
|
540
|
+
$VERBOSE = old_verbose
|
541
|
+
end
|
542
|
+
|
543
|
+
# Forward Logger methods to an active instance, when there is one.
|
544
|
+
def method_missing(meth, *args, &block)
|
545
|
+
if (Logger::SEV_LABEL - %w[ANY]).include? meth.to_s.upcase
|
546
|
+
@logger.send(meth, *args, &block) unless @logger.nil?
|
547
|
+
else
|
548
|
+
super
|
549
|
+
end
|
550
|
+
end
|
551
|
+
|
552
|
+
private
|
553
|
+
|
554
|
+
# Called during initialization; loads the plugin_configs (local plugin configurations for passwords, etc)
|
555
|
+
# if the file is there. Returns a hash like {"db.username"=>"secr3t"}
|
556
|
+
def load_plugin_configs(path)
|
557
|
+
temp_configs={}
|
558
|
+
if File.exist?(path)
|
559
|
+
debug "Loading Plugin Configs at #{path}"
|
560
|
+
begin
|
561
|
+
File.open(path,"r").read.each_line do |line|
|
562
|
+
line.strip!
|
563
|
+
next if line[0] == '#'
|
564
|
+
next unless line.include? "="
|
565
|
+
k,v =line.split('=')
|
566
|
+
temp_configs[k]=v
|
567
|
+
end
|
568
|
+
debug("#{temp_configs.size} plugin config(s) loaded.")
|
569
|
+
rescue
|
570
|
+
info "Error loading Plugin Configs at #{path}: #{$!}"
|
571
|
+
end
|
572
|
+
else
|
573
|
+
debug "No Plugin Configs at #{path}"
|
574
|
+
end
|
575
|
+
return temp_configs
|
576
|
+
end
|
577
|
+
end
|
578
|
+
end
|
data/lib/es-scout.rb
ADDED