bosh_cli 0.18 → 0.19

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.
@@ -16,7 +16,7 @@ module Bosh::Cli::Command
16
16
  name = manifest["name"]
17
17
  end
18
18
 
19
- say("Deployment `#{name.green}'")
19
+ say("Deployment #{name.green}")
20
20
 
21
21
  vms = director.fetch_vm_state(name)
22
22
  err("No VMs") if vms.size == 0
@@ -30,7 +30,7 @@ module Bosh::Cli::Command
30
30
 
31
31
  vms_table = table do |t|
32
32
  headings = ["Job/index", "State", "Resource Pool", "IPs"]
33
- headings += ["Agent ID", "CID"] if show_full_stats
33
+ headings += ["CID", "Agent ID"] if show_full_stats
34
34
 
35
35
  t.headings = headings
36
36
 
@@ -8,6 +8,7 @@ module Bosh::Cli
8
8
  attr_accessor :colorize
9
9
  attr_accessor :output
10
10
  attr_accessor :interactive
11
+ attr_accessor :cache
11
12
  end
12
13
 
13
14
  def initialize(filename, work_dir = Dir.pwd)
@@ -45,15 +45,40 @@ module Bosh::Cli
45
45
  manifest = YAML.load(manifest_yaml)
46
46
  end
47
47
 
48
- if manifest["name"].blank? || manifest["release"].blank? ||
49
- manifest["director_uuid"].blank?
48
+ if manifest["name"].blank? || manifest["director_uuid"].blank?
50
49
  err("Invalid manifest `#{File.basename(deployment)}': " +
51
- "name, release and director UUID are all required")
50
+ "name and director UUID are required")
51
+ end
52
+
53
+ if manifest["release"].blank? && manifest["releases"].blank?
54
+ err("Deployment manifest doesn't have release information: '" +
55
+ "please add 'release' or 'releases' section")
52
56
  end
53
57
 
54
58
  options[:yaml] ? manifest_yaml : manifest
55
59
  end
56
60
 
61
+ # Check if the 2 deployments are different.
62
+ # Print out a summary if "show" is true.
63
+ def deployment_changed?(current_manifest, manifest, show=true)
64
+ diff = Bosh::Cli::HashChangeset.new
65
+ diff.add_hash(normalize_deployment_manifest(manifest), :new)
66
+ diff.add_hash(normalize_deployment_manifest(current_manifest), :old)
67
+ changed = diff.changed?
68
+
69
+ if changed && show
70
+ @_diff_key_visited = { }
71
+ diff.keys.each do |key|
72
+ unless @_diff_key_visited[key]
73
+ print_summary(diff, key)
74
+ nl
75
+ end
76
+ end
77
+ end
78
+
79
+ changed
80
+ end
81
+
57
82
  # Interactive walkthrough of deployment changes,
58
83
  # expected to bail out of CLI using 'cancel_deployment'
59
84
  # if something goes wrong, so it doesn't need to have
@@ -94,23 +119,19 @@ module Bosh::Cli
94
119
  return false
95
120
  end
96
121
 
97
- print_summary(diff, :release)
122
+ if diff[:release]
123
+ print_summary(diff, :release)
124
+ warn_about_release_changes(diff[:release])
125
+ nl
126
+ end
98
127
 
99
- if diff[:release][:name].changed?
100
- say("Release name has changed: %s -> %s".red % [
101
- diff[:release][:name].old, diff[:release][:name].new])
102
- unless confirmed?("This is very serious and potentially destructive " +
103
- "change. ARE YOU SURE YOU WANT TO DO IT?")
104
- cancel_deployment
105
- end
106
- elsif diff[:release][:version].changed?
107
- say("Release version has changed: %s -> %s".yellow % [
108
- diff[:release][:version].old, diff[:release][:version].new])
109
- unless confirmed?("Are you sure you want to deploy this version?")
110
- cancel_deployment
128
+ if diff[:releases]
129
+ print_summary(diff, :releases)
130
+ diff[:releases].each do |release_diff|
131
+ warn_about_release_changes(release_diff)
111
132
  end
133
+ nl
112
134
  end
113
- nl
114
135
 
115
136
  print_summary(diff, :compilation)
116
137
  nl
@@ -125,12 +146,12 @@ module Bosh::Cli
125
146
 
126
147
  diff[:resource_pools].each do |pool|
127
148
  old_stemcells << {
128
- :name => pool[:stemcell][:name].old,
129
- :version => pool[:stemcell][:version].old
149
+ :name => pool[:stemcell][:name].old,
150
+ :version => pool[:stemcell][:version].old
130
151
  }
