right_chimp 1.1.3 → 2.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,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