bosh_cli 0.16

Sign up to get free protection for your applications and to get access to all the features.
Files changed (113) hide show
  1. data/README +4 -0
  2. data/Rakefile +55 -0
  3. data/bin/bosh +17 -0
  4. data/lib/cli.rb +76 -0
  5. data/lib/cli/cache.rb +44 -0
  6. data/lib/cli/changeset_helper.rb +142 -0
  7. data/lib/cli/command_definition.rb +52 -0
  8. data/lib/cli/commands/base.rb +245 -0
  9. data/lib/cli/commands/biff.rb +300 -0
  10. data/lib/cli/commands/blob.rb +125 -0
  11. data/lib/cli/commands/cloudcheck.rb +169 -0
  12. data/lib/cli/commands/deployment.rb +147 -0
  13. data/lib/cli/commands/job.rb +42 -0
  14. data/lib/cli/commands/job_management.rb +117 -0
  15. data/lib/cli/commands/log_management.rb +81 -0
  16. data/lib/cli/commands/maintenance.rb +131 -0
  17. data/lib/cli/commands/misc.rb +240 -0
  18. data/lib/cli/commands/package.rb +112 -0
  19. data/lib/cli/commands/property_management.rb +125 -0
  20. data/lib/cli/commands/release.rb +469 -0
  21. data/lib/cli/commands/ssh.rb +271 -0
  22. data/lib/cli/commands/stemcell.rb +184 -0
  23. data/lib/cli/commands/task.rb +213 -0
  24. data/lib/cli/commands/user.rb +28 -0
  25. data/lib/cli/commands/vms.rb +53 -0
  26. data/lib/cli/config.rb +154 -0
  27. data/lib/cli/core_ext.rb +145 -0
  28. data/lib/cli/dependency_helper.rb +62 -0
  29. data/lib/cli/deployment_helper.rb +263 -0
  30. data/lib/cli/deployment_manifest_compiler.rb +28 -0
  31. data/lib/cli/director.rb +633 -0
  32. data/lib/cli/director_task.rb +64 -0
  33. data/lib/cli/errors.rb +48 -0
  34. data/lib/cli/event_log_renderer.rb +351 -0
  35. data/lib/cli/job_builder.rb +226 -0
  36. data/lib/cli/package_builder.rb +254 -0
  37. data/lib/cli/packaging_helper.rb +248 -0
  38. data/lib/cli/release.rb +176 -0
  39. data/lib/cli/release_builder.rb +215 -0
  40. data/lib/cli/release_compiler.rb +178 -0
  41. data/lib/cli/release_tarball.rb +272 -0
  42. data/lib/cli/runner.rb +771 -0
  43. data/lib/cli/stemcell.rb +83 -0
  44. data/lib/cli/task_log_renderer.rb +40 -0
  45. data/lib/cli/templates/help_message.erb +75 -0
  46. data/lib/cli/validation.rb +42 -0
  47. data/lib/cli/version.rb +7 -0
  48. data/lib/cli/version_calc.rb +48 -0
  49. data/lib/cli/versions_index.rb +126 -0
  50. data/lib/cli/yaml_helper.rb +62 -0
  51. data/spec/assets/biff/bad_gateway_config.yml +28 -0
  52. data/spec/assets/biff/good_simple_config.yml +63 -0
  53. data/spec/assets/biff/good_simple_golden_config.yml +63 -0
  54. data/spec/assets/biff/good_simple_template.erb +69 -0
  55. data/spec/assets/biff/multiple_subnets_config.yml +40 -0
  56. data/spec/assets/biff/network_only_template.erb +34 -0
  57. data/spec/assets/biff/no_cc_config.yml +27 -0
  58. data/spec/assets/biff/no_range_config.yml +27 -0
  59. data/spec/assets/biff/no_subnet_config.yml +16 -0
  60. data/spec/assets/biff/ok_network_config.yml +30 -0
  61. data/spec/assets/biff/properties_template.erb +6 -0
  62. data/spec/assets/deployment.MF +0 -0
  63. data/spec/assets/plugins/bosh/cli/commands/echo.rb +43 -0
  64. data/spec/assets/plugins/bosh/cli/commands/ruby.rb +24 -0
  65. data/spec/assets/release/jobs/cacher.tgz +0 -0
  66. data/spec/assets/release/jobs/cacher/config/file1.conf +0 -0
  67. data/spec/assets/release/jobs/cacher/config/file2.conf +0 -0
  68. data/spec/assets/release/jobs/cacher/job.MF +6 -0
  69. data/spec/assets/release/jobs/cacher/monit +1 -0
  70. data/spec/assets/release/jobs/cleaner.tgz +0 -0
  71. data/spec/assets/release/jobs/cleaner/job.MF +4 -0
  72. data/spec/assets/release/jobs/cleaner/monit +1 -0
  73. data/spec/assets/release/jobs/sweeper.tgz +0 -0
  74. data/spec/assets/release/jobs/sweeper/config/test.conf +1 -0
  75. data/spec/assets/release/jobs/sweeper/job.MF +5 -0
  76. data/spec/assets/release/jobs/sweeper/monit +1 -0
  77. data/spec/assets/release/packages/mutator.tar.gz +0 -0
  78. data/spec/assets/release/packages/stuff.tgz +0 -0
  79. data/spec/assets/release/release.MF +17 -0
  80. data/spec/assets/release_invalid_checksum.tgz +0 -0
  81. data/spec/assets/release_invalid_jobs.tgz +0 -0
  82. data/spec/assets/release_no_name.tgz +0 -0
  83. data/spec/assets/release_no_version.tgz +0 -0
  84. data/spec/assets/stemcell/image +1 -0
  85. data/spec/assets/stemcell/stemcell.MF +6 -0
  86. data/spec/assets/stemcell_invalid_mf.tgz +0 -0
  87. data/spec/assets/stemcell_no_image.tgz +0 -0
  88. data/spec/assets/valid_release.tgz +0 -0
  89. data/spec/assets/valid_stemcell.tgz +0 -0
  90. data/spec/spec_helper.rb +25 -0
  91. data/spec/unit/base_command_spec.rb +66 -0
  92. data/spec/unit/biff_spec.rb +135 -0
  93. data/spec/unit/cache_spec.rb +36 -0
  94. data/spec/unit/cli_commands_spec.rb +481 -0
  95. data/spec/unit/config_spec.rb +139 -0
  96. data/spec/unit/core_ext_spec.rb +77 -0
  97. data/spec/unit/dependency_helper_spec.rb +52 -0
  98. data/spec/unit/deployment_manifest_compiler_spec.rb +63 -0
  99. data/spec/unit/director_spec.rb +511 -0
  100. data/spec/unit/director_task_spec.rb +48 -0
  101. data/spec/unit/event_log_renderer_spec.rb +171 -0
  102. data/spec/unit/hash_changeset_spec.rb +73 -0
  103. data/spec/unit/job_builder_spec.rb +454 -0
  104. data/spec/unit/package_builder_spec.rb +567 -0
  105. data/spec/unit/release_builder_spec.rb +65 -0
  106. data/spec/unit/release_spec.rb +66 -0
  107. data/spec/unit/release_tarball_spec.rb +33 -0
  108. data/spec/unit/runner_spec.rb +140 -0
  109. data/spec/unit/ssh_spec.rb +78 -0
  110. data/spec/unit/stemcell_spec.rb +17 -0
  111. data/spec/unit/version_calc_spec.rb +27 -0
  112. data/spec/unit/versions_index_spec.rb +132 -0
  113. metadata +338 -0
