emissary 1.3.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,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