131
152
  new_stemcells << {
132
- :name => pool[:stemcell][:name].new,
133
- :version => pool[:stemcell][:version].new
153
+ :name => pool[:stemcell][:name].new,
154
+ :version => pool[:stemcell][:version].new
134
155
  }
135
156
  end
136
157
 
@@ -214,7 +235,9 @@ module Bosh::Cli
214
235
  def normalize_deployment_manifest(manifest)
215
236
  normalized = manifest.dup
216
237
 
217
- %w(networks jobs resource_pools).each do |section|
238
+ %w(releases networks jobs resource_pools).each do |section|
239
+ normalized[section] ||= []
240
+
218
241
  unless normalized[section].kind_of?(Array)
219
242
  manifest_error("#{section} is expected to be an array")
220
243
  end
@@ -259,5 +282,22 @@ module Bosh::Cli
259
282
  normalized
260
283
  end
261
284
 
285
+ def warn_about_release_changes(release_diff)
286
+ if release_diff[:name].changed?
287
+ say("Release name has changed: %s -> %s".red % [
288
+ release_diff[:name].old, release_diff[:name].new])
289
+ unless confirmed?("This is very serious and potentially destructive " +
290
+ "change. ARE YOU SURE YOU WANT TO DO IT?")
291
+ cancel_deployment
292
+ end
293
+ elsif release_diff[:version].changed?
294
+ say("Release version has changed: %s -> %s".yellow % [
295
+ release_diff[:version].old, release_diff[:version].new])
296
+ unless confirmed?("Are you sure you want to deploy this version?")
297
+ cancel_deployment
298
+ end
299
+ end
300
+ end
301
+
262
302
  end
263
303
  end
@@ -3,29 +3,27 @@
3
3
  module Bosh
4
4
  module Cli
5
5
  class Director
6
- include VersionCalc
6
+ include Bosh::Cli::VersionCalc
7
7
 
8
8
  DIRECTOR_HTTP_ERROR_CODES = [400, 403, 500]
9
9
 
10
- DEFAULT_MAX_POLLS = nil # Not limited
11
- DEFAULT_POLL_INTERVAL = 1
12
- API_TIMEOUT = 86400 * 3
13
- CONNECT_TIMEOUT = 30
10
+ API_TIMEOUT = 86400 * 3
11
+ CONNECT_TIMEOUT = 30
14
12
 
15
13
  attr_reader :director_uri
16
14
 
17
- # The current task number. An accessor so it can be used in tests.
18
- # @return [String] The task number.
19
- attr_accessor :current_running_task
20
-
21
15
  def initialize(director_uri, user = nil, password = nil)
22
16
  if director_uri.nil? || director_uri =~ /^\s*$/
23
17
  raise DirectorMissing, "no director URI given"
24
18
  end
25
19
 
26
20
  @director_uri = director_uri
27
- @user = user
28
- @password = password
21
+ @user = user
22
+ @password = password
23
+ end
24
+
25
+ def uuid
26
+ @uuid ||= get_status["uuid"]
29
27
  end
30
28
 
31
29
  def exists?
@@ -54,8 +52,7 @@ module Bosh
54
52
  end
55
53
 
56
54
  def upload_stemcell(filename)
57
- upload_and_track("/stemcells", "application/x-compressed",
58
- filename, :log_type => "event")
55
+ upload_and_track("/stemcells", "application/x-compressed", filename)
59
56
  end
60
57
 
61
58
  def get_version
@@ -112,15 +109,11 @@ module Bosh
112
109
  end
113
110
 
114
111
  def upload_release(filename)
115
- upload_and_track("/releases", "application/x-compressed",
116
- filename, :log_type => "event")
112
+ upload_and_track("/releases", "application/x-compressed", filename)
117
113
  end
118
114
 
119
115
  def delete_stemcell(name, version, options = {})
120
- track_options = { :log_type => "event" }
121
- track_options[:quiet] = options[:quiet] if options.has_key?(:quiet)
122
- request_and_track(:delete, "/stemcells/%s/%s" % [name, version],
123
- nil, nil, track_options)
116
+ request_and_track(:delete, "/stemcells/#{name}/#{version}")
124
117
  end
125
118
 
126
119
  def delete_deployment(name, options = {})
@@ -129,7 +122,7 @@ module Bosh
129
122
  query_params << "force=true" if options[:force]
130
123
  url += "?#{query_params.join("&")}" if query_params.size > 0
131
124
 
132
- request_and_track(:delete, url, nil, nil, :log_type => "event")
125
+ request_and_track(:delete, url)
133
126
  end
