cloud66 0.0.26

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of cloud66 might be problematic. Click here for more details.

data/bin/c66-agent ADDED
@@ -0,0 +1,632 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'yaml'
4
+ require File.expand_path("../../lib/cloud-quartz", __FILE__)
5
+ require File.expand_path('../../lib/version', __FILE__)
6
+ require File.expand_path("../../lib/client_auth", __FILE__)
7
+ require File.expand_path("../../lib/vital_signs_utils", __FILE__)
8
+ require 'optparse'
9
+ require 'socket'
10
+ require 'logger'
11
+ require 'highline/import'
12
+
13
+ begin
14
+ gem 'eventmachine', '~>1.0.0.beta.4'
15
+ gem 'faye', '~>0.8.3'
16
+ gem 'highline', '~>1.6.11'
17
+
18
+ require 'eventmachine'
19
+ require 'faye'
20
+ rescue LoadError => exc
21
+ warn "Cannot find required ruby gems needed to run this agent. Please install the cloud66 gem by running 'gem install cloud66'"
22
+ warn exc
23
+ exit -1
24
+ end
25
+
26
+ #statuses
27
+ ST_UNREGISTERED = 0
28
+ ST_STOPPED = 1
29
+ ST_STARTED = 2
30
+
31
+ #number of pulses between pulses that check for network settings
32
+ NETWORK_CHECK_PULSE_FREQUENCY = 5
33
+
34
+ private
35
+
36
+ def safefork
37
+ tryagain = true
38
+
39
+ while tryagain
40
+ tryagain = false
41
+ begin
42
+ if pid = fork
43
+ return pid
44
+ end
45
+ rescue Errno::EWOULDBLOCK
46
+ sleep 5
47
+ tryagain = true
48
+ end
49
+ end
50
+ end
51
+
52
+ def daemonize(oldmode=0, closefd=false)
53
+ srand # Split rand streams between spawning and daemonized process
54
+ safefork and exit # Fork and exit from the parent
55
+
56
+ # Detach from the controlling terminal
57
+ unless sess_id = Process.setsid
58
+ raise 'Cannot detach from controlled terminal'
59
+ end
60
+
61
+ # Prevent the possibility of acquiring a controlling terminal
62
+ if oldmode.zero?
63
+ trap 'SIGHUP', 'IGNORE'
64
+ exit if pid = safefork
65
+ end
66
+
67
+ Dir.chdir "/" # Release old working directory
68
+ File.umask 0000 # Insure sensible umask
69
+
70
+ if closefd
71
+ # Make sure all file descriptors are closed
72
+ ObjectSpace.each_object(IO) do |io|
73
+ unless [STDIN, STDOUT, STDERR].include?(io)
74
+ io.close rescue nil
75
+ end
76
+ end
77
+ end
78
+
79
+ STDIN.reopen "/dev/null" # Free file descriptors and
80
+ STDOUT.reopen "/dev/null", "a" # point them somewhere sensible
81
+ STDERR.reopen STDOUT # STDOUT/STDERR should go to a logfile
82
+ return oldmode ? sess_id : 0 # Return value is mostly irrelevant
83
+ end
84
+
85
+ def save_config
86
+ Dir.mkdir(@config_dir) if !FileTest::directory?(@config_dir)
87
+ File.open(@config_full, 'w+') do |out|
88
+ data = {
89
+ 'api_key' => @api_key,
90
+ 'agent_id' => @agent_id,
91
+ 'secret_key' => @secret_key
92
+ }
93
+ # store the url if it is different
94
+ data['url'] = @url if @url != 'https://api.cloud66.com'
95
+ # store the faye url if it is different
96
+ data['faye_url'] = @faye_url if @faye_url != 'https://sockets.cloud66.com/push'
97
+ YAML::dump(data, out)
98
+ end
99
+ end
100
+
101
+ def load_config
102
+ if File.exists?(@config_full)
103
+ # config file present
104
+ config = YAML::load(File.open(@config_full))
105
+ @api_key = config['api_key']
106
+ @agent_id = config['agent_id']
107
+ @secret_key = config['secret_key']
108
+
109
+ # set if it exists in the config
110
+ config_url = config['url']
111
+ @url = config_url if !config_url.nil? && !config_url.strip.empty?
112
+
113
+ # set if it exists in the config
114
+ config_faye_url = config['faye_url']
115
+ @faye_url = config_faye_url if !config_faye_url.nil? && !config_faye_url.strip.empty?
116
+ end
117
+ end
118
+
119
+ def delete_config
120
+ File.delete(@config_full) if File.exists?(@config_full)
121
+ end
122
+
123
+ def get_pid
124
+ if File.exists?(@pid_full)
125
+ file = File.new(@pid_full, "r")
126
+ pid = file.read
127
+ file.close
128
+ pid
129
+ else
130
+ 0
131
+ end
132
+ end
133
+
134
+ def delete_pid
135
+ File.delete(@pid_full) if get_pid != 0
136
+ end
137
+
138
+ def pid_process_running?(pid)
139
+ begin
140
+ pid_number = pid.to_i
141
+ Process.getpgid(pid_number)
142
+ true
143
+ rescue Errno::ESRCH
144
+ false
145
+ end
146
+ end
147
+
148
+ def get_status
149
+ pid = get_pid
150
+ #check if the process is actually running
151
+ return ST_STARTED if pid != 0 && pid_process_running?(pid)
152
+ return ST_STOPPED if File.exists?(@config_full)
153
+ return ST_UNREGISTERED
154
+ end
155
+
156
+ public
157
+
158
+ def register
159
+
160
+ if get_status != ST_UNREGISTERED
161
+ begin
162
+ unregister
163
+ rescue
164
+ end
165
+ end
166
+
167
+ puts 'Cloud 66 Agent Registration:' if @api_key.empty? || @secret_key.empty?
168
+ # still no api key, we need to get it
169
+ if @api_key.empty?
170
+ @api_key = ask('Please enter your API key. (you can find it at https://cloud66.com/me): ')
171
+ if @api_key.length != 32
172
+ puts 'Invalid API key'
173
+ exit -1
174
+ end
175
+ end
176
+ if @secret_key.empty?
177
+ @secret_key = ask('Please enter your Secret Key (you can find it at https://cloud66.com/me): ')
178
+ if @secret_key.length != 32
179
+ puts 'Invalid Secret key'
180
+ exit -1
181
+ end
182
+ end
183
+
184
+ @quartz = CloudQuartz.new(:api_key => @api_key, :url => @url, :secret_key => @secret_key)
185
+ puts "Registering the Cloud 66 Agent..."
186
+
187
+ os_name = RUBY_PLATFORM
188
+ os_id = os_name.include?('darwin') ? 5 : 1
189
+
190
+ timezone = Time.new.zone
191
+ agent = { :agent_type_id => os_id, :agent_name => @name, :agent_timezone => timezone, :extra => os_name, :server_uid => @server_uid }
192
+ result = @quartz.register(agent)
193
+
194
+ if result['ok']
195
+ @agent_id = result['uid']
196
+ puts "Registered successfully (and now ready to be started)!"
197
+ save_config
198
+ else
199
+ puts "Failed to register due to #{result['error']}"
200
+ exit -1
201
+ end
202
+ end
203
+
204
+ def unregister
205
+ begin
206
+ stop if get_status == ST_STARTED
207
+
208
+ load_config
209
+ @quartz = CloudQuartz.new(:api_key => @api_key, :url => @url, :agent_id => @agent_id, :secret_key => @secret_key)
210
+
211
+ puts "Unregistering the Cloud 66 Agent..."
212
+ @agent_id = ""
213
+ @quartz.unregister(@agent_id)
214
+
215
+ rescue
216
+ ensure
217
+ delete_config
218
+ puts "Unregistered successfully!"
219
+ end
220
+ end
221
+
222
+ def stop(signalCatch = false)
223
+
224
+ #signalCatch indicates a TERM or INT trap (where the app is already stopped, but server not told)
225
+ if !signalCatch && get_status == ST_STOPPED
226
+ puts "This agent was already stopped."
227
+
228
+ #delete the pid file just in case its hanging around
229
+ delete_pid
230
+
231
+ exit -1
232
+ end
233
+
234
+ #unregister the agent on the server
235
+ begin
236
+ load_config
237
+ @quartz = CloudQuartz.new(:api_key => @api_key, :url => @url, :agent_id => @agent_id, :secret_key => @secret_key)
238
+ puts "Stopping the Cloud 66 Agent..."
239
+ @log.debug "Stopping the Cloud 66 Agent..."
240
+ @quartz.status(2)
241
+ rescue
242
+ end
243
+
244
+ begin
245
+ EM.stop
246
+ rescue
247
+ end
248
+
249
+ pid = get_pid
250
+ if pid != 0
251
+ begin
252
+ Process.kill('TERM', pid.to_i)
253
+ rescue
254
+ end
255
+ end
256
+ delete_pid
257
+
258
+ puts "Stopped successfully!"
259
+ @log.debug "Stopped successfully!"
260
+ end
261
+
262
+ def start
263
+
264
+ if get_status == ST_STARTED
265
+ puts "This agent is already started. To stop it, please use the 'stop' command."
266
+ exit -1
267
+ end
268
+
269
+ #we know it isn't running, so delete leftover pid file if it exists
270
+ delete_pid
271
+
272
+ load_config
273
+ @quartz = CloudQuartz.new(:api_key => @api_key, :url => @url, :agent_id => @agent_id, :secret_key => @secret_key)
274
+ load_plugins
275
+
276
+ begin
277
+ begin
278
+ facter_data = VitalSignsUtils.get_facter_info
279
+ rescue => exc
280
+ facter_data = {}
281
+ end
282
+ @log.info @quartz.init({ :version => Agent::Version.current, plugins: plugin_meta_data, facter: facter_data })
283
+ rescue => exc
284
+ message = exc.message
285
+ if message =~ /Couldn't find Agent with uid =/
286
+ @log.warn "This agent is no longer registered at the server. The old registration details have been removed from this agent. Please re-run the agent to re-register it."
287
+ puts "This agent is no longer registered at the server. The old registration details have been removed from this agent. Please re-run the agent to re-register it."
288
+ puts @config_full
289
+ File.delete(@config_full)
290
+ else
291
+ @log.error exc.message
292
+ end
293
+ exit -1
294
+ end
295
+
296
+ puts "Starting the Cloud 66 Agent..."
297
+ if @daemon_mode
298
+ daemonize
299
+ pid = Process.pid
300
+ begin
301
+ file = File.new(@pid_full, "w")
302
+ file.write(pid)
303
+ file.close
304
+ rescue => exc
305
+ Process.kill('TERM', pid)
306
+ warn "Cannot start the Cloud 66 Agent: #{exc.message}"
307
+ end
308
+
309
+ run
310
+ else
311
+ run
312
+ end
313
+ exit 0
314
+ end
315
+
316
+ private
317
+
318
+ def handle(result)
319
+ # if the server sends a shutdown signal
320
+ if !result.nil? && result.is_a?(Hash) && result['shut_down']
321
+ puts "Agent shutting down (server sent shut_down command)"
322
+ @log.debug "Agent shutting down (server sent shut_down command)"
323
+ # ensure bluepill doesn't bring this up again
324
+ `sudo bluepill cloud66_agent unmonitor` rescue nil
325
+ # stop the agent
326
+ stop(true)
327
+ end
328
+ end
329
+
330
+ def load_plugins
331
+ @load_path = File.expand_path(File.join(File.dirname(__FILE__), '../lib/plugins'))
332
+ @log.info "Loading plugins from #{@load_path}"
333
+
334
+ files = Dir.glob("#{@load_path}/*.rb")
335
+ files.each do |file|
336
+ unless file =~ /quartz_plugin/
337
+
338
+ # is it a valid plugin?
339
+ require "#{file}"
340
+ classname = File.basename(file, '.rb').split('_').collect { |part| part.capitalize }.join
341
+ begin
342
+ clazz = Kernel.const_get(classname)
343
+ if clazz.ancestors[1].name == 'QuartzPlugin'
344
+ instance = clazz.new(@log, { :api_key => @api_key, :agent_id => @agent_id })
345
+ guid = instance.info[:uid]
346
+ @plugins = @plugins.merge({ guid => instance })
347
+ @log.info "Found plugin #{instance.info[:name]}/#{instance.info[:version]} with uid #{guid}"
348
+ else
349
+ @log.error "Invalid plugin found #{clazz}"
350
+ end
351
+ rescue
352
+ end
353
+ end
354
+ end
355
+
356
+ @log.debug "All plugins #{plugin_meta_data}"
357
+ end
358
+
359
+ def plugin_meta_data
360
+ result = []
361
+ @plugins.each do |k, v|
362
+ result << v.info
363
+ end
364
+ result
365
+ end
366
+
367
+ def get_job
368
+ begin
369
+ result = @quartz.get_job
370
+ if result['ok']
371
+ if !result['empty']
372
+ message = JSON.parse(result['message'])
373
+ guid = message['plugin_uid']
374
+ name = message['template_name']
375
+ drt = message['desired_run_time']
376
+
377
+ @log.info "Going to run #{name} (uid:#{guid})"
378
+
379
+ # get the plugin
380
+ if @plugins.include?(guid)
381
+ plugin = @plugins[guid]
382
+
383
+ #run the job (new thread)
384
+ operation = proc { run_plugin(plugin, message) }
385
+ EM.defer(operation)
386
+
387
+ #drain the queue until it is empty
388
+ get_job
389
+ else
390
+ @log.error "No plugin found with uid #{guid}"
391
+ job_id = message['job_id']
392
+ data = { :run_start => Time.now.utc.to_i, :run_end => Time.now.utc.to_i, :agent_uid => @agent_id, :ok => false, :fail_reason => "Requested plugin not found. Does this agent support this job type?" }
393
+ @quartz.post_results(job_id, data)
394
+ end
395
+ end
396
+ else
397
+ @log.error "Failed to retrieve job due to #{result['error']}"
398
+ end
399
+ rescue => exc
400
+ @log.error "Failed to retrieve job due to #{exc}"
401
+ end
402
+ end
403
+
404
+ def run_plugin(plugin, message)
405
+ run_start = Time.now.utc.to_i
406
+ begin
407
+ job_id = message['job_id']
408
+ result = plugin.run(message)
409
+ @log.debug "Run returned for job #{job_id} with #{result}"
410
+ @log.debug result
411
+ ok = result[:ok]
412
+ to_return = result[:message]
413
+ rescue => exc
414
+ @log.error "Failure during execution of plugin #{plugin} due to #{exc}"
415
+ ok = false
416
+ if result.nil?
417
+ to_return = exc.message
418
+ else
419
+ to_return = result[:message]
420
+ end
421
+ ensure
422
+ data = { :run_start => run_start, :run_end => Time.now.utc.to_i, :agent_uid => @agent_id, :ok => ok }
423
+ data = ok ? data.merge({ :run_result => to_return }) : data.merge({ :fail_reason => to_return })
424
+ begin
425
+ @log.debug "Posting results for job #{job_id} back to the server #{data}"
426
+ @quartz.post_results(job_id, data)
427
+ rescue => e
428
+ @log.error "Failed to post results back to server due to #{e}"
429
+ end
430
+ end
431
+ end
432
+
433
+ def pulsate
434
+ begin
435
+ if @pulse_count == NETWORK_CHECK_PULSE_FREQUENCY
436
+
437
+ #reset pulse count
438
+ @pulse_count = 0
439
+
440
+ begin
441
+ handle(@quartz.pulse_with_ip_address(VitalSignsUtils.get_ip_address_info))
442
+ rescue => exc
443
+ #do a normal pulse if we have any detection issues
444
+ handle(@quartz.pulse_without_ip_address)
445
+ end
446
+ else
447
+ handle(@quartz.pulse_without_ip_address)
448
+
449
+ end
450
+ @pulse_count += 1
451
+ rescue => exc
452
+ @log.error "Failed to pulsate due to #{exc.message}"
453
+ end
454
+ end
455
+
456
+ def update_vital_signs
457
+ begin
458
+ data = {
459
+ :disk => VitalSignsUtils.get_disk_usage_info,
460
+ :cpu => VitalSignsUtils.get_cpu_usage_info,
461
+ :memory => VitalSignsUtils.get_memory_usage_info
462
+ }
463
+ handle(@quartz.send_vital_signs(data))
464
+ rescue => exc
465
+ @log.error "Failed to update vital signs due to #{exc.message}"
466
+ end
467
+ end
468
+
469
+ def run
470
+ EM.run {
471
+ Signal.trap('INT') { @log.debug("trapped INT signal"); stop(true) }
472
+ Signal.trap('TERM') { @log.debug("trapped TERM signal"); stop(true) }
473
+
474
+ # pulse
475
+ @pulse_count = NETWORK_CHECK_PULSE_FREQUENCY
476
+ pulsate
477
+ update_vital_signs
478
+ begin
479
+
480
+ EM.add_periodic_timer 60 do
481
+ pulsate
482
+ end
483
+ EM.add_periodic_timer 1800 do
484
+ update_vital_signs
485
+ end
486
+ rescue => exc
487
+ @log.error "Unable to add EM timer due to: #{exc.message}"
488
+ @log.error "#{exc.backtrace}"
489
+ exit -1
490
+ end
491
+
492
+ if @realtime
493
+ channel = "/agent_user/#{@api_key}/agent/#{@agent_id}/newjob"
494
+ @log.info "Listening to realtime notifications from '#{channel}' on '#{@faye_url}'"
495
+ client = Faye::Client.new(@faye_url)
496
+ client.subscribe(channel) do |message|
497
+ @log.info "Got realtime notice for a new job #{message}"
498
+ get_job
499
+ end
500
+ else
501
+ @log.info "Checking for new jobs every 60 seconds"
502
+ #reduced job check frequency (for stale checker)
503
+ EM.add_periodic_timer 45 do
504
+ get_job
505
+ end
506
+ end
507
+ }
508
+ end
509
+
510
+ public
511
+
512
+ config_file = 'agent.yml'
513
+ @pid_file = 'c66-agent.pid'
514
+ @log_file = 'c66-agent.log'
515
+ @config_dir = '/etc/cloud66/'
516
+ @config_full = File.join(@config_dir, config_file)
517
+ @cb_tmp_dir = '/tmp/cloud66'
518
+ Dir.mkdir(@cb_tmp_dir) if !File.exists?(@cb_tmp_dir)
519
+
520
+ @pid_full = File.join(@cb_tmp_dir, @pid_file)
521
+ @log_full = File.join(@cb_tmp_dir, @log_file)
522
+ commands = %w[register unregister start stop]
523
+ @plugins = {}
524
+ command = nil
525
+
526
+
527
+ OptionParser.new do |opts|
528
+ opts.banner = <<-EOF
529
+ Cloud 66 Agent. v#{Agent::Version.current} (c) 2012 Cloud 66
530
+ For more information please visit http://cloud66.com
531
+
532
+ Usage: c66-agent [register|unregister|start|stop] [options]
533
+
534
+ Options:
535
+ EOF
536
+
537
+ opts.on('--url URL', 'Server URL') do |server_url|
538
+ @url = server_url
539
+ end
540
+ @url ||= 'https://api.cloud66.com'
541
+
542
+ opts.on('-d', '--no-daemon', 'Not in daemon mode') do |v|
543
+ @daemon_mode = false
544
+ end
545
+ @daemon_mode ||= true
546
+
547
+ opts.on('-p', '--pid PID', 'PID file path') do |v|
548
+ @pid_full = v
549
+ end
550
+
551
+ opts.on('-l', '--log LOG', 'Full log file path') do |v|
552
+ @log_full = v
553
+ end
554
+
555
+ opts.on('-n', '--name NAME', 'Name of this agent') do |v|
556
+ @name = v
557
+ end
558
+ @name = Socket.gethostname if @name.nil? || @name.empty?
559
+
560
+ opts.on('-c', '--config CONFIG', 'Config file path') do |v|
561
+ @config_full = v
562
+ end
563
+
564
+ opts.on('--sockets SOCKETS', 'Sockets URL') do |v|
565
+ @faye_url = v
566
+ end
567
+ @faye_url ||= 'https://sockets.cloud66.com/push'
568
+
569
+ opts.on('--api-key APIKEY', 'API key') do |v|
570
+ @api_key = v
571
+ end
572
+ @api_key ||= ''
573
+
574
+ opts.on('--secret-key SECRETKET', 'Secret Key') do |v|
575
+ @secret_key = v
576
+ end
577
+ @secret_key ||= ''
578
+
579
+ opts.on('--server SERVERUID', 'Server id') do |v|
580
+ @server_uid = v
581
+ end
582
+
583
+ opts.on('-r', '--realtime', 'Enable realtime notifications (default)') do |v|
584
+ @realtime = true
585
+ end
586
+ opts.on('-R', '--no-realtime', 'Disable realtime notifications') do |v|
587
+ @realtime = false
588
+ end
589
+ @realtime ||= true
590
+
591
+ opts.on_tail("-h", "--help", "Show this message") do
592
+ puts opts
593
+ puts <<-EOF
594
+
595
+ Commands:
596
+ register Register the Cloud 66 Agent
597
+ start Starts the Cloud 66 Agent as a deamon
598
+ stop Stops the Cloud 66 Agent daemon
599
+ unregister Unregisters the Cloud 66 Agent
600
+
601
+ EOF
602
+ exit 0
603
+ end
604
+ end.parse!
605
+
606
+ #set logging output
607
+ @log = @daemon_mode ? Logger.new(@log_full) : Logger.new(STDOUT)
608
+ @log.level = Logger::DEBUG
609
+
610
+ #return status information
611
+ command = ARGV[0].downcase unless ARGV[0].nil?
612
+ if command.nil? || command.empty?
613
+ status = get_status
614
+ puts "v#{Agent::Version.current} Started (use --help for commands)" if status == ST_STARTED
615
+ puts "v#{Agent::Version.current} Stopped (use --help for commands)" if status == ST_STOPPED
616
+ puts "v#{Agent::Version.current} Unregistered (use --help for commands)" if status == ST_UNREGISTERED
617
+ exit -1
618
+ end
619
+
620
+ unless commands.include?(command)
621
+ puts 'Invalid command. Use --help for more information'
622
+ exit -1
623
+ end
624
+
625
+ begin
626
+ send(command)
627
+ rescue => exc
628
+ @log.error exc.message
629
+ puts "An error has occurred: #{exc.message}"
630
+ exit -1
631
+ end
632
+
@@ -0,0 +1,32 @@
1
+ class ClientAuth
2
+
3
+ def initialize api_key, secret_key
4
+ @headers = self.class.build_headers api_key, secret_key
5
+ end
6
+
7
+ def outgoing(message, callback)
8
+
9
+ # Again, leave non-subscribe messages alone
10
+ if message['channel'] != '/meta/subscribe'
11
+ return callback.call(message)
12
+ end
13
+
14
+ # Add ext field if it's not present
15
+ message['ext'] ||= {}
16
+
17
+ # Set the tokens
18
+ message['ext']['api_key'] = @headers['api_key']
19
+ message['ext']['hash'] = @headers['hash']
20
+ message['ext']['time'] = @headers['time']
21
+
22
+ # Carry on and send the message to the server
23
+ callback.call(message)
24
+ end
25
+
26
+ def self.build_headers api_key, secret_key
27
+ time = Time.now.utc.to_i
28
+ hash = Digest::SHA1.hexdigest("#{api_key}#{secret_key}#{time}").downcase
29
+ { 'api_key' => api_key, 'hash' => hash, 'time' => time.to_s }
30
+ end
31
+
32
+ end