right_chimp 1.1.3 → 2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,393 @@
1
+ #
2
+ # Extra classes needed to operate with Chimp
3
+ #
4
+ module Chimp
5
+ #
6
+ # This class contains all the necessary to make calls to api1.5 via the right_api_client gem
7
+ # or obtain a list of instances via api1.6 calls.
8
+ #
9
+ class Connection
10
+
11
+ include Singleton
12
+ attr_accessor :client, :all_instances, :retry
13
+
14
+ def initialize
15
+ end
16
+
17
+ def self.connect_and_cache
18
+
19
+ self.start_right_api_client
20
+
21
+ Log.debug "Making initial Api1.6 call to cache entries."
22
+
23
+ result = self.all_instances
24
+ if result.empty? || result.nil?
25
+ Log.error "Couldnt contact API1.6 correctly, will now exit."
26
+ exit -1
27
+ else
28
+ Log.debug "API lists #{result.count} operational instances in the account"
29
+ end
30
+ end
31
+
32
+ def self.connect
33
+ self.start_right_api_client
34
+ end
35
+
36
+ def self.start_right_api_client
37
+ require 'yaml'
38
+ require 'right_api_client'
39
+ begin
40
+ creds = YAML.load_file("#{ENV['HOME']}/.rest_connection/rest_api_config.yaml")
41
+
42
+ # Extract the account
43
+ creds[:account] = File.basename(creds[:api_url])
44
+
45
+ # Figure out url to hit:
46
+ creds[:api_url] = "https://"+URI.parse(creds[:api_url]).host
47
+
48
+ @endpoint = URI.parse(creds[:api_url]).host
49
+
50
+ Log.debug "Logging into Api 1.5 right_api_client"
51
+
52
+ @client = RightApi::Client.new(:email => creds[:user], :password => creds[:pass],
53
+ :account_id => creds[:account], :api_url => creds[:api_url],
54
+ :timeout => nil)
55
+ rescue
56
+ puts "##############################################################################"
57
+ puts "Error: "
58
+ puts " - credentials file could not be loaded correctly"
59
+ puts "or "
60
+ puts " - connection couldnt be establishhed"
61
+ puts "##############################################################################"
62
+ exit -1
63
+ end
64
+ end
65
+
66
+ def self.client
67
+ @client
68
+ end
69
+
70
+ def self.endpoint
71
+ @endpoint
72
+ end
73
+
74
+ #
75
+ # Returns every single operational instance in the account
76
+ #
77
+ def self.all_instances()
78
+ begin
79
+ filters_list = "state=operational"
80
+ filters = CGI::escape(filters_list)
81
+
82
+ query="/api/instances?view=full&filter="+filters
83
+
84
+ @all_instances = Connection.api16_call(query)
85
+
86
+ rescue Exception => e
87
+ puts e.message
88
+ end
89
+
90
+ return @all_instances
91
+ end
92
+
93
+ #
94
+ # Returns every single operational instance in the account, matching the filters passed.
95
+ #
96
+ def self.instances(extra_filters)
97
+ begin
98
+ filters_list = "state=operational&"+extra_filters
99
+ filters = CGI::escape(filters_list)
100
+
101
+ query="/api/instances?view=full&filter="+filters
102
+
103
+ instances = Connection.api16_call(query)
104
+
105
+ rescue Exception => e
106
+ puts e.message
107
+ end
108
+
109
+ return instances
110
+ end
111
+
112
+ #
113
+ # Provides a way to make an api1.6 call directly
114
+ #
115
+ def Connection.api16_call(query)
116
+
117
+ @retry = true
118
+ retries = 5
119
+ attempts = 0
120
+ sleep_for = 20
121
+
122
+ begin
123
+ get = Net::HTTP::Get.new(query)
124
+ get['Cookie'] = @client.cookies.map { |key, value| "%s=%s" % [key, value] }.join(';')
125
+ get['X-Api_Version'] = '1.6'
126
+ get['X-Account'] = @client.account_id
127
+
128
+ http = Net::HTTP.new(@endpoint, 443)
129
+ http.use_ssl = true
130
+
131
+ Log.debug "Querying API for: #{query}"
132
+
133
+
134
+ while attempts < retries
135
+ if @retry
136
+ if attempts > 0
137
+ Log.debug "Retrying..."
138
+ sleep_time = sleep_for * attempts
139
+ # Add a random amount to avoid staggering calls
140
+ sleep_time += rand(15)
141
+
142
+ Log.debug "Sleeping between retries for #{sleep_time}"
143
+ sleep(sleep_time)
144
+ end
145
+
146
+ Log.debug "Attempt # #{attempts+1} at querying the API" unless attempts == 0
147
+
148
+ time = Benchmark.measure do
149
+ @response = http.request(get)
150
+ attempts += 1
151
+ end
152
+
153
+ Log.debug "API Request time: #{time.real} seconds"
154
+ Log.debug "[#{Chimp.get_job_uuid}] API Query was: #{query}"
155
+
156
+ # Validate API response
157
+ instances = validate_response(@response, query)
158
+ else
159
+ # We dont retry, exit the loop.
160
+ break
161
+ end
162
+ end
163
+
164
+ if attempts == retries
165
+ Chimp.set_failure(true)
166
+ instances = []
167
+ raise "[#{Chimp.get_job_uuid}] Api call failed more than #{retries} times."
168
+ end
169
+
170
+ rescue Exception => e
171
+ Log.debug "Catched exception on http request to the api"
172
+ Log.debug "#{e.message}"
173
+ Chimp.set_failure(true)
174
+
175
+ instances = []
176
+ attempts += 1
177
+ retry
178
+ end
179
+
180
+ Log.debug "[#{Chimp.get_job_uuid}] #{instances.count} instances matching"
181
+
182
+ return instances
183
+ end
184
+
185
+ #
186
+ # Verify the results are valid JSON
187
+ #
188
+ def Connection.validate_response(response, query)
189
+
190
+ #Log.debug "Validating API response"
191
+
192
+ resp_code = response.code
193
+ # handle response codes we want to work with (200 or 404) and verify json hash from github
194
+ if resp_code == "200" || resp_code == "404"
195
+ body_data = response.body
196
+ # verify json hash is valid and operate accordingly
197
+ begin
198
+ result = JSON.parse(body_data)
199
+ if result.is_a?(Array)
200
+ # Operate on a 200 or 404 with valid JSON response, catch error messages from github in json hash
201
+ if result.include? 'message'
202
+ raise "[CONTENT] Error: Problem with API request: '#{resp_code} #{response.body}'" #we know this checkout will fail (branch input, repo name, etc. wrong)
203
+ end
204
+ if result.include? 'Error'
205
+ Log.error "[CONTENT] Warning BAD CONTENT: Response content: '#{response.body}'."
206
+ return {} # Return an empty json
207
+ end
208
+ # extract the most recent commit on designated branch from hash
209
+ # Log.debug "We received a valid JSON response, therefore returning it."
210
+ @retry = false
211
+ return result
212
+ end
213
+ rescue JSON::ParserError
214
+ Log.error "Warning: Expected JSON response but was unable to parse!"
215
+ #Log.error "Warning: #{response.body}!"
216
+
217
+ return {} # Return an empty result
218
+ end
219
+
220
+ elsif resp_code == "502"
221
+ Log.debug "Api returned code: 502"
222
+ Log.debug "Query was: #{query}"
223
+
224
+ @retry = true
225
+
226
+ elsif resp_code == "500"
227
+ Log.debug "Api returned code: 500"
228
+ Log.debug "Query was: #{query}"
229
+
230
+ @retry = true
231
+
232
+ elsif resp_code == "504"
233
+ Log.debug "Api returned code: 504"
234
+ Log.debug "Query was: #{query}"
235
+
236
+ @retry = true
237
+
238
+ else
239
+ # We are here because response was not 200 or 404
240
+ # Any http response code that is not 200 / 404 / 500 / 502 should error out.
241
+ Log.error "ERROR: Got '#{resp_code} #{response.msg}' response from api! "
242
+ Log.error "Query was: #{query}"
243
+ raise "Couldnt contact the API"
244
+ return {}
245
+ end
246
+ end
247
+
248
+ end
249
+
250
+ #
251
+ # This class allows to check on the status of any of the tasks created.
252
+ #
253
+ class Task
254
+
255
+ attr_writer :tasker
256
+ attr_reader :tasker
257
+
258
+ def wait_for_state(desired_state, timeout=900)
259
+ while(timeout > 0)
260
+ state=self.tasker.show.summary
261
+ return true if self.state.match(desired_state)
262
+ friendly_url = "https://"+Connection.endpoint+"/audit_entries/"
263
+ friendly_url += self.href.split(/\//).last
264
+ friendly_url = friendly_url.gsub("ae-","")
265
+ # raise "FATAL error, #{self.tasker.show.summary}\n See Audit: API:#{self.href}, WWW:<a href='#{friendly_url}'>#{friendly_url}</a>\n " if self.state.match("failed")
266
+ raise "FATAL error, #{self.tasker.show.summary}\n See Audit: #{friendly_url}'\n " if self.state.match("failed")
267
+ sleep 30
268
+ timeout -= 30
269
+ end
270
+ raise "FATAL: Timeout waiting for Executable to complete. State was #{self.state}" if timeout <= 0
271
+ end
272
+
273
+ def wait_for_completed(timeout=900)
274
+ wait_for_state("completed", timeout)
275
+ end
276
+
277
+ def state
278
+ self.tasker.show.summary
279
+ end
280
+
281
+ def href
282
+ self.tasker.href
283
+ end
284
+ end
285
+
286
+ #
287
+ # This task contains parameters that describe a script/task to be executed
288
+ #
289
+ class Executable
290
+
291
+ attr_writer :params
292
+ attr_reader :params
293
+
294
+ def initialize
295
+ @params = {
296
+ "position"=>5,
297
+ "right_script"=>{
298
+ "created_at"=>"",
299
+ "href"=>"dummy_href",
300
+ "updated_at"=>"",
301
+ "version"=>4,
302
+ "is_head_version"=>false,
303
+ "script"=>"",
304
+ "name"=>"dummy_name",
305
+ "description"=>"dummy_description"
306
+ },
307
+ "recipe"=>nil,
308
+ "apply"=>"operational"
309
+ }
310
+ end
311
+
312
+ def href
313
+ @params['right_script']['href']
314
+ end
315
+ def name
316
+ @params['right_script']['name']
317
+ end
318
+ end
319
+
320
+ #
321
+ # This class holds all necessary information regarding an instance
322
+ # and provides a way of executing a script on it via the run_executable method.
323
+ #
324
+ class Server
325
+
326
+ attr_writer :params, :object
327
+ attr_reader :params, :object
328
+
329
+ attr_accessor :run_executable
330
+
331
+ def initialize
332
+ @params = {
333
+ "href" => "dummy href",
334
+ "current_instance_href" => nil,
335
+ "current-instance-href " => nil,
336
+ "name" => "dummy name",
337
+ "nickname" => "dummy nickname",
338
+ "ip_address" => nil,
339
+ "ip-address" => nil,
340
+ "private-ip-address" => nil,
341
+ "aws-id" => "",
342
+ "ec2-instance-type" => "",
343
+ "dns-name" => "",
344
+ "locked" => "",
345
+ "state" => "",
346
+ "datacenter" => nil
347
+ }
348
+ @object = nil
349
+ end
350
+
351
+ def href
352
+ @params['href']
353
+ end
354
+
355
+ def name
356
+ @params['name']
357
+ end
358
+
359
+ def nickname
360
+ @params['nickname']
361
+ end
362
+
363
+ def ip_address
364
+ @params['ip_address']
365
+ end
366
+
367
+ def encode_with(coder)
368
+ vars = instance_variables.map{|x| x.to_s}
369
+ vars = vars - ['@object']
370
+
371
+ vars.each do |var|
372
+ var_val = eval(var)
373
+ coder[var.gsub('@', '')] = var_val
374
+ end
375
+ end
376
+
377
+ #In order to run the task, we need to call run_executable on ourselves
378
+ def run_executable(exec, options)
379
+ script_href = "right_script_href="+exec.href
380
+ # Construct the parameters to pass for the inputs
381
+ params=options.collect { |k, v|
382
+ "&inputs[][name]=#{k}&inputs[][value]=#{v}" unless k == :ignore_lock
383
+ }.join('&')
384
+
385
+ if options[:ignore_lock]
386
+ params+="&ignore_lock=true"
387
+ end
388
+ # self is the actual Server object
389
+ task = self.object.run_executable(script_href + params)
390
+ return task
391
+ end
392
+ end
393
+ end
@@ -5,9 +5,9 @@ module Chimp
5
5
  #
6
6
  class ChimpQueue
7
7
  include Singleton
8
-
8
+
9
9
  attr_accessor :delay, :retry_count, :max_threads, :group
10
-
10
+
11
11
  def initialize
12
12
  @delay = 0
13
13
  @retry_count = 0
@@ -17,7 +17,7 @@ module Chimp
17
17
  @semaphore = Mutex.new
18
18
  self.reset!
19
19
  end
20
-
20
+
21
21
  #
22
22
  # Reset the queue and the :default group
23
23
  #
@@ -27,13 +27,13 @@ module Chimp
27
27
  @group = {}
28
28
  @group[:default] = ParallelExecutionGroup.new(:default)
29
29
  end
30
-
30
+
31
31
  #
32
32
  # Start up queue runners
33
33
  #
34
34
  def start
35
35
  self.sort_queues!
36
-
36
+
37
37
  for i in (1..max_threads)
38
38
  @threads << Thread.new(i) do
39
39
  worker = QueueWorker.new
@@ -43,7 +43,7 @@ module Chimp
43
43
  end
44
44
  end
45
45
  end
46
-
46
+
47
47
  #
48
48
  # Push a task into the queue
49
49
  #
@@ -52,7 +52,7 @@ module Chimp
52
52
  create_group(g) if not ChimpQueue[g]
53
53
  ChimpQueue[g].push(w) unless ChimpQueue[g].get_job(w.job_id)
54
54
  end
55
-
55
+
56
56
  def create_group(name, type = :parallel, concurrency = 1)
57
57
  Log.debug "Creating new execution group #{name} type=#{type} concurrency=#{concurrency}"
58
58
  new_group = ExecutionGroupFactory.from_type(type)
@@ -60,7 +60,7 @@ module Chimp
60
60
  new_group.concurrency = concurrency
61
61
  ChimpQueue[name] = new_group
62
62
  end
63
-
63
+
64
64
  #
65
65
  # Grab the oldest work item available
66
66
  #
@@ -70,29 +70,29 @@ module Chimp
70
70
  @group.values.each do |group|
71
71
  if group.ready?
72
72
  r = group.shift
73
- Log.debug "Shifting job '#{r.job_id}' from group '#{group.group_id}'"
73
+ Log.debug "Shifting job '#{r.job_id}' from group '#{group.group_id}'" unless r.nil?
74
74
  break
75
75
  end
76
- end
76
+ end
77
77
  end
78
78
  return(r)
79
79
  end
80
-
80
+
81
81
  #
82
82
  # Wait until a group is done
83
83
  #
84
84
  def wait_until_done(g, &block)
85
- while @group[g].running?
85
+ while @group[g].running?
86
86
  @threads.each do |t|
87
87
  t.join(1)
88
- yield
88
+ yield
89
89
  end
90
90
  end
91
91
  end
92
-
92
+
93
93
  #
94
94
  # Quit - empty the queue and wait for remaining jobs to complete
95
- #
95
+ #
96
96
  def quit
97
97
  i = 0
98
98
  @group.keys.each do |group|
@@ -106,20 +106,20 @@ module Chimp
106
106
  end
107
107
  end
108
108
  end
109
-
109
+
110
110
  @threads.each { |t| t.kill }
111
111
  puts " done."
112
112
  end
113
-
113
+
114
114
  #
115
115
  # Run all threads forever (used by chimpd)
116
116
  #
117
117
  def run_threads
118
- @threads.each do |t|
118
+ @threads.each do |t|
119
119
  t.join(5)
120
120
  end
121
121
  end
122
-
122
+
123
123
  #
124
124
  # return the total number of queued (non-executing) objects
125
125
  #
@@ -130,60 +130,60 @@ module Chimp
130
130
  end
131
131
  return(s)
132
132
  end
133
-
133
+
134
134
  #
135
135
  # Allow the groups to be accessed as ChimpQueue.group[:foo]
136
136
  #
137
137
  def self.[](group)
138
138
  return ChimpQueue.instance.group[group]
139
139
  end
140
-
140
+
141
141
  def self.[]=(k,v)
142
142
  ChimpQueue.instance.group[k] = v
143
143
  end
144
-
144
+
145
145
  #
146
146
  # Return an array of all jobs with the requested
147
147
  # status.
148
148
  #
149
149
  def get_jobs_by_status(status)
150
150
  r = []
151
- @group.values.each do |group|
151
+ @group.values.each do |group|
152
152
  v = group.get_jobs_by_status(status)
153
153
  if v != nil and v != []
154
154
  r += v
155
155
  end
156
156
  end
157
-
157
+
158
158
  return r
159
159
  end
160
-
160
+
161
161
  def get_job(id)
162
162
  jobs = self.get_jobs
163
-
163
+
164
164
  jobs.each do |j|
165
165
  return j if j.job_id == id
166
166
  end
167
167
  end
168
-
168
+
169
169
  def get_jobs
170
170
  r = []
171
171
  @group.values.each do |group|
172
172
  group.get_jobs.each { |job| r << job }
173
173
  end
174
-
174
+
175
175
  return r
176
176
  end
177
-
178
- #############################################################
177
+
178
+ #############################################################
179
179
  protected
180
-
180
+
181
181
  #
182
182
  # Sort all the things, er, queues
183
183
  #
184
184
  def sort_queues!
185
185
  return @group.values.each { |group| group.sort! }
186
186
  end
187
-
187
+
188
188
  end
189
189
  end
@@ -71,11 +71,10 @@ module Chimp
71
71
  return self.get_jobs.map do |task|
72
72
  next if task == nil
73
73
  next if task.server == nil
74
-
75
74
  {
76
75
  :job_id => task.job_id,
77
- :name => task.info,
78
- :host => task.server['nickname'] || task.server['name'],
76
+ :name => task.info[0],
77
+ :host => task.server.name,
79
78
  :status => task.status,
80
79
  :error => task.error,
81
80
  :total => self.get_total_execution_time(task.status, task.time_start, task.time_end),
@@ -99,7 +98,7 @@ module Chimp
99
98
  def sort!
100
99
  if @queue != nil
101
100
  @queue.sort! do |a,b|
102
- a.server['nickname'] <=> b.server['nickname']
101
+ a.server.nickname <=> b.server.nickname
103
102
  end
104
103
  end
105
104
  end
@@ -289,10 +288,6 @@ module Chimp
289
288
  @concurrency = 25
290
289
  end
291
290
 
292
- #
293
- # FIXME - we're not currently using the @concurrency setting to limit execution
294
- # due to an unknown bug...
295
- #
296
291
  def ready?
297
292
  return (get_jobs_by_status(Executor::STATUS_NONE).size > 0) # and get_jobs_by_status(Executor::STATUS_RUNNING).size < @concurrency)
298
293
  end
@@ -30,10 +30,6 @@ module Chimp
30
30
  sleep 1
31
31
  end
32
32
 
33
- #
34
- # the rest_connection gem raises RuntimeErrors so we need to
35
- # rescue Exception here
36
- #
37
33
  rescue Exception => ex
38
34
  Log.error "Exception in QueueWorker.run: #{ex}"
39
35
  Log.debug ex.inspect
@@ -112,12 +112,12 @@ jobs.each do |j|
112
112
 
113
113
  status = j.status
114
114
  id = j.job_id
115
-
116
- server_name = CGI::escapeHTML(j.server['nickname']) if j.server
115
+
116
+ server_name = CGI::escapeHTML(j.server.params['nickname']) if j.server
117
117
 
118
118
  if j.exec
119
- action = j.exec.to_s
120
- action = j.exec['right_script']['name'] if j.exec['right_script']
119
+ action = j.exec.params.to_s
120
+ action = j.exec.params['right_script']['name'] if j.exec.params['right_script']
121
121
  else
122
122
  action = "unknown"
123
123
  end
@@ -1,3 +1,3 @@
1
1
  module Chimp
2
- VERSION = "1.1.3"
2
+ VERSION = "2.0"
3
3
  end
data/lib/right_chimp.rb CHANGED
@@ -8,10 +8,18 @@ require 'singleton'
8
8
  require 'base64'
9
9
  require 'rake'
10
10
 
11
- require 'rest_connection'
12
- require 'rest-client'
13
11
  require 'progressbar'
14
12
  require 'json'
13
+ require 'yaml'
14
+
15
+ require 'highline/import'
16
+
17
+ require 'right_api_client'
18
+ require 'rest-client'
19
+ require 'logger'
20
+
21
+ require "benchmark"
22
+
15
23
 
16
24
  module Chimp
17
25
  require 'right_chimp/version'
@@ -29,4 +37,5 @@ module Chimp
29
37
  require 'right_chimp/exec/ExecSSH'
30
38
  require 'right_chimp/exec/ExecReport'
31
39
  require 'right_chimp/exec/ExecNoop'
40
+ require 'right_chimp/objects/ChimpObjects'
32
41
  end