134
127
 
135
128
  def delete_release(name, options = {})
@@ -141,57 +134,56 @@ module Bosh
141
134
 
142
135
  url += "?#{query_params.join("&")}" if query_params.size > 0
143
136
 
144
- track_options = { :log_type => "event" }
145
- track_options[:quiet] = options[:quiet] if options.has_key?(:quiet)
146
-
147
- request_and_track(:delete, url, nil, nil, track_options)
137
+ request_and_track(:delete, url)
148
138
  end
149
139
 
150
140
  def deploy(manifest_yaml, options = {})
151
141
  url = "/deployments"
152
142
  url += "?recreate=true" if options[:recreate]
153
- request_and_track(:post, url, "text/yaml",
154
- manifest_yaml, :log_type => "event")
143
+ request_and_track(:post, url, "text/yaml", manifest_yaml)
155
144
  end
156
145
 
157
146
  def setup_ssh(deployment_name, job, index, user, public_key, password)
158
147
  url = "/deployments/#{deployment_name}/ssh"
159
- payload = JSON.generate("command" => "setup",
160
- "deployment_name" => deployment_name,
161
- "target" => {
162
- "job" => job,
163
- "indexes" => [index].compact
164
- },
165
- "params" => {
166
- "user" => user,
167
- "public_key" => public_key,
168
- "password" => password })
169
-
170
- results = ""
171
- output_stream = lambda do |entries|
172
- results << entries
173
- ""
174
- end
148
+ payload = {
149
+ "command" => "setup",
150
+ "deployment_name" => deployment_name,
151
+ "target" => {
152
+ "job" => job,
153
+ "indexes" => [index].compact
154
+ },
155
+ "params" => {
156
+ "user" => user,
157
+ "public_key" => public_key,
158
+ "password" => password
159
+ }
160
+ }
161
+
162
+ status, task_id, output =
163
+ request_and_track(:post, url, "application/json",
164
+ JSON.generate(payload), :log_type => "result")
175
165
 
176
- status, task_id = request_and_track(:post, url, "application/json",
177
- payload, :log_type => "result",
178
- :output_stream => output_stream)
179
166
  return nil if status != :done || task_id.nil?
180
- JSON.parse(results)
167
+
168
+ JSON.parse(output)
181
169
  end
182
170
 
183
171
  def cleanup_ssh(deployment_name, job, user_regex, indexes)
184
172
  indexes ||= []
185
173
  url = "/deployments/#{deployment_name}/ssh"
186
- payload = JSON.generate("command" => "cleanup",
187
- "deployment_name" => deployment_name,
188
- "target" => {
189
- "job" => job,
190
- "indexes" => indexes.compact
191
- },
192
- "params" => { "user_regex" => user_regex })
174
+
175
+ payload = {
176
+ "command" => "cleanup",
177
+ "deployment_name" => deployment_name,
178
+ "target" => {
179
+ "job" => job,
180
+ "indexes" => indexes.compact
181
+ },
182
+ "params" => { "user_regex" => user_regex }
183
+ }
184
+
193
185
  request_and_track(:post, url, "application/json",
194
- payload, :quiet => true)
186
+ JSON.generate(payload))
195
187
  end
196
188
 
