bosh_cli 0.16

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.
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