right_chimp 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.
@@ -0,0 +1,18 @@
1
+ #
2
+ # Utility class for generating sequential job ids
3
+ #
4
+ module Chimp
5
+ class IDManager
6
+ @@id = 0
7
+ @@mutex = Mutex.new
8
+
9
+ def self.get
10
+ r = nil
11
+ @@mutex.synchronize do
12
+ @@id = @@id + 1
13
+ r = @@id
14
+ end
15
+ return r
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,34 @@
1
+ module Chimp
2
+ class Log
3
+ @@logger = Logger.new(STDOUT)
4
+ @@logger.sev_threshold=Logger::INFO
5
+
6
+ def self.logger=(l)
7
+ @@logger = l
8
+ end
9
+
10
+ def self.threshold=(level)
11
+ @@logger.sev_threshold=level
12
+ end
13
+
14
+ def self.default_level(level)
15
+ @@logger.level = level
16
+ end
17
+
18
+ def self.debug(m)
19
+ @@logger.debug(m)
20
+ end
21
+
22
+ def self.info(m)
23
+ @@logger.info(m)
24
+ end
25
+
26
+ def self.warn(m)
27
+ @@logger.warn(m)
28
+ end
29
+
30
+ def self.error(m)
31
+ @@logger.error(m)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,416 @@
1
+ #
2
+ # ChimpDaemon.rb
3
+ #
4
+ # Classes for the Chimp Daemon (chimpd)
5
+ #
6
+
7
+ module Chimp
8
+ class ChimpDaemon
9
+ attr_accessor :verbose, :debug, :port, :concurrency, :delay, :retry_count, :dry_run, :logfile
10
+ attr_reader :queue, :running
11
+
12
+ include Singleton
13
+
14
+ def initialize
15
+ @verbose = false
16
+ @debug = false
17
+ @port = 9055
18
+ @concurrency = 50
19
+ @delay = 0
20
+ @retry_count = 0
21
+ @threads = []
22
+ @running = false
23
+ @queue = ChimpQueue.instance
24
+ end
25
+
26
+ #
27
+ # Main entry point for chimpd command line application
28
+ #
29
+ def run
30
+ install_signal_handlers
31
+ parse_command_line
32
+
33
+ puts "chimpd #{VERSION} launching with #{@concurrency} workers"
34
+ spawn_queue_runner
35
+ spawn_webserver
36
+ run_forever
37
+ end
38
+
39
+ #
40
+ # Parse chimpd command line options
41
+ #
42
+ def parse_command_line
43
+ begin
44
+ opts = GetoptLong.new(
45
+ [ '--logfile', '-l', GetoptLong::REQUIRED_ARGUMENT ],
46
+ [ '--verbose', '-v', GetoptLong::NO_ARGUMENT ],
47
+ [ '--quiet', '-q', GetoptLong::NO_ARGUMENT ],
48
+ [ '--concurrency', '-c', GetoptLong::REQUIRED_ARGUMENT ],
49
+ [ '--delay', '-d', GetoptLong::REQUIRED_ARGUMENT ],
50
+ [ '--retry', '-y', GetoptLong::REQUIRED_ARGUMENT ],
51
+ [ '--port', '-p', GetoptLong::REQUIRED_ARGUMENT ],
52
+ [ '--exit', '-x', GetoptLong::NO_ARGUMENT ]
53
+ )
54
+
55
+ opts.each do |opt, arg|
56
+ case opt
57
+ when '--logfile', '-l'
58
+ @logfile = arg
59
+ Log.logger = Logger.new(@logfile)
60
+ when '--concurrency', '-c'
61
+ @concurrency = arg.to_i
62
+ when '--delay', '-d'
63
+ @delay = arg.to_i
64
+ when '--retry', '-y'
65
+ @retry_count = arg.to_i
66
+ when '--verbose', '-v'
67
+ @verbose = true
68
+ when '--quiet', '-q'
69
+ @quiet = true
70
+ when '--port', '-p'
71
+ @port = arg
72
+ when '--exit', '-x'
73
+ uri = "http://localhost:9055/admin"
74
+ response = RestClient.post uri, { 'shutdown' => true }.to_yaml
75
+ exit 0
76
+ end
77
+ end
78
+ rescue GetoptLong::InvalidOption => ex
79
+ puts "Syntax: chimpd [--logfile=<name>] [--concurrency=<c>] [--delay=<d>] [--retry=<r>] [--port=<p>] [--verbose]"
80
+ exit 1
81
+ end
82
+
83
+ #
84
+ # Set up logging/verbosity
85
+ #
86
+ Chimp.set_verbose(@verbose, @quiet)
87
+
88
+ if not @verbose
89
+ ENV['REST_CONNECTION_LOG'] = "/dev/null"
90
+ ENV['RESTCLIENT_LOG'] = "/dev/null"
91
+ end
92
+ end
93
+
94
+ #
95
+ # Spawn the ChimpQueue threads
96
+ #
97
+ def spawn_queue_runner
98
+ @queue.max_threads = @concurrency
99
+ @queue.delay = @delay
100
+ @queue.retry_count = @retry_count
101
+ @queue.start
102
+ @running = true
103
+ end
104
+
105
+ #
106
+ # Spawn a WEBrick Web server
107
+ #
108
+ def spawn_webserver
109
+ opts = {
110
+ :BindAddress => "localhost",
111
+ :Port => @port,
112
+ :MaxClients => 50,
113
+ :RequestTimeout => 60
114
+ }
115
+
116
+ if not @verbose
117
+ opts[:Logger] = WEBrick::Log.new("/dev/null")
118
+ opts[:AccessLog] = [nil, nil]
119
+ end
120
+
121
+ @server = ::WEBrick::HTTPServer.new(opts)
122
+ @server.mount('/', DisplayServlet)
123
+ @server.mount('/display', DisplayServlet)
124
+ @server.mount('/job', JobServlet)
125
+ @server.mount('/group', GroupServlet)
126
+ @server.mount('/admin', AdminServlet)
127
+
128
+ #
129
+ # WEBrick threads
130
+ #
131
+ @threads << Thread.new(1001) do
132
+ @server.start
133
+ end
134
+ end
135
+
136
+ #
137
+ # Process requests forever until we're killed
138
+ #
139
+ def run_forever
140
+ @running = true
141
+ while @running
142
+ @threads.each do |t|
143
+ t.join(5)
144
+ end
145
+ end
146
+ end
147
+
148
+ #
149
+ # Trap signals to exit cleanly
150
+ #
151
+ def install_signal_handlers
152
+ ['INT', 'TERM'].each do |signal|
153
+ trap(signal) do
154
+ self.quit
155
+ end
156
+ end
157
+ end
158
+
159
+ #
160
+ # Quit by waiting for all chimp jobs to finish, not allowing
161
+ # new jobs on the queue, and killing the web server.
162
+ #
163
+ # TODO: call @queue.quit, but with a short timeout?
164
+ #
165
+ def quit
166
+ @running = false
167
+ @server.shutdown
168
+ sleep 5
169
+ exit 0
170
+ end
171
+
172
+ #
173
+ # GenericServlet -- servlet superclass
174
+ #
175
+ class GenericServlet < WEBrick::HTTPServlet::AbstractServlet
176
+ def get_verb(req)
177
+ r = req.request_uri.path.split('/')[2]
178
+ end
179
+
180
+ def get_id(req)
181
+ uri_parts = req.request_uri.path.split('/')
182
+ id = uri_parts[-2]
183
+ return id
184
+ end
185
+
186
+ #
187
+ # Get the body of the request-- assume YAML
188
+ #
189
+ def get_payload(req)
190
+ begin
191
+ return YAML::load(req.body)
192
+ rescue StandardError => ex
193
+ return nil
194
+ end
195
+ end
196
+ end # GenericServlet
197
+
198
+ #
199
+ # AdminServlet - admin functions
200
+ #
201
+ class AdminServlet < GenericServlet
202
+ def do_POST(req, resp)
203
+ payload = self.get_payload(req)
204
+ shutdown = payload['shutdown'] || false
205
+
206
+ if shutdown == true
207
+ ChimpDaemon.instance.quit
208
+ end
209
+
210
+ raise WEBrick::HTTPStatus::OK
211
+ end
212
+ end # AdminServlet
213
+
214
+ #
215
+ # GroupServlet - group information and control
216
+ #
217
+ # http://localhost:9055/group/default/running
218
+ #
219
+ class GroupServlet < GenericServlet
220
+ #
221
+ # GET a group by name and status
222
+ # /group/<name>/<status>
223
+ #
224
+ def do_GET(req, resp)
225
+ jobs = {}
226
+
227
+ group_name = req.request_uri.path.split('/')[-2]
228
+ filter = req.request_uri.path.split('/')[-1]
229
+
230
+ g = ChimpQueue[group_name.to_sym]
231
+ raise WEBrick::HTTPStatus::NotFound, "Group not found" unless g
232
+ jobs = g.get_jobs_by_status(filter)
233
+ resp.body = jobs.to_yaml
234
+ raise WEBrick::HTTPStatus::OK
235
+ end
236
+
237
+ #
238
+ # POST to a group to trigger a group action
239
+ # /group/<name>/<action>
240
+ #
241
+ def do_POST(req, resp)
242
+ group_name = req.request_uri.path.split('/')[-2]
243
+ filter = req.request_uri.path.split('/')[-1]
244
+ payload = self.get_payload(req)
245
+
246
+ if filter == 'create'
247
+ ChimpQueue.instance.create_group(group_name, payload['type'], payload['concurrency'])
248
+
249
+ elsif filter == 'retry'
250
+ group = ChimpQueue[group_name.to_sym]
251
+ raise WEBrick::HTTPStatus::NotFound, "Group not found" unless group
252
+
253
+ group.requeue_failed_jobs!
254
+ raise WEBrick::HTTPStatus::OK
255
+
256
+ else
257
+ raise WEBrick::HTTPStatus::PreconditionFailed.new("invalid action")
258
+ end
259
+ end
260
+
261
+ end
262
+
263
+ #
264
+ # JobServlet - job control
265
+ #
266
+ # HTTP body is a yaml serialized chimp object
267
+ #
268
+ class JobServlet < GenericServlet
269
+ def do_POST(req, resp)
270
+ id = -1
271
+ job_id = self.get_id(req)
272
+ verb = self.get_verb(req)
273
+
274
+ payload = self.get_payload(req)
275
+ raise WEBrick::HTTPStatus::PreconditionFailed.new('missing payload') unless payload
276
+
277
+ q = ChimpQueue.instance
278
+ group = payload.group
279
+
280
+ #
281
+ # Ask chimpd to process a Chimp object directly
282
+ #
283
+ if verb == 'process' or verb == 'add'
284
+ payload.interactive = false
285
+ tasks = payload.process
286
+ tasks.each do |task|
287
+ q.push(group, task)
288
+ end
289
+
290
+ elsif verb == 'update'
291
+ puts "UPDATE"
292
+ q.get_job(job_id).status = payload.status
293
+ end
294
+
295
+ resp.body = {
296
+ 'id' => id
297
+ }.to_yaml
298
+
299
+ raise WEBrick::HTTPStatus::OK
300
+ end
301
+
302
+ def do_GET(req, resp)
303
+ id = self.get_id(req)
304
+ verb = self.get_verb(req)
305
+ job_results = "OK"
306
+ queue = ChimpQueue.instance
307
+
308
+ #
309
+ # check for special job ids
310
+ #
311
+ jobs = []
312
+ jobs << queue.get_job(id.to_i)
313
+
314
+ jobs = queue.get_jobs_by_status(:running) if id == 'running'
315
+ jobs = queue.get_jobs_by_status(:error) if id == 'error'
316
+ jobs = queue.get_jobs if id == 'all'
317
+
318
+ raise WEBrick::HTTPStatus::PreconditionFailed.new('invalid or missing job_id #{id}') unless jobs.size > 0
319
+
320
+ #
321
+ # ACK a job -- mark it as successful even if it failed
322
+ #
323
+ if req.request_uri.path =~ /ack$/
324
+ jobs.each do |j|
325
+ j.status = Executor::STATUS_DONE
326
+ end
327
+
328
+ resp.set_redirect( WEBrick::HTTPStatus::TemporaryRedirect, req.header['referer'])
329
+
330
+ #
331
+ # retry a job
332
+ #
333
+ elsif req.request_uri.path =~ /retry$/
334
+ jobs.each do |j|
335
+ j.requeue
336
+ end
337
+
338
+ resp.set_redirect( WEBrick::HTTPStatus::TemporaryRedirect, req.header['referer'])
339
+
340
+ #
341
+ # cancel an active job
342
+ #
343
+ elsif req.request_uri.path =~ /cancel$/
344
+ jobs.each do |j|
345
+ j.cancel if j.respond_to? :cancel
346
+ end
347
+
348
+ resp.set_redirect( WEBrick::HTTPStatus::TemporaryRedirect, req.header['referer'])
349
+
350
+ #
351
+ # produce a report
352
+ #
353
+ elsif req.request_uri.path =~ /report$/
354
+ results = ["group_name,type,job_id,script,target,start_time,end_time,total_time,status"]
355
+ jobs.each do |j|
356
+ results << [j.group.group_id, j.class.to_s.sub("Chimp::",""), j.job_id, j.info, j.target, j.time_start, j.time_end, j.get_total_exec_time, j.status].join(",")
357
+ end
358
+
359
+ queue.group.values.each do |g|
360
+ results << [g.group_id, g.class.to_s.sub("Chimp::",""), "", "", "", g.time_start, g.time_end, g.get_total_exec_time, ""].join(",")
361
+ end
362
+
363
+ job_results = results.join("\n") + "\n"
364
+
365
+ resp['Content-type'] = "text/csv"
366
+ resp['Content-disposition'] = "attachment;filename=chimp.csv"
367
+ end
368
+
369
+ #
370
+ # return a list of the results
371
+ #
372
+ resp.body = job_results
373
+ raise WEBrick::HTTPStatus::OK
374
+ end
375
+ end # JobServlet
376
+
377
+ #
378
+ # DisplayServlet
379
+ #
380
+ class DisplayServlet < GenericServlet
381
+ def do_GET(req, resp)
382
+ job_filter = self.get_verb(req) || "running"
383
+
384
+ if not @template
385
+ if ENV['CHIMP_TEST'] != 'TRUE'
386
+ template_file_name = File.join(Gem.dir, 'gems', 'chimp-' + VERSION, 'lib/chimp/templates/all_jobs.erb')
387
+ else
388
+ template_file_name = 'lib/chimp/templates/all_jobs.erb'
389
+ end
390
+
391
+ @template = ERB.new(File.read(template_file_name), nil, ">")
392
+ end
393
+
394
+ queue = ChimpQueue.instance
395
+ jobs = queue.get_jobs
396
+ group_name = nil
397
+
398
+ if job_filter == "group"
399
+ group_name = req.request_uri.path.split('/')[-1]
400
+ g = ChimpQueue[group_name.to_sym]
401
+ jobs = g.get_jobs if g
402
+ end
403
+
404
+ count_jobs_running = queue.get_jobs_by_status(:running).size
405
+ count_jobs_queued = queue.get_jobs_by_status(:none).size
406
+ count_jobs_failed = queue.get_jobs_by_status(:error).size
407
+ count_jobs_done = queue.get_jobs_by_status(:done).size
408
+
409
+ resp.body = @template.result(binding)
410
+ raise WEBrick::HTTPStatus::OK
411
+ end
412
+ end # DisplayServlet
413
+ end
414
+ end
415
+
416
+
@@ -0,0 +1,74 @@
1
+ module Chimp
2
+ #
3
+ # The ChimpDaemonClient contains the code for communicating with the
4
+ # RESTful chimpd Web service
5
+ #
6
+ class ChimpDaemonClient
7
+ #
8
+ # Submit a Chimp object to a remote chimpd
9
+ #
10
+ def self.submit(host, port, chimp_object)
11
+ uri = "http://#{host}:#{port}/job/process"
12
+ response = RestClient.post uri, chimp_object.to_yaml
13
+
14
+ if response.code > 199 and response.code < 300
15
+ begin
16
+ id = YAML::load(response.body)['id']
17
+ rescue StandardError => ex
18
+ puts ex
19
+ end
20
+ else
21
+ $stderr.puts "error submitting to chimpd! response code: #{reponse.code}"
22
+ return false
23
+ end
24
+
25
+ puts "chimpd submission complete"
26
+ return true
27
+ end
28
+
29
+ #
30
+ # quit a remote chimpd
31
+ #
32
+ def self.quit(host, port)
33
+ response = RestClient.post "http://#{host}:#{port}/admin", { 'shutdown' => 'true' }.to_yaml
34
+ return response.code
35
+ end
36
+
37
+ #
38
+ # retrieve job info from a remote chimpd
39
+ #
40
+ def self.retrieve_job_info(host, port)
41
+ uri = "http://#{host}:#{port}/job/0"
42
+ response = RestClient.get uri
43
+ jobs = YAML::load(response.body)
44
+ return jobs
45
+ end
46
+
47
+ def self.retrieve_group_info(host, port, group_name, status)
48
+ uri = "http://#{host}:#{port}/group/#{group_name}/#{status}"
49
+ response = RestClient.get uri
50
+ group = YAML::load(response.body)
51
+ return group
52
+ end
53
+
54
+ def self.set_job_status(host, port, job_id, status)
55
+ uri = "http://#{host}:#{port}/job/#{job_id}/update"
56
+ response = RestClient.post uri, { :status => status}
57
+ return YAML::load(response.body)
58
+ end
59
+
60
+ def self.create_group(host, port, name, type, concurrency)
61
+ uri = "http://#{host}:#{port}/group/#{name}/create"
62
+ payload = { 'type' => type, 'concurrency' => concurrency }.to_yaml
63
+ response = RestClient.post(uri, payload)
64
+ return YAML::load(response.body)
65
+ end
66
+
67
+ def self.retry_group(host, port, group_name)
68
+ uri = "http://#{host}:#{port}/group/#{group_name}/retry"
69
+ response = RestClient.post(uri, {})
70
+ return YAML::load(response.body)
71
+ end
72
+
73
+ end
74
+ end
@@ -0,0 +1,43 @@
1
+ #
2
+ # Run a RightScript on an array
3
+ #
4
+ module Chimp
5
+ class ExecArray < Executor
6
+ def run
7
+ run_with_retry do
8
+ audit_entry = []
9
+
10
+ #
11
+ # If we have API 0.1 access, then use the 0.1 version of this call.
12
+ # Otherwise use the 1.0 version.
13
+ #
14
+ if @array.internal
15
+ audit_entry = @array.internal.run_script_on_instances(@exec, @server['href'], {})
16
+ else
17
+ audit_entry = @array.run_script_on_instances(@exec, @server['href'], {})
18
+ end
19
+
20
+ if audit_entry
21
+ audit_entry.each do |a|
22
+ a.wait_for_completed
23
+ end
24
+ else
25
+ Log.warn "No audit entries returned for job_id=#{@job_id}"
26
+ end
27
+ end
28
+ end
29
+
30
+ def describe_work
31
+ return "ExecArray job_id=#{@job_id} script=\"#{@exec['right_script']['name']}\" server=\"#{@server['nickname']}\""
32
+ end
33
+
34
+ def info
35
+ return @exec['right_script']['name']
36
+ end
37
+
38
+ def target
39
+ return @server['nickname']
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,15 @@
1
+ #
2
+ # Run an SSH script
3
+ #
4
+ module Chimp
5
+ class ExecCallback < Executor
6
+ def run
7
+ response = RestClient.get @uri
8
+ if response.code > 199 and response.code < 300
9
+ return true
10
+ else
11
+ return false
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ module Chimp
2
+ class ExecNoop < Executor
3
+ def run
4
+ run_with_retry do
5
+ # do nothing
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,57 @@
1
+ #
2
+ # Report
3
+ #
4
+ # :fields is a comma seperated list of fields to report on,
5
+ # and are taken from the RestConnection Server objects directly.
6
+ #
7
+ # Example: ip-address,name,href,private-ip-address,aws-id,ec2-instance-type,dns-name,locked
8
+ #
9
+ module Chimp
10
+ class ExecReport < Executor
11
+ attr_reader :server, :fields
12
+ attr_writer :server, :fields
13
+
14
+ def info
15
+ return "report on server #{fields.inspect}"
16
+ end
17
+
18
+ def run
19
+ run_with_retry do
20
+ output = []
21
+
22
+ #
23
+ # The API and rest_connection return very different data
24
+ # depending on the API call made (here, tag query vs. array)
25
+ # so the logic to load the server information and tags is
26
+ # messy.
27
+ #
28
+ begin
29
+ s = Server.find(@server.href)
30
+ s.settings
31
+ response = ::Tag.search_by_href(s.current_instance_href)
32
+ rescue Exception => ex
33
+ s = @server
34
+ response = ::Tag.search_by_href(s.href)
35
+ end
36
+
37
+ s.tags = [] unless s.tags
38
+ response.each do |t|
39
+ s.tags += [ t['name'] ]
40
+ end
41
+
42
+ @fields.split(",").each do |f|
43
+ if f =~ /^tag=([^,]+)/
44
+ tag_search_string = $1
45
+ s.tags.each do |tag|
46
+ output << tag if tag =~ /^#{tag_search_string}/
47
+ end
48
+ else
49
+ output << s.params[f]
50
+ end
51
+ end
52
+
53
+ puts output.join(",")
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,35 @@
1
+ #
2
+ # Run a RightScript
3
+ #
4
+ module Chimp
5
+ class ExecRightScript < Executor
6
+
7
+ def run
8
+ options = {:ignore_lock => true}.merge(@inputs)
9
+
10
+ if @timeout < 300
11
+ Log.error "timeout was less than 5 minutes! resetting to 5 minutes"
12
+ @timeout = 300
13
+ end
14
+
15
+ run_with_retry do
16
+ audit_entry = server.run_executable(@exec, options)
17
+ audit_entry.wait_for_state("completed", @timeout)
18
+ @results = audit_entry.summary
19
+ end
20
+ end
21
+
22
+ def describe_work
23
+ return "ExecRightScript job_id=#{@job_id} script=\"#{@exec['right_script']['name']}\" server=\"#{@server['nickname']}\""
24
+ end
25
+
26
+ def info
27
+ return @exec['right_script']['name']
28
+ end
29
+
30
+ def target
31
+ return @server['nickname']
32
+ end
33
+
34
+ end
35
+ end