197
189
  def change_job_state(deployment_name, manifest_yaml,
@@ -199,38 +191,43 @@ module Bosh
199
191
  url = "/deployments/#{deployment_name}/jobs/#{job_name}"
200
192
  url += "/#{index}" if index
201
193
  url += "?state=#{new_state}"
194
+ request_and_track(:put, url, "text/yaml", manifest_yaml)
195
+ end
196
+
197
+ def rename_job(deployment_name, manifest_yaml, old_name, new_name,
198
+ force = nil)
199
+ url = "/deployments/#{deployment_name}/jobs/#{old_name}"
200
+
201
+ rename_params = ["new_name=#{new_name}"]
202
+ rename_params << "force=true" if force
203
+
204
+ url += "?#{rename_params.join("&")}" if rename_params.size > 0
202
205
  request_and_track(:put, url, "text/yaml",
203
206
  manifest_yaml, :log_type => "event")
204
207
  end
205
208
 
206
209
  def fetch_logs(deployment_name, job_name, index, log_type, filters = nil)
207
- url = "/deployments/#{deployment_name}/jobs/#{job_name}" +
208
- "/#{index}/logs?type=#{log_type}&filters=#{filters}"
209
- status, task_id = request_and_track(:get, url, nil,
210
- nil, :log_type => "event")
210
+ url = "/deployments/#{deployment_name}/jobs/#{job_name}"
211
+ url += "/#{index}/logs?type=#{log_type}&filters=#{filters}"
212
+
213
+ status, task_id = request_and_track(:get, url)
211
214
  return nil if status != :done || task_id.nil?
212
215
  get_task_result(task_id)
213
216
  end
214
217
 
215
218
  def fetch_vm_state(deployment_name)
216
219
  url = "/deployments/#{deployment_name}/vms?format=full"
217
- vms = []
218
- # CLEANUP TODO output stream only being used for side effects
219
- output_stream = lambda do |vm_states|
220
- vm_states.to_s.split("\n").each do |vm_state|
221
- vms << JSON.parse(vm_state)
222
- end
223
- ""
224
- end
225
220
 
226
- status, task_id = request_and_track(:get, url, nil, nil,
227
- :log_type => "result",
228
- :output_stream => output_stream,
229
- :quiet => true)
221
+ status, task_id, output =
222
+ request_and_track(:get, url, nil, nil, :log_type => "result")
223
+
230
224
  if status != :done || task_id.nil?
231
225
  raise DirectorError, "Failed to fetch VMs information from director"
232
226
  end
233
- vms
227
+
228
+ output.to_s.split("\n").map do |vm_state|
229
+ JSON.parse(vm_state)
230
+ end
234
231
  end
235
232
 
236
233
  def download_resource(id)
@@ -241,7 +238,7 @@ module Bosh
241
238
  tmp_file
242
239
  else
243
240
  raise DirectorError, "Cannot download resource `#{id}': " +
244
- "HTTP status #{status}"
241
+ "HTTP status #{status}"
245
242
  end
246
243
  end
247
244
 
@@ -274,8 +271,7 @@ module Bosh
274
271
 
275
272
  def perform_cloud_scan(deployment_name)
276
273
  url = "/deployments/#{deployment_name}/scans"
277
- request_and_track(:post, url, nil, nil,
278
- :log_type => "event", :log_only => true)
274
+ request_and_track(:post, url)
279
275
  end
280
276
 
281
277
  def list_problems(deployment_name)
@@ -286,8 +282,7 @@ module Bosh
286
282
  def apply_resolutions(deployment_name, resolutions)
287
283
  url = "/deployments/#{deployment_name}/problems"
288
284
  request_and_track(:put, url, "application/json",
289
- JSON.generate("resolutions" => resolutions),
290
- :log_type => "event", :log_only => true)
285
+ JSON.generate("resolutions" => resolutions))
291
286
  end
292
287
 
293
288
  def get_current_time
@@ -296,7 +291,7 @@ module Bosh
296
291
  end
297
292
 
298
293
  def get_time_difference
299
- # This includes the roundtrip to director
294
+ # This includes the round-trip to director
300
295
  ctime = get_current_time
301
296
  ctime ? Time.now - ctime : 0
302
297
  end
@@ -308,13 +303,13 @@ module Bosh
308
303
 
309
304
  if response_code != 200
310
305
  raise TaskTrackError, "Got HTTP #{response_code} " +
311
- "while tracking task state"
306
+ "while tracking task state"
312
307
  end
313
308
 
314
309
  JSON.parse(body)
315
310
  rescue JSON::ParserError
316
311
  raise TaskTrackError, "Cannot parse task JSON, " +
317
- "incompatible director version"
312
+ "incompatible director version"
318
313
  end
319
314
 
320
315
  def get_task_state(task_id)
@@ -333,7 +328,7 @@ module Bosh
333
328
  response_code, body, headers = get(uri, nil, nil, headers)
334
329
 
335
330
  if response_code == 206 &&
336
- headers[:content_range].to_s =~ /bytes \d+-(\d+)\/\d+/
331
+ headers[:content_range].to_s =~ /bytes \d+-(\d+)\/\d+/
337
332
  new_offset = $1.to_i + 1
338
333
  else
339
334
  new_offset = nil
@@ -348,45 +343,26 @@ module Bosh
348
343
  [body, response_code]
349
344
  end
350
345
 
351
- ##
352
- # Cancels the task currently running.
353
- def cancel_current
354
- body, response_code = cancel_task(@current_running_task)
355
- if (200..299).include?(response_code)
356
- say("Cancelling task ##{@current_running_task}.".red)
357
- end
358
- end
359
-
360
- ##
361
- # Returns whether there is a task currently running.
362
- #
363
- # @return [Boolean] Whether there is a task currently running.
364
- def has_current?
365
- unless @current_running_task
366
- return false
367
- end
368
- task_state = get_task_state(@current_running_task)
369
- task_state == "queued" || task_state == "processing"
370
- end
371
-
372
346
  [:post, :put, :get, :delete].each do |method_name|