@@ -0,0 +1,28 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ module Bosh::Cli
4
+ class DeploymentManifestCompiler
5
+ attr_accessor :properties
6
+
7
+ def initialize(raw_manifest)
8
+ @raw_manifest = raw_manifest
9
+ @properties = {}
10
+ end
11
+
12
+ def property(name)
13
+ @properties[name] || raise(UndefinedProperty,
14
+ "Cannot resolve deployment property `#{name}'")
15
+ end
16
+
17
+ def result
18
+ # TODO: erb is just a fancy eval, so it's not very trustworthy,
19
+ # consider using more constrained template engine.
20
+ # Note that we use $SAFE=4 for ERB which is a strawman sandbox.
21
+ ERB.new(@raw_manifest, 4).result(binding.taint)
22
+ rescue SyntaxError => e
23
+ raise MalformedManifest,
24
+ "Deployment manifest contains a syntax error\n" + e.to_s
25
+ end
26
+ end
27
+
28
+ end
@@ -0,0 +1,633 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ module Bosh
4
+ module Cli
5
+ class Director
6
+ include VersionCalc
7
+
8
+ DIRECTOR_HTTP_ERROR_CODES = [400, 403, 500]
9
+
10
+ DEFAULT_MAX_POLLS = nil # Not limited
11
+ DEFAULT_POLL_INTERVAL = 1
12
+ API_TIMEOUT = 86400 * 3
13
+ CONNECT_TIMEOUT = 30
14
+
15
+ attr_reader :director_uri
16
+
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
+ def initialize(director_uri, user = nil, password = nil)
22
+ if director_uri.nil? || director_uri =~ /^\s*$/
23
+ raise DirectorMissing, "no director URI given"
24
+ end
25
+
26
+ @director_uri = director_uri
27
+ @user = user
28
+ @password = password
29
+ end
30
+
31
+ def exists?
32
+ get_status
33
+ true
34
+ rescue AuthError
35
+ true # For compatibility with directors that return 401 for /info
36
+ rescue DirectorError
37
+ false
38
+ end
39
+
40
+ def authenticated?
41
+ status = get_status
42
+ # Backward compatibility: older directors return 200
43
+ # only for logged in users
44
+ return true if !status.has_key?("version")
45
+ !status["user"].nil?
46
+ rescue DirectorError
47
+ false
48
+ end
49
+
50
+ def create_user(username, password)
51
+ payload = JSON.generate("username" => username, "password" => password)
52
+ response_code, body = post("/users", "application/json", payload)
53
+ response_code == 204
54
+ end
55
+
56
+ def upload_stemcell(filename)
57
+ upload_and_track("/stemcells", "application/x-compressed",
58
+ filename, :log_type => "event")
59
+ end
60
+
61
+ def get_version
62
+ get_status["version"]
63
+ end
64
+
65
+ def get_status
66
+ get_json("/info")
67
+ end
68
+
69
+ def list_stemcells
70
+ get_json("/stemcells")
71
+ end
72
+
73
+ def list_releases
74
+ get_json("/releases")
75
+ end
76
+
77
+ def list_deployments
78
+ get_json("/deployments")
79
+ end
80
+
81
+ def list_running_tasks
82
+ if version_less(get_version, "0.3.5")
83
+ get_json("/tasks?state=processing")
84
+ else
85
+ get_json("/tasks?state=processing,cancelling,queued")
86
+ end
87
+ end
88
+
89
+ def list_recent_tasks(count = 30)
90
+ count = [count.to_i, 100].min
91
+ get_json("/tasks?limit=#{count}")
92
+ end
93
+
94
+ def get_release(name)
95
+ get_json("/releases/#{name}")
96
+ end
97
+
98
+ def get_deployment(name)
99
+ status, body = get_json_with_status("/deployments/#{name}")
100
+ if status == 404
101
+ raise DeploymentNotFound, "Deployment `#{name}' not found"
102
+ end
103
+ body
104
+ end
105
+
106
+ def list_vms(name)
107
+ status, body = get_json_with_status("/deployments/#{name}/vms")
108
+ if status == 404
109
+ raise DeploymentNotFound, "Deployment `#{name}' not found"
110
+ end
111
+ body
112
+ end
113
+
114
+ def upload_release(filename)
115
+ upload_and_track("/releases", "application/x-compressed",
116
+ filename, :log_type => "event")
117
+ end
118
+
119
+ 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)
124
+ end
125
+
126
+ def delete_deployment(name, options = {})
127
+ url = "/deployments/#{name}"
128
+ query_params = []
129
+ query_params << "force=true" if options[:force]
130
+ url += "?#{query_params.join("&")}" if query_params.size > 0
131
+
132
+ request_and_track(:delete, url, nil, nil, :log_type => "event")
133
+ end
134
+
135
+ def delete_release(name, options = {})
136
+ url = "/releases/#{name}"
137
+
138
+ query_params = []
139
+ query_params << "force=true" if options[:force]
140
+ query_params << "version=#{options[:version]}" if options[:version]
141
+
142
+ url += "?#{query_params.join("&")}" if query_params.size > 0
143
+
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)
148
+ end
149
+
150
+ def deploy(manifest_yaml, options = {})
151
+ url = "/deployments"
152
+ url += "?recreate=true" if options[:recreate]
153
+ request_and_track(:post, url, "text/yaml",
154
+ manifest_yaml, :log_type => "event")
155
+ end
156
+
157
+ def setup_ssh(deployment_name, job, index, user, public_key, password)
158
+ 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
175
+
176
+ status, task_id = request_and_track(:post, url, "application/json",
177
+ payload, :log_type => "result",
178
+ :output_stream => output_stream)
179
+ return nil if status != :done || task_id.nil?
180
+ JSON.parse(results)
181
+ end
182
+
183
+ def cleanup_ssh(deployment_name, job, user_regex, indexes)
184
+ indexes ||= []
185
+ 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 })
193
+ request_and_track(:post, url, "application/json",
194
+ payload, :quiet => true)
195
+ end
196
+
197
+ def change_job_state(deployment_name, manifest_yaml,
198
+ job_name, index, new_state)
199
+ url = "/deployments/#{deployment_name}/jobs/#{job_name}"
200
+ url += "/#{index}" if index
201
+ url += "?state=#{new_state}"
202
+ request_and_track(:put, url, "text/yaml",
203
+ manifest_yaml, :log_type => "event")
204
+ end
205
+
206
+ 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")
211
+ return nil if status != :done || task_id.nil?
212
+ get_task_result(task_id)
213
+ end
214
+
215
+ def fetch_vm_state(deployment_name)
216
+ 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
+
226
+ status, task_id = request_and_track(:get, url, nil, nil,
227
+ :log_type => "result",
228
+ :output_stream => output_stream,
229
+ :quiet => true)
230
+ if status != :done || task_id.nil?
231
+ raise DirectorError, "Failed to fetch VMs information from director"
232
+ end
233
+ vms
234
+ end
235
+
236
+ def download_resource(id)
237
+ status, tmp_file, headers = get("/resources/#{id}", nil,
238
+ nil, {}, :file => true)
239
+
240
+ if status == 200
241
+ tmp_file
242
+ else
243
+ raise DirectorError, "Cannot download resource `#{id}': " +
244
+ "HTTP status #{status}"
245
+ end
246
+ end
247
+
248
+ def create_property(deployment_name, property_name, value)
249
+ url = "/deployments/#{deployment_name}/properties"
250
+ payload = JSON.generate("name" => property_name, "value" => value)
251
+ post(url, "application/json", payload)
252
+ end
253
+
254
+ def update_property(deployment_name, property_name, value)
255
+ url = "/deployments/#{deployment_name}/properties/#{property_name}"
256
+ payload = JSON.generate("value" => value)
257
+ put(url, "application/json", payload)
258
+ end
259
+
260
+ def delete_property(deployment_name, property_name)
261
+ url = "/deployments/#{deployment_name}/properties/#{property_name}"
262
+ delete(url, "application/json")
263
+ end
264
+
265
+ def get_property(deployment_name, property_name)
266
+ url = "/deployments/#{deployment_name}/properties/#{property_name}"
267
+ get_json_with_status(url)
268
+ end
269
+
270
+ def list_properties(deployment_name)
271
+ url = "/deployments/#{deployment_name}/properties"
272
+ get_json(url)
273
+ end
274
+
275
+ def perform_cloud_scan(deployment_name)
276
+ url = "/deployments/#{deployment_name}/scans"
277
+ request_and_track(:post, url, nil, nil,
278
+ :log_type => "event", :log_only => true)
279
+ end
280
+
281
+ def list_problems(deployment_name)
282
+ url = "/deployments/#{deployment_name}/problems"
283
+ get_json(url)
284
+ end
285
+
286
+ def apply_resolutions(deployment_name, resolutions)
287
+ url = "/deployments/#{deployment_name}/problems"
288
+ request_and_track(:put, url, "application/json",
289
+ JSON.generate("resolutions" => resolutions),
290
+ :log_type => "event", :log_only => true)
291
+ end
292
+
293
+ def get_current_time
294
+ status, body, headers = get("/info")
295
+ Time.parse(headers[:date]) rescue nil
296
+ end
297
+
298
+ def get_time_difference
299
+ # This includes the roundtrip to director
300
+ ctime = get_current_time
301
+ ctime ? Time.now - ctime : 0
302
+ end
303
+
304
+ def get_task(task_id)
305
+ response_code, body = get("/tasks/#{task_id}")
306
+ raise AuthError if response_code == 401
307
+ raise MissingTask, "Task #{task_id} not found" if response_code == 404
308
+
309
+ if response_code != 200
310
+ raise TaskTrackError, "Got HTTP #{response_code} " +
311
+ "while tracking task state"
312
+ end
313
+
314
+ JSON.parse(body)
315
+ rescue JSON::ParserError
316
+ raise TaskTrackError, "Cannot parse task JSON, " +
317
+ "incompatible director version"
318
+ end
319
+
320
+ def get_task_state(task_id)
321
+ get_task(task_id)["state"]
322
+ end
323
+
324
+ def get_task_result(task_id)
325
+ get_task(task_id)["result"]
326
+ end
327
+
328
+ def get_task_output(task_id, offset, log_type = nil)
329
+ uri = "/tasks/#{task_id}/output"
330
+ uri += "?type=#{log_type}" if log_type
331
+
332
+ headers = { "Range" => "bytes=#{offset}-" }
333
+ response_code, body, headers = get(uri, nil, nil, headers)
334
+
335
+ if response_code == 206 &&
336
+ headers[:content_range].to_s =~ /bytes \d+-(\d+)\/\d+/
337
+ new_offset = $1.to_i + 1
338
+ else
339
+ new_offset = nil
340
+ end
341
+ [body, new_offset]
342
+ end
343
+
344
+ def cancel_task(task_id)
345
+ response_code, body = delete("/task/#{task_id}")
346
+ raise AuthError if response_code == 401
347
+ raise MissingTask, "No task##{task_id} found" if response_code == 404
348
+ [body, response_code]
349
+ end
350
+
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
+ [:post, :put, :get, :delete].each do |method_name|
373
+ define_method method_name do |*args|
374
+ request(method_name, *args)
375
+ end
376
+ end
377
+
378
+ def request_and_track(method, uri, content_type,
379
+ payload = nil, options = {})
380
+ http_status, body, headers = request(method, uri, content_type, payload)
381
+ location = headers[:location]
382
+ redirected = http_status == 302
383
+ task_id = nil
384
+
385
+ if redirected
386
+ if location =~ /\/tasks\/(\d+)\/?$/ # Looks like we received task URI
387
+ task_id = $1
388
+ @current_running_task = task_id
389
+ status = poll_task(task_id, options)
390
+ else
391
+ status = :non_trackable
392
+ end
393
+ else
394
+ status = :failed
395
+ end
396
+
397
+ [status, task_id]
398
+ end
399
+
400
+ def upload_and_track(uri, content_type, filename, options = {})
401
+ file = FileWithProgressBar.open(filename, "r")
402
+ method = options[:method] || :post
403
+ request_and_track(method, uri, content_type, file, options)
404
+ ensure
405
+ file.stop_progress_bar if file
406
+ end
407
+
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
+ def request(method, uri, content_type = nil, payload = nil,
496
+ headers = {}, options = { })
497
+ headers = headers.dup
498
+ headers["Content-Type"] = content_type if content_type
499
+
500
+ if options[:file]
501
+ tmp_file = File.open(File.join(Dir.mktmpdir, "streamed-response"),
502
+ "w")
503
+
504
+ response_reader = lambda do |part|
505
+ tmp_file.write(part)
506
+ end
507
+ else
508
+ response_reader = nil
509
+ end
510
+
511
+ response = perform_http_request(method, @director_uri + uri,
512
+ payload, headers, &response_reader)
513
+
514
+ if options[:file]
515
+ tmp_file.close
516
+ body = tmp_file.path
517
+ else
518
+ body = response.body
519
+ end
520
+
521
+ if DIRECTOR_HTTP_ERROR_CODES.include?(response.code)
522
+ raise DirectorError, parse_error_message(response.code, body)
523
+ end
524
+
525
+ headers = response.headers.inject({}) do |hash, (k, v)|
526
+ # Some HTTP clients symbolize headers, some do not.
527
+ # To make it easier to switch between them, we try
528
+ # to symbolize them ourselves.
529
+ hash[k.to_s.downcase.gsub(/-/, "_").to_sym] = v
530
+ hash
531
+ end
532
+
533
+ [response.code, body, headers]
534
+
535
+ rescue URI::Error, SocketError, Errno::ECONNREFUSED => e
536
+ raise DirectorInaccessible,
537
+ "cannot access director (#{e.message})"
538
+ rescue SystemCallError => e
539
+ raise DirectorError, "System call error while talking to director: #{e}"
540
+ end
541
+
542
+ def parse_error_message(status, body)
543
+ parsed_body = JSON.parse(body.to_s)
544
+
545
+ if parsed_body["code"] && parsed_body["description"]
546
+ "Director error %s: %s" % [parsed_body["code"],
547
+ parsed_body["description"]]
548
+ else
549
+ "Director error (HTTP %s): %s" % [status, body]
550
+ end
551
+ rescue JSON::ParserError
552
+ "Director error (HTTP %s): %s" % [status, body]
553
+ end
554
+
555
+ private
556
+
557
+ def perform_http_request(method, uri, payload = nil, headers = {}, &block)
558
+ http_client = HTTPClient.new
559
+
560
+ http_client.send_timeout = API_TIMEOUT
561
+ http_client.receive_timeout = API_TIMEOUT
562
+ http_client.connect_timeout = CONNECT_TIMEOUT
563
+
564
+ # HTTPClient#set_auth doesn't seem to work properly,
565
+ # injecting header manually instead.
566
+ # TODO: consider using vanilla Net::HTTP
567
+ if @user && @password
568
+ headers["Authorization"] = "Basic " +
569
+ Base64.encode64("#{@user}:#{@password}").strip
570
+ end
571
+
572
+ http_client.request(method, uri, :body => payload,
573
+ :header => headers, &block)
574
+
575
+ rescue HTTPClient::BadResponseError => e
576
+ err("Received bad HTTP response from director: #{e}")
577
+ rescue URI::Error, SocketError, Errno::ECONNREFUSED, SystemCallError
578
+ raise # We handle these upstream
579
+ rescue => e
580
+ # httpclient (sadly) doesn't have a generic exception
581
+ err("REST API call exception: #{e}")
582
+ end
583
+
584
+ def get_json(url)
585
+ status, body = get_json_with_status(url)
586
+ raise AuthError if status == 401
587
+ raise DirectorError, "Director HTTP #{status}" if status != 200
588
+ body
589
+ end
590
+
591
+ def get_json_with_status(url)
592
+ status, body, headers = get(url, "application/json")
593
+ body = JSON.parse(body) if status == 200
594
+ [status, body]
595
+ rescue JSON::ParserError
596
+ raise DirectorError, "Cannot parse director response: #{body}"
597
+ end
598
+
599
+ end
600
+
601
+ class FileWithProgressBar < ::File
602
+ def progress_bar
603
+ return @progress_bar if @progress_bar
604
+ out = Bosh::Cli::Config.output || StringIO.new
605
+ @progress_bar = ProgressBar.new(File.basename(self.path),
606
+ File.size(self.path), out)
607
+ @progress_bar.file_transfer_mode
608
+ @progress_bar
609
+ end
610
+
611
+ def stop_progress_bar
612
+ progress_bar.halt unless progress_bar.finished?
613
+ end
614
+
615
+ def size
616
+ File.size(self.path)
617
+ end
618
+
619
+ def read(*args)
620
+ result = super(*args)
621
+
622
+ if result && result.size > 0
623
+ progress_bar.inc(result.size)
624
+ else
625
+ progress_bar.finish
626
+ end
627
+
628
+ result
629
+ end
630
+ end
631
+
632
+ end
633
+ end