emissary 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,60 @@
1
+ ##### BORROWED FROM ACTIVESUPPORT #####
2
+
3
+ class Object
4
+ # An object is blank if it's false, empty, or a whitespace string.
5
+ # For example, "", " ", +nil+, [], and {} are blank.
6
+ #
7
+ # This simplifies
8
+ #
9
+ # if !address.nil? && !address.empty?
10
+ #
11
+ # to
12
+ #
13
+ # if !address.blank?
14
+ def blank?
15
+ respond_to?(:empty?) ? empty? : !self
16
+ end
17
+
18
+ # An object is present if it's not blank.
19
+ def present?
20
+ !blank?
21
+ end
22
+ end
23
+
24
+ class NilClass #:nodoc:
25
+ def blank?
26
+ true
27
+ end
28
+ end
29
+
30
+ class FalseClass #:nodoc:
31
+ def blank?
32
+ true
33
+ end
34
+ end
35
+
36
+ class TrueClass #:nodoc:
37
+ def blank?
38
+ false
39
+ end
40
+ end
41
+
42
+ class Array #:nodoc:
43
+ alias_method :blank?, :empty?
44
+ end
45
+
46
+ class Hash #:nodoc:
47
+ alias_method :blank?, :empty?
48
+ end
49
+
50
+ class String #:nodoc:
51
+ def blank?
52
+ self !~ /\S/
53
+ end
54
+ end
55
+
56
+ class Numeric #:nodoc:
57
+ def blank?
58
+ false
59
+ end
60
+ end
@@ -0,0 +1,21 @@
1
+ ##### BORROWED FROM ACTIVESUPPORT #####
2
+
3
+ class Object
4
+ def try name, *args
5
+ self.__send__(name, *args) unless not self.respond_to? name
6
+ end
7
+
8
+ def __method__
9
+ caller[0] =~ /\d:in `([^']+)'/
10
+ $1.to_sym rescue nil
11
+ end
12
+
13
+ def __caller__
14
+ caller[1] =~ /\d:in `([^']+)'/
15
+ $1.to_sym rescue nil
16
+ end
17
+
18
+ def clone_deep
19
+ Marshal.load(Marshal.dump(self)) rescue self.clone
20
+ end
21
+ end
@@ -0,0 +1,33 @@
1
+ class Array
2
+ def symbolize
3
+ collect do |value|
4
+ case value
5
+ when Hash, Array
6
+ value.symbolize
7
+ else
8
+ value
9
+ end
10
+ end
11
+ end
12
+
13
+ def symbolize!
14
+ self.replace(self.symbolize)
15
+ end
16
+ end
17
+
18
+ class Hash
19
+ def symbolize
20
+ inject({}) do |hash,(key,value)|
21
+ hash[(key.to_sym rescue key) || key] = case value
22
+ when Hash, Array
23
+ value.symbolize
24
+ else
25
+ value
26
+ end
27
+ hash
28
+ end
29
+ end
30
+ def symbolize!
31
+ self.replace(self.symbolize)
32
+ end
33
+ end
@@ -0,0 +1,404 @@
1
+ # Copyright 2010 The New York Times
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ #
15
+ #
16
+ require 'emissary/servolux'
17
+ require 'emissary/server'
18
+ require 'daemons'
19
+
20
+ module Emissary
21
+
22
+ # Some of the ServerController/Daemon stuff has been borrowed
23
+ # from Servolux::Daemon and Servolux::Server, so:
24
+ # Thanks to Tim Pease for those parts that are gleaned from Servolux
25
+ module ServerController
26
+
27
+ SIGNALS = %w[HUP INT TERM USR1 USR2 EXIT] & Signal.list.keys
28
+ SIGNALS.each {|sig| sig.freeze}.freeze
29
+
30
+ DEFAULT_PID_FILE_MODE = 0640
31
+
32
+ attr_accessor :pid_file_mode
33
+ attr_writer :pid_file
34
+
35
+ # Returns +true+ if the daemon process is currently running. Returns
36
+ # +false+ if this is not the case. The status of the process is determined
37
+ # by sending a signal to the process identified by the +pid_file+.
38
+ #
39
+ # @return [Boolean]
40
+ #
41
+ def alive?
42
+ pid = retrieve_pid
43
+ Process.kill(0, pid)
44
+ true
45
+ rescue Errno::ESRCH, Errno::ENOENT
46
+ false
47
+ rescue Errno::EACCES => err
48
+ logger.error "You do not have access to the PID file at " \
49
+ "#{pid_file.inspect}: #{err.message}"
50
+ false
51
+ end
52
+
53
+ def retrieve_pid
54
+ Integer(File.read(pid_file).strip)
55
+ rescue TypeError
56
+ raise Error, "A PID file was not specified."
57
+ rescue ArgumentError
58
+ raise Error, "#{pid_file.inspect} does not contain a valid PID."
59
+ end
60
+
61
+ # Send a signal to the daemon process identified by the PID file. The
62
+ # default signal to send is 'INT' (2). The signal can be given either as a
63
+ # string or a signal number.
64
+ #
65
+ # @param [String, Integer] signal The kill signal to send to the daemon
66
+ # process
67
+ # @return [Daemon] self
68
+ #
69
+ def kill( signal = 'INT' )
70
+ signal = Signal.list.invert[signal] if signal.is_a?(Integer)
71
+ pid = retrieve_pid
72
+ logger.info "Killing PID #{pid} with #{signal}"
73
+ Process.kill(signal, pid)
74
+ self
75
+ rescue Errno::EINVAL
76
+ logger.error "Failed to kill PID #{pid} with #{signal}: " \
77
+ "'#{signal}' is an invalid or unsupported signal number."
78
+ rescue Errno::EPERM
79
+ logger.error "Failed to kill PID #{pid} with #{signal}: " \
80
+ "Insufficient permissions."
81
+ rescue Errno::ESRCH
82
+ logger.error "Failed to kill PID #{pid} with #{signal}: " \
83
+ "Process is deceased or zombie."
84
+ rescue Errno::EACCES => err
85
+ logger.error err.message
86
+ rescue Errno::ENOENT => err
87
+ logger.error "Could not find a PID file at #{pid_file.inspect}. " \
88
+ "Most likely the process is no longer running."
89
+ rescue Exception => err
90
+ unless err.is_a?(SystemExit)
91
+ logger.error "Failed to kill PID #{pid} with #{signal}: #{err.message}"
92
+ end
93
+ end
94
+
95
+ def pid
96
+ alive? ? retrieve_pid : nil
97
+ end
98
+
99
+ def pid_file
100
+ @pid_file ||= File.join(config[:general][:pid_dir], (config[:general][:pid_file] || 'emissary.pid'))
101
+ end
102
+
103
+ def pid_dir
104
+ @pid_dir ||= File.join(config[:general][:pid_dir])
105
+ end
106
+
107
+ def create_pid_file
108
+ logger.debug "Server #{name.inspect} creating pid file #{pid_file.inspect}"
109
+ File.open(pid_file, 'w', pid_file_mode) {|fd| fd.write(Process.pid.to_s)}
110
+ end
111
+
112
+ def delete_pid_file
113
+ if test(?f, pid_file)
114
+ pid = Integer(File.read(pid_file).strip)
115
+ return unless pid == Process.pid
116
+
117
+ logger.debug "Server #{name.inspect} removing pid file #{pid_file.inspect}"
118
+ File.delete(pid_file)
119
+ end
120
+ end
121
+
122
+ def trap_signals
123
+ SIGNALS.each do |sig|
124
+ m = sig.downcase.to_sym
125
+ Signal.trap(sig) { self.send(m) rescue nil } if self.respond_to? m
126
+ end
127
+ end
128
+ end
129
+
130
+ class Daemon
131
+ include Emissary::ServerController
132
+
133
+ SHUTDOWN_RETRY = 1
134
+ MAX_RESTARTS = 10
135
+ REQUIRED_AGENTS = [ :emissary, :ping, :error ]
136
+
137
+ attr_accessor :logger, :state, :name, :config, :config_file
138
+ attr_reader :operators
139
+
140
+ def initialize(name, opts = {})
141
+
142
+ @operators = {}
143
+ @name = name
144
+ @mutex = Mutex.new
145
+ @shutdown = nil
146
+
147
+ self.class.const_set('STARTUP_OPTS', opts.clone_deep)
148
+ @config_file = File.expand_path(opts.delete(:config_file) || '/etc/emissary/config.ini')
149
+ @config = Daemon.get_config(config_file, STARTUP_OPTS)
150
+
151
+ self.class.const_set('CONFIG_FILE', @config_file)
152
+
153
+ @logger = Emissary::Logger.instance
154
+ @logger.level = @config[:general][:log_level]
155
+
156
+ @pid_file = config[:general][:pid_file]
157
+ @pid_file_mode = config[:general][:pid_file_mode] || DEFAULT_PID_FILE_MODE
158
+
159
+ ary = %w[name config_file].map { |var|
160
+ self.send(var.to_sym).nil? ? var : nil
161
+ }.compact
162
+ raise Error, "These variables are required: #{ary.join(', ')}." unless ary.empty?
163
+ end
164
+
165
+ def self.get_config(config_file, opts = {})
166
+ config = Daemon.validate_config!(Emissary::ConfigFile.new(config_file))
167
+
168
+ config[:general][:daemonize] = opts.delete(:daemonize) || false
169
+
170
+ config[:general][:agents] ||= 'all'
171
+ config[:general][:agents] = if config[:general][:agents].instance_of? String
172
+ config[:general][:agents].split(/\s*,\s*/)
173
+ else
174
+ config[:general][:agents].to_a
175
+ end
176
+
177
+ config[:general][:log_level] = opts.delete(:log_level) || config[:general][:log_level] || 'NOTICE'
178
+
179
+ unless (log_level = config[:general][:log_level]).kind_of? Fixnum
180
+ case log_level
181
+ when /^(LOG_){0,1}([A-Z]+)$/i
182
+ log_level = Emissary::Logger::CONSTANT_NAME_MAP[$2]
183
+ when Symbol
184
+ log_level = Emissary::Logger::CONSTANT_NAME_MAP[log_level]
185
+ when /[0-9]+/
186
+ log_level = log_level.to_i
187
+ end
188
+ config[:general][:log_level] = log_level
189
+ end
190
+
191
+ config[:general][:pid_dir] = opts.delete(:pid_dir) || '/var/run'
192
+
193
+ # set up defaults
194
+ config[:agents] ||= {}
195
+ config[:agents][:emissary] ||= {}
196
+ config[:agents][:emissary][:config_file] = File.expand_path(config_file)
197
+ config[:agents][:emissary][:config_path] = File.dirname(File.expand_path(config_file))
198
+
199
+
200
+ config[:general][:operators].each do |operator|
201
+ config[operator.to_sym].each do |name,data|
202
+ # setup the enabled and disabled agents on a per operator basis
203
+ agents = data[:agents].blank? ? config[:general][:agents] : if data[:agents].kind_of?(Array)
204
+ data[:agents]
205
+ else
206
+ data[:agents].split(/\s*,\s*/)
207
+ end
208
+
209
+ disable = agents.select { |v| v =~ /^-/ }.inject([]) { |a,v| a << v.gsub(/^-/,'').to_sym }
210
+ disable.include?(:all) && disable.delete_if { |v| v != :all }
211
+
212
+ enable = agents.select { |v| v !~ /^-/ }.inject([]) { |a,v| a << v.to_sym; a }
213
+ enable.include?(:all) && enable.delete_if { |v| v != :all }
214
+
215
+ # don't let the user disable REQUIRED AGENTS
216
+ disable -= REQUIRED_AGENTS
217
+
218
+ enable = ( enable.include?(:all) ? [ :all ] : enable | REQUIRED_AGENTS )
219
+
220
+ if not (conflicts = (enable - (enable - disable))).empty?
221
+ raise "Conflicting enabled/disabled agents: [#{conflicts.join(', ')}] - you can not both specifically enable and disable an agent!"
222
+ end
223
+
224
+ # now copy over the agent specific settings and
225
+ # append __enabled__ and __disabled__ list
226
+ data[:agents] = config[:agents].clone
227
+ data[:agents][:__enabled__] = enable
228
+ data[:agents][:__disabled__] = disable
229
+ end
230
+ end
231
+
232
+ config
233
+ end
234
+
235
+ def self.validate_config!(config)
236
+ unless config[:general]
237
+ raise ::Emissary::ConfigValidationError.new(Exception, "Missing 'general' section in configuration file")
238
+ end
239
+
240
+ unless config[:general][:operators]
241
+ logger.debug config[:general].inspect
242
+ raise ::Emissary::ConfigValidationError.new(Exception, "[general][operators] not set")
243
+ end
244
+
245
+ unless config[:general][:operators].kind_of? Array
246
+ raise ::Emissary::ConfigValidationError.new(Exception, "[general][operators] not a list")
247
+ end
248
+
249
+ config[:general][:operators].each do |operator|
250
+ operator = operator.to_sym
251
+ unless config[operator]
252
+ raise ::Emissary::ConfigValidationError.new(Exception, "Missing Operator Section '#{operator}'")
253
+ end
254
+
255
+ unless config[operator].kind_of? Hash
256
+ raise ::Emissary::ConfigValidationError.new(Exception, "Operator Section '#{operator}' not a dictionary of operators")
257
+ end
258
+ end
259
+
260
+ config
261
+ end
262
+
263
+ def become_daemon
264
+ # switch to syslog mode for logging
265
+ @logger.mode = Emissary::Logger::LOG_SYSLOG
266
+ Daemonize::daemonize(nil, name)
267
+ create_pid_file
268
+ end
269
+
270
+ def can_startup? operator
271
+ result = true
272
+ result &= (!operator[:daemon] || !operator[:daemon].alive?)
273
+ result &= operator[:start_count] < MAX_RESTARTS
274
+ result &= (not @shutting_down)
275
+ result
276
+ end
277
+
278
+ def call_operators
279
+ config[:general][:operators].each do |operator|
280
+ opsym = operator.to_sym
281
+ config[opsym].each do |name,data|
282
+ op = Emissary.call operator, data.merge({:signature => name, :parent_pid => $$})
283
+ @operators[op.signature] = { :operator => op, :start_count => 0 }
284
+ end
285
+ end
286
+ end
287
+
288
+ def reconfig
289
+ Emissary.logger.warn "Reloading configuration."
290
+ begin
291
+ new_config = Daemon.get_config(config_file, STARTUP_OPTS)
292
+ rescue Exception => e
293
+ Emissary.logger.error "Unable to reload configuration due to error:\n#{e.message}\n\t#{e.backtrace.join("\n\t")}"
294
+ else
295
+ @config = new_config
296
+ end
297
+ self.restart
298
+ end
299
+
300
+ def restart
301
+ shutdown false
302
+ startup
303
+ end
304
+
305
+ def startup
306
+ return if alive?
307
+
308
+ begin
309
+ become_daemon if config[:general][:daemonize]
310
+ trap_signals
311
+ call_operators
312
+ start_run_loop
313
+ rescue StandardError => e
314
+ Emissary.logger.error "Error Starting up: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
315
+ ensure
316
+ delete_pid_file
317
+ end
318
+ end
319
+
320
+ def shutdown do_exit = true
321
+ Emissary.logger.info "Shutdown Requested - Stopping operators"
322
+
323
+ @operators.each_key do |name|
324
+ operator = do_exit ? @operators.delete(name) : @operators[name]
325
+
326
+ Emissary.logger.notice "Shutting down operator '#{name}' - current status: #{operator[:daemon].alive? ? 'running' : 'stopped'}"
327
+ while operator[:daemon].alive?
328
+ Emissary.logger.debug "[SHUTDOWN] Hanging Up on Operator call '#{name}' (process: #{operator[:daemon_pid]})"
329
+ # should have shutdown above but, let's be sure here
330
+ operator[:daemon].shutdown if operator[:daemon].alive?
331
+ end
332
+
333
+ # Our shutdowns don't count toward restart limit for operators
334
+ # We're only protecting against multiple failed starts with it.
335
+ operator[:start_count] -= 1 unless operator[:start_count] <= 0
336
+ end
337
+
338
+ if do_exit
339
+ Emissary.logger.info "Shutdown Complete - Exiting..."
340
+ exit!(0)
341
+ end
342
+ end
343
+
344
+ def start_run_loop
345
+ while not @shutting_down do
346
+ @operators.each do |name,operator|
347
+ if operators[:start_count].to_i > MAX_RESTARTS
348
+ ::Emissary.logger.warning "Start Count > MAX_RESTARTS for operator '#{name}' - removing from list of operators..."
349
+ @operators.delete(name)
350
+ next
351
+ end
352
+
353
+ if can_startup? operator
354
+ Emissary.logger.notice "Starting up Operator: #{name}"
355
+
356
+ server_data = {
357
+ :operator => @operators[name][:operator],
358
+ :pid_file => File.join(pid_dir, "emop_#{name}"),
359
+ }
360
+
361
+ operator[:server] = Emissary::Server.new("emop_#{name}", server_data)
362
+ operator[:daemon] = Servolux::Daemon.new(:server => operator[:server])
363
+
364
+ # if the daemon is already alive before we've called startup
365
+ # then some other process started it, so we don't bother
366
+ if operator[:daemon].alive?
367
+ Emissary.logger.warning "Operator '#{name}' already running with pid '#{operator[:daemon].get_pid}'."
368
+ @operators.delete(name)
369
+ next
370
+ end
371
+
372
+ operator[:daemon].startup false
373
+ operator[:parent_pid] = retrieve_pid rescue $$
374
+ operator[:daemon_pid] = operator[:daemon].get_pid
375
+ operator[:start_count] += 1
376
+
377
+ if operator[:start_count] >= MAX_RESTARTS
378
+ Emissary.logger.warning "Operator '#{name}' has been restarted #{MAX_RESTARTS} times. " +
379
+ "I will not attempt to restart it anymore."
380
+ end
381
+
382
+ Emissary.logger.notice "Forked Operator '#{name}' with pid #{operator[:daemon_pid]}"
383
+ end
384
+ end
385
+
386
+ # if there are no operators left, then there is no point
387
+ # continue - so exit...
388
+ if @operators.length <= 0
389
+ Emissary.logger.notice "No operators left - shutting down."
390
+ shutdown true
391
+ end
392
+
393
+ sleep DAEMON_RECHECK_INTERVAL
394
+ end
395
+ end
396
+
397
+ alias :int :shutdown # handles the INT signal
398
+ alias :term :shutdown # handles the TERM signal
399
+ alias :kill :shutdown # handles the KILL signal
400
+ alias :exit :shutdown # handles the EXIT signal
401
+ alias :hup :reconfig # handles the HUP signal
402
+ alias :usr1 :restart # handles the USR1 signal
403
+ end
404
+ end