373
347
  define_method method_name do |*args|
374
348
  request(method_name, *args)
375
349
  end
376
350
  end
377
351
 
378
- def request_and_track(method, uri, content_type,
379
- payload = nil, options = {})
352
+ def request_and_track(method, uri, content_type = nil,
353
+ payload = nil,options = {})
380
354
  http_status, body, headers = request(method, uri, content_type, payload)
381
- location = headers[:location]
355
+ location = headers[:location]
382
356
  redirected = http_status == 302
383
- task_id = nil
357
+ task_id = nil
358
+ output = nil
384
359
 
385
360
  if redirected
386
361
  if location =~ /\/tasks\/(\d+)\/?$/ # Looks like we received task URI
387
362
  task_id = $1
388
- @current_running_task = task_id
389
- status = poll_task(task_id, options)
363
+ tracker = Bosh::Cli::TaskTracker.new(self, task_id, options)
364
+ status = tracker.track
365
+ output = tracker.output
390
366
  else
391
367
  status = :non_trackable
392
368
  end
@@ -394,7 +370,7 @@ module Bosh
394
370
  status = :failed
395
371
  end
396
372
 
397
- [status, task_id]
373
+ [status, task_id, output]
398
374
  end
399
375
 
400
376
  def upload_and_track(uri, content_type, filename, options = {})
@@ -405,95 +381,8 @@ module Bosh
405
381
  file.stop_progress_bar if file
406
382
  end
407
383
 
408
- def poll_task(task_id, options = {})
409
- polls = 0
410
-
411
- log_type = options[:log_type]
412
- poll_interval = options[:poll_interval] || DEFAULT_POLL_INTERVAL
413
- max_polls = options[:max_polls] || DEFAULT_MAX_POLLS
414
- start_time = Time.now
415
- quiet = options[:quiet]
416
- output_stream = options[:output_stream]
417
- log_only = options[:log_only]
418
-
419
- task = DirectorTask.new(self, task_id, log_type)
420
-
421
- unless quiet || log_only
422
- say("Tracking task output for task##{task_id}...")
423
- end
424
-
425
- renderer = Bosh::Cli::TaskLogRenderer.create_for_log_type(log_type)
426
- renderer.time_adjustment = get_time_difference
427
-
428
- no_output_yet = true
429
-
430
- while true
431
- polls += 1
432
- state, output = task.state, task.output
433
-
434
- if output
435
- no_output_yet = false
436
- output = output_stream.call(output) unless output_stream.nil?
437
- renderer.add_output(output) unless quiet
438
- end
439
-
440
- if no_output_yet && polls % 10 == 0 && !quiet && !log_only
441
- say("Task state is '#{state}', waiting for output...")
442
- end
443
-
444
- renderer.refresh
445
-
446
- if state == "done"
447
- result = :done
448
- break
449
- elsif state == "error"
450
- result = :error
451
- break
452
- elsif state == "cancelled"
453
- result = :cancelled
454
- break
455
- elsif !max_polls.nil? && polls >= max_polls
456
- result = :track_timeout
457
- break
458
- end
459
-
460
- sleep(poll_interval)
461
- end
462
-
463
- unless quiet
464
- renderer.add_output(task.flush_output)
465
- renderer.finish(state)
466
- end
467
-
468
- return result if quiet
469
- return result if log_only && result == :done
470
-
471
- if Bosh::Cli::Config.interactive &&
472
- log_type != "debug" && result == :error
473
- confirm = ask("\nThe task has returned an error status, " +
474
- "do you want to see debug log? [Yn]: ")
475
- if confirm.empty? || confirm =~ /y(es)?/i
476
- options.delete(:output_stream)
477
- poll_task(task_id, options.merge(:log_type => "debug"))
478
- else
479
- say("Please use 'bosh task #{task_id}' command " +
480
- "to see the debug log".red)
481
- result
482
- end
483
- else
484
- nl
485
- status = "Task #{task_id}: state is '#{state}'"
486
- duration = renderer.duration || (Time.now - start_time)
487
- if result == :done
488
- status += ", took #{format_time(duration).green} to complete"
489
- end
490
- say(status)
491
- result
492
- end
493
- end
494
-
495
384
  def request(method, uri, content_type = nil, payload = nil,
496
- headers = {}, options = { })
385
+ headers = {}, options = {})
497
386
  headers = headers.dup
498
387
  headers["Content-Type"] = content_type if content_type
499
388