bosh_cli 1.0.3 → 1.5.0.pre.1113

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 (144) hide show
  1. data/bin/bosh +0 -9
  2. data/lib/cli.rb +69 -64
  3. data/lib/cli/backup_destination_path.rb +33 -0
  4. data/lib/cli/base_command.rb +57 -56
  5. data/lib/cli/blob_manager.rb +12 -12
  6. data/lib/cli/changeset_helper.rb +6 -7
  7. data/lib/cli/client/director.rb +724 -0
  8. data/lib/cli/command_handler.rb +6 -7
  9. data/lib/cli/commands/backup.rb +39 -0
  10. data/lib/cli/commands/biff.rb +42 -21
  11. data/lib/cli/commands/blob_management.rb +1 -1
  12. data/lib/cli/commands/cloudcheck.rb +11 -13
  13. data/lib/cli/commands/deployment.rb +53 -37
  14. data/lib/cli/commands/help.rb +3 -2
  15. data/lib/cli/commands/job_management.rb +67 -103
  16. data/lib/cli/commands/job_rename.rb +6 -8
  17. data/lib/cli/commands/log_management.rb +78 -55
  18. data/lib/cli/commands/maintenance.rb +36 -30
  19. data/lib/cli/commands/misc.rb +72 -51
  20. data/lib/cli/commands/package.rb +2 -2
  21. data/lib/cli/commands/property_management.rb +10 -12
  22. data/lib/cli/commands/release.rb +236 -133
  23. data/lib/cli/commands/snapshot.rb +93 -0
  24. data/lib/cli/commands/ssh.rb +216 -213
  25. data/lib/cli/commands/stemcell.rb +46 -34
  26. data/lib/cli/commands/task.rb +2 -2
  27. data/lib/cli/commands/user.rb +27 -3
  28. data/lib/cli/commands/vm.rb +28 -0
  29. data/lib/cli/commands/vms.rb +81 -23
  30. data/lib/cli/config.rb +6 -2
  31. data/lib/cli/core_ext.rb +31 -30
  32. data/lib/cli/deployment_helper.rb +134 -159
  33. data/lib/cli/deployment_manifest.rb +66 -0
  34. data/lib/cli/deployment_manifest_compiler.rb +0 -3
  35. data/lib/cli/event_log_renderer.rb +10 -10
  36. data/lib/cli/file_with_progress_bar.rb +52 -0
  37. data/lib/cli/job_builder.rb +1 -1
  38. data/lib/cli/job_command_args.rb +23 -0
  39. data/lib/cli/job_property_collection.rb +4 -7
  40. data/lib/cli/job_property_validator.rb +22 -12
  41. data/lib/cli/job_state.rb +54 -0
  42. data/lib/cli/line_wrap.rb +54 -0
  43. data/lib/cli/packaging_helper.rb +10 -10
  44. data/lib/cli/release.rb +18 -15
  45. data/lib/cli/release_builder.rb +9 -4
  46. data/lib/cli/release_compiler.rb +9 -9
  47. data/lib/cli/release_tarball.rb +3 -6
  48. data/lib/cli/resurrection.rb +31 -0
  49. data/lib/cli/runner.rb +56 -30
  50. data/lib/cli/stemcell.rb +25 -10
  51. data/lib/cli/task_log_renderer.rb +1 -1
  52. data/lib/cli/task_tracker.rb +10 -9
  53. data/lib/cli/validation.rb +3 -1
  54. data/lib/cli/version.rb +1 -1
  55. data/lib/cli/version_calc.rb +5 -18
  56. data/lib/cli/versions_index.rb +1 -1
  57. data/lib/cli/vm_state.rb +43 -0
  58. data/lib/cli/yaml_helper.rb +26 -35
  59. metadata +75 -208
  60. data/Rakefile +0 -56
  61. data/lib/cli/director.rb +0 -628
  62. data/spec/assets/biff/bad_gateway_config.yml +0 -28
  63. data/spec/assets/biff/good_simple_config.yml +0 -63
  64. data/spec/assets/biff/good_simple_golden_config.yml +0 -63
  65. data/spec/assets/biff/good_simple_template.erb +0 -69
  66. data/spec/assets/biff/ip_out_of_range.yml +0 -63
  67. data/spec/assets/biff/multiple_subnets_config.yml +0 -40
  68. data/spec/assets/biff/network_only_template.erb +0 -34
  69. data/spec/assets/biff/no_cc_config.yml +0 -27
  70. data/spec/assets/biff/no_range_config.yml +0 -27
  71. data/spec/assets/biff/no_subnet_config.yml +0 -16
  72. data/spec/assets/biff/ok_network_config.yml +0 -30
  73. data/spec/assets/biff/properties_template.erb +0 -6
  74. data/spec/assets/config/atmos/config/final.yml +0 -6
  75. data/spec/assets/config/atmos/config/private.yml +0 -4
  76. data/spec/assets/config/bad-providers/config/final.yml +0 -5
  77. data/spec/assets/config/bad-providers/config/private.yml +0 -4
  78. data/spec/assets/config/deprecation/config/final.yml +0 -5
  79. data/spec/assets/config/deprecation/config/private.yml +0 -2
  80. data/spec/assets/config/local/config/final.yml +0 -5
  81. data/spec/assets/config/local/config/private.yml +0 -1
  82. data/spec/assets/config/s3/config/final.yml +0 -5
  83. data/spec/assets/config/s3/config/private.yml +0 -5
  84. data/spec/assets/config/swift-hp/config/final.yml +0 -6
  85. data/spec/assets/config/swift-hp/config/private.yml +0 -7
  86. data/spec/assets/config/swift-rackspace/config/final.yml +0 -6
  87. data/spec/assets/config/swift-rackspace/config/private.yml +0 -6
  88. data/spec/assets/deployment.MF +0 -0
  89. data/spec/assets/plugins/bosh/cli/commands/echo.rb +0 -43
  90. data/spec/assets/plugins/bosh/cli/commands/ruby.rb +0 -24
  91. data/spec/assets/release/jobs/cacher.tgz +0 -0
  92. data/spec/assets/release/jobs/cacher/config/file1.conf +0 -0
  93. data/spec/assets/release/jobs/cacher/config/file2.conf +0 -0
  94. data/spec/assets/release/jobs/cacher/job.MF +0 -6
  95. data/spec/assets/release/jobs/cacher/monit +0 -1
  96. data/spec/assets/release/jobs/cleaner.tgz +0 -0
  97. data/spec/assets/release/jobs/cleaner/job.MF +0 -4
  98. data/spec/assets/release/jobs/cleaner/monit +0 -1
  99. data/spec/assets/release/jobs/sweeper.tgz +0 -0
  100. data/spec/assets/release/jobs/sweeper/config/test.conf +0 -1
  101. data/spec/assets/release/jobs/sweeper/job.MF +0 -5
  102. data/spec/assets/release/jobs/sweeper/monit +0 -1
  103. data/spec/assets/release/packages/mutator.tar.gz +0 -0
  104. data/spec/assets/release/packages/stuff.tgz +0 -0
  105. data/spec/assets/release/release.MF +0 -17
  106. data/spec/assets/release_invalid_checksum.tgz +0 -0
  107. data/spec/assets/release_invalid_jobs.tgz +0 -0
  108. data/spec/assets/release_no_name.tgz +0 -0
  109. data/spec/assets/release_no_version.tgz +0 -0
  110. data/spec/assets/stemcell/image +0 -1
  111. data/spec/assets/stemcell/stemcell.MF +0 -6
  112. data/spec/assets/stemcell_invalid_mf.tgz +0 -0
  113. data/spec/assets/stemcell_no_image.tgz +0 -0
  114. data/spec/assets/valid_release.tgz +0 -0
  115. data/spec/assets/valid_stemcell.tgz +0 -0
  116. data/spec/spec_helper.rb +0 -28
  117. data/spec/unit/base_command_spec.rb +0 -87
  118. data/spec/unit/biff_spec.rb +0 -172
  119. data/spec/unit/blob_manager_spec.rb +0 -288
  120. data/spec/unit/cache_spec.rb +0 -36
  121. data/spec/unit/cli_commands_spec.rb +0 -356
  122. data/spec/unit/config_spec.rb +0 -125
  123. data/spec/unit/core_ext_spec.rb +0 -81
  124. data/spec/unit/dependency_helper_spec.rb +0 -52
  125. data/spec/unit/deployment_manifest_compiler_spec.rb +0 -63
  126. data/spec/unit/deployment_manifest_spec.rb +0 -153
  127. data/spec/unit/director_spec.rb +0 -471
  128. data/spec/unit/director_task_spec.rb +0 -48
  129. data/spec/unit/event_log_renderer_spec.rb +0 -171
  130. data/spec/unit/hash_changeset_spec.rb +0 -73
  131. data/spec/unit/job_builder_spec.rb +0 -455
  132. data/spec/unit/job_property_collection_spec.rb +0 -111
  133. data/spec/unit/job_property_validator_spec.rb +0 -7
  134. data/spec/unit/job_rename_spec.rb +0 -200
  135. data/spec/unit/package_builder_spec.rb +0 -593
  136. data/spec/unit/release_builder_spec.rb +0 -120
  137. data/spec/unit/release_spec.rb +0 -173
  138. data/spec/unit/release_tarball_spec.rb +0 -29
  139. data/spec/unit/runner_spec.rb +0 -7
  140. data/spec/unit/ssh_spec.rb +0 -84
  141. data/spec/unit/stemcell_spec.rb +0 -17
  142. data/spec/unit/task_tracker_spec.rb +0 -131
  143. data/spec/unit/version_calc_spec.rb +0 -27
  144. data/spec/unit/versions_index_spec.rb +0 -144
@@ -55,21 +55,20 @@ module Bosh::Cli
55
55
 
56
56
  @children.each_pair do |k, v|
57
57
  if v.state == :mismatch
58
- out << indent + "#{k} type changed: ".yellow +
58
+ out << indent + "#{k} type changed: ".make_yellow +
59
59
  "#{v.old.class.to_s} -> #{v.new.class.to_s}"
60
60
  out << diff(v.old, v.new, indent + " ")
61
61
  elsif v.leaf?
62
62
  case v.state
63
63
  when :added
64
- out << indent + "added #{k}: ".yellow + v.new.to_s
64
+ out << indent + "added #{k}: ".make_yellow + v.new.to_s
65
65
  when :removed
66
- out << indent + "removed #{k}: ".red + v.old.to_s
66
+ out << indent + "removed #{k}: ".make_red + v.old.to_s
67
67
  when :changed
68
- out << indent + "changed #{k}: ".yellow
68
+ out << indent + "changed #{k}: ".make_yellow
69
69
  out << diff(v.old, v.new, indent + " ")
70
70
  end
71
71
  else
72
- # TODO: track renames?
73
72
  child_summary = v.summary(level + 1)
74
73
 
75
74
  unless child_summary.empty?
@@ -94,12 +93,12 @@ module Bosh::Cli
94
93
 
95
94
  removed.each do |line|
96
95
  line = line.inspect if line.is_a?(Hash)
97
- lines << "#{indent}- #{line}".red
96
+ lines << "#{indent}- #{line}".make_red
98
97
  end
99
98
 
100
99
  added.each do |line|
101
100
  line = line.inspect if line.is_a?(Hash)
102
- lines << "#{indent}+ #{line}".green
101
+ lines << "#{indent}+ #{line}".make_green
103
102
  end
104
103
 
105
104
  lines.join("\n")
@@ -0,0 +1,724 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+ require 'cli/version_calc'
3
+ require 'cli/core_ext'
4
+ require 'cli/errors'
5
+
6
+ require 'json'
7
+ require 'httpclient'
8
+ require 'base64'
9
+ require 'cli/file_with_progress_bar'
10
+
11
+ module Bosh
12
+ module Cli
13
+ module Client
14
+ class Director
15
+ include Bosh::Cli::VersionCalc
16
+
17
+ DIRECTOR_HTTP_ERROR_CODES = [400, 403, 404, 500]
18
+
19
+ API_TIMEOUT = 86400 * 3
20
+ CONNECT_TIMEOUT = 30
21
+
22
+ attr_reader :director_uri
23
+
24
+ # @return [String]
25
+ attr_accessor :user
26
+
27
+ # @return [String]
28
+ attr_accessor :password
29
+
30
+ # Options can include:
31
+ # * :no_track => true - do not use +TaskTracker+ for long-running
32
+ # +request_and_track+ calls
33
+ def initialize(director_uri, user = nil, password = nil, options = {})
34
+ if director_uri.nil? || director_uri =~ /^\s*$/
35
+ raise DirectorMissing, 'no director URI given'
36
+ end
37
+
38
+ @director_uri = URI.parse(director_uri)
39
+ @director_ip = Resolv.getaddresses(@director_uri.host).last
40
+ @scheme = @director_uri.scheme
41
+ @port = @director_uri.port
42
+ @user = user
43
+ @password = password
44
+ @track_tasks = !options.delete(:no_track)
45
+ @num_retries = options.fetch(:num_retries, 5)
46
+ @retry_wait_interval = options.fetch(:retry_wait_interval, 5)
47
+ end
48
+
49
+ def uuid
50
+ @uuid ||= get_status['uuid']
51
+ end
52
+
53
+ def exists?
54
+ get_status
55
+ true
56
+ rescue AuthError
57
+ true # For compatibility with directors that return 401 for /info
58
+ rescue DirectorError
59
+ false
60
+ end
61
+
62
+ def wait_until_ready
63
+ num_retries.times do
64
+ return if exists?
65
+ sleep retry_wait_interval
66
+ end
67
+ end
68
+
69
+ def authenticated?
70
+ status = get_status
71
+ # Backward compatibility: older directors return 200
72
+ # only for logged in users
73
+ return true if !status.has_key?('version')
74
+ !status['user'].nil?
75
+ rescue DirectorError
76
+ false
77
+ end
78
+
79
+ def create_user(username, password)
80
+ payload = JSON.generate('username' => username, 'password' => password)
81
+ response_code, _ = post('/users', 'application/json', payload)
82
+ response_code == 204
83
+ end
84
+
85
+ def delete_user(username)
86
+ response_code, _ = delete("/users/#{username}")
87
+ response_code == 204
88
+ end
89
+
90
+ def upload_stemcell(filename, options = {})
91
+ options = options.dup
92
+ options[:content_type] = 'application/x-compressed'
93
+
94
+ upload_and_track(:post, '/stemcells', filename, options)
95
+ end
96
+
97
+ def upload_remote_stemcell(stemcell_location, options = {})
98
+ options = options.dup
99
+ payload = { 'location' => stemcell_location }
100
+ options[:payload] = JSON.generate(payload)
101
+ options[:content_type] = 'application/json'
102
+
103
+ request_and_track(:post, '/stemcells', options)
104
+ end
105
+
106
+ def get_version
107
+ get_status['version']
108
+ end
109
+
110
+ def get_status
111
+ get_json('/info')
112
+ end
113
+
114
+ def list_stemcells
115
+ get_json('/stemcells')
116
+ end
117
+
118
+ def list_releases
119
+ get_json('/releases')
120
+ end
121
+
122
+ def list_deployments
123
+ get_json('/deployments')
124
+ end
125
+
126
+ def list_running_tasks(verbose = 1)
127
+ if version_less(get_version, '0.3.5')
128
+ get_json('/tasks?state=processing')
129
+ else
130
+ get_json('/tasks?state=processing,cancelling,queued' +
131
+ "&verbose=#{verbose}")
132
+ end
133
+ end
134
+
135
+ def list_recent_tasks(count = 30, verbose = 1)
136
+ get_json("/tasks?limit=#{count}&verbose=#{verbose}")
137
+ end
138
+
139
+ def get_release(name)
140
+ get_json("/releases/#{name}")
141
+ end
142
+
143
+ def match_packages(manifest_yaml)
144
+ url = '/packages/matches'
145
+ status, body = post(url, 'text/yaml', manifest_yaml)
146
+
147
+ if status == 200
148
+ JSON.parse(body)
149
+ else
150
+ err(parse_error_message(status, body))
151
+ end
152
+ end
153
+
154
+ def get_deployment(name)
155
+ _, body = get_json_with_status("/deployments/#{name}")
156
+ body
157
+ end
158
+
159
+ def list_vms(name)
160
+ _, body = get_json_with_status("/deployments/#{name}/vms")
161
+ body
162
+ end
163
+
164
+ def upload_release(filename, options = {})
165
+ options = options.dup
166
+ options[:content_type] = 'application/x-compressed'
167
+
168
+ upload_and_track(:post, '/releases', filename, options)
169
+ end
170
+
171
+ def rebase_release(filename, options = {})
172
+ options = options.dup
173
+ options[:content_type] = 'application/x-compressed'
174
+ upload_and_track(:post, '/releases?rebase=true', filename, options)
175
+ end
176
+
177
+ def upload_remote_release(release_location, options = {})
178
+ options = options.dup
179
+ payload = { 'location' => release_location }
180
+ options[:payload] = JSON.generate(payload)
181
+ options[:content_type] = 'application/json'
182
+
183
+ request_and_track(:post, '/releases', options)
184
+ end
185
+
186
+ def rebase_remote_release(release_location, options = {})
187
+ options = options.dup
188
+ payload = { 'location' => release_location }
189
+ options[:payload] = JSON.generate(payload)
190
+ options[:content_type] = 'application/json'
191
+
192
+ request_and_track(:post, '/releases?rebase=true', options)
193
+ end
194
+
195
+ def delete_stemcell(name, version, options = {})
196
+ options = options.dup
197
+ force = options.delete(:force)
198
+
199
+ url = "/stemcells/#{name}/#{version}"
200
+
201
+ extras = []
202
+ extras << 'force=true' if force
203
+
204
+ request_and_track(:delete, add_query_string(url, extras), options)
205
+ end
206
+
207
+ def delete_deployment(name, options = {})
208
+ options = options.dup
209
+ force = options.delete(:force)
210
+
211
+ url = "/deployments/#{name}"
212
+
213
+ extras = []
214
+ extras << 'force=true' if force
215
+
216
+ request_and_track(:delete, add_query_string(url, extras), options)
217
+ end
218
+
219
+ def delete_release(name, options = {})
220
+ options = options.dup
221
+ force = options.delete(:force)
222
+ version = options.delete(:version)
223
+
224
+ url = "/releases/#{name}"
225
+
226
+ extras = []
227
+ extras << 'force=true' if force
228
+ extras << "version=#{version}" if version
229
+
230
+ request_and_track(:delete, add_query_string(url, extras), options)
231
+ end
232
+
233
+ def deploy(manifest_yaml, options = {})
234
+ options = options.dup
235
+
236
+ recreate = options.delete(:recreate)
237
+ options[:content_type] = 'text/yaml'
238
+ options[:payload] = manifest_yaml
239
+
240
+ url = '/deployments'
241
+
242
+ extras = []
243
+ extras << 'recreate=true' if recreate
244
+
245
+ request_and_track(:post, add_query_string(url, extras), options)
246
+ end
247
+
248
+ def setup_ssh(deployment_name, job, index, user,
249
+ public_key, password, options = {})
250
+ options = options.dup
251
+
252
+ url = "/deployments/#{deployment_name}/ssh"
253
+
254
+ payload = {
255
+ 'command' => 'setup',
256
+ 'deployment_name' => deployment_name,
257
+ 'target' => {
258
+ 'job' => job,
259
+ 'indexes' => [index].compact
260
+ },
261
+ 'params' => {
262
+ 'user' => user,
263
+ 'public_key' => public_key,
264
+ 'password' => password
265
+ }
266
+ }
267
+
268
+ options[:payload] = JSON.generate(payload)
269
+ options[:content_type] = 'application/json'
270
+
271
+ request_and_track(:post, url, options)
272
+ end
273
+
274
+ def cleanup_ssh(deployment_name, job, user_regex, indexes, options = {})
275
+ options = options.dup
276
+
277
+ url = "/deployments/#{deployment_name}/ssh"
278
+
279
+ payload = {
280
+ 'command' => 'cleanup',
281
+ 'deployment_name' => deployment_name,
282
+ 'target' => {
283
+ 'job' => job,
284
+ 'indexes' => (indexes || []).compact
285
+ },
286
+ 'params' => { 'user_regex' => user_regex }
287
+ }
288
+
289
+ options[:payload] = JSON.generate(payload)
290
+ options[:content_type] = 'application/json'
291
+
292
+ request_and_track(:post, url, options)
293
+ end
294
+
295
+ def change_job_state(deployment_name, manifest_yaml,
296
+ job_name, index, new_state, options = {})
297
+ options = options.dup
298
+
299
+ url = "/deployments/#{deployment_name}/jobs/#{job_name}"
300
+ url += "/#{index}" if index
301
+ url += "?state=#{new_state}"
302
+
303
+ options[:payload] = manifest_yaml
304
+ options[:content_type] = 'text/yaml'
305
+
306
+ request_and_track(:put, url, options)
307
+ end
308
+
309
+ def change_vm_resurrection(deployment_name, job_name, index, value)
310
+ url = "/deployments/#{deployment_name}/jobs/#{job_name}/#{index}/resurrection"
311
+ payload = JSON.generate('resurrection_paused' => value)
312
+ put(url, 'application/json', payload)
313
+ end
314
+
315
+ def change_vm_resurrection_for_all(value)
316
+ url = "/resurrection"
317
+ payload = JSON.generate('resurrection_paused' => value)
318
+ put(url, 'application/json', payload)
319
+ end
320
+
321
+ def rename_job(deployment_name, manifest_yaml, old_name, new_name,
322
+ force = false, options = {})
323
+ options = options.dup
324
+
325
+ url = "/deployments/#{deployment_name}/jobs/#{old_name}"
326
+
327
+ extras = []
328
+ extras << "new_name=#{new_name}"
329
+ extras << 'force=true' if force
330
+
331
+ options[:content_type] = 'text/yaml'
332
+ options[:payload] = manifest_yaml
333
+
334
+ request_and_track(:put, add_query_string(url, extras), options)
335
+ end
336
+
337
+ def fetch_logs(deployment_name, job_name, index, log_type,
338
+ filters = nil, options = {})
339
+ options = options.dup
340
+
341
+ url = "/deployments/#{deployment_name}/jobs/#{job_name}"
342
+ url += "/#{index}/logs?type=#{log_type}&filters=#{filters}"
343
+
344
+ status, task_id = request_and_track(:get, url, options)
345
+
346
+ return nil if status != :done
347
+ get_task_result(task_id)
348
+ end
349
+
350
+ def fetch_vm_state(deployment_name, options = {})
351
+ options = options.dup
352
+
353
+ url = "/deployments/#{deployment_name}/vms?format=full"
354
+
355
+ status, task_id = request_and_track(:get, url, options)
356
+
357
+ if status != :done
358
+ raise DirectorError, 'Failed to fetch VMs information from director'
359
+ end
360
+
361
+ output = get_task_result_log(task_id)
362
+
363
+ output.to_s.split("\n").map do |vm_state|
364
+ JSON.parse(vm_state)
365
+ end
366
+ end
367
+
368
+ def download_resource(id)
369
+ status, tmp_file, _ = get("/resources/#{id}",
370
+ nil, nil, {}, :file => true)
371
+
372
+ if status == 200
373
+ tmp_file
374
+ else
375
+ raise DirectorError,
376
+ "Cannot download resource `#{id}': HTTP status #{status}"
377
+ end
378
+ end
379
+
380
+ def create_property(deployment_name, property_name, value)
381
+ url = "/deployments/#{deployment_name}/properties"
382
+ payload = JSON.generate('name' => property_name, 'value' => value)
383
+ post(url, 'application/json', payload)
384
+ end
385
+
386
+ def update_property(deployment_name, property_name, value)
387
+ url = "/deployments/#{deployment_name}/properties/#{property_name}"
388
+ payload = JSON.generate('value' => value)
389
+ put(url, 'application/json', payload)
390
+ end
391
+
392
+ def delete_property(deployment_name, property_name)
393
+ url = "/deployments/#{deployment_name}/properties/#{property_name}"
394
+ delete(url, 'application/json')
395
+ end
396
+
397
+ def get_property(deployment_name, property_name)
398
+ url = "/deployments/#{deployment_name}/properties/#{property_name}"
399
+ get_json_with_status(url)
400
+ end
401
+
402
+ def list_properties(deployment_name)
403
+ url = "/deployments/#{deployment_name}/properties"
404
+ get_json(url)
405
+ end
406
+
407
+ def take_snapshot(deployment_name, job = nil, index = nil, options = {})
408
+ options = options.dup
409
+
410
+ if job && index
411
+ url = "/deployments/#{deployment_name}/jobs/#{job}/#{index}/snapshots"
412
+ else
413
+ url = "/deployments/#{deployment_name}/snapshots"
414
+ end
415
+
416
+ request_and_track(:post, url, options)
417
+ end
418
+
419
+ def list_snapshots(deployment_name, job = nil, index = nil)
420
+ if job && index
421
+ url = "/deployments/#{deployment_name}/jobs/#{job}/#{index}/snapshots"
422
+ else
423
+ url = "/deployments/#{deployment_name}/snapshots"
424
+ end
425
+ get_json(url)
426
+ end
427
+
428
+ def delete_all_snapshots(deployment_name, options = {})
429
+ options = options.dup
430
+
431
+ url = "/deployments/#{deployment_name}/snapshots"
432
+
433
+ request_and_track(:delete, url, options)
434
+ end
435
+
436
+ def delete_snapshot(deployment_name, snapshot_cid, options = {})
437
+ options = options.dup
438
+
439
+ url = "/deployments/#{deployment_name}/snapshots/#{snapshot_cid}"
440
+
441
+ request_and_track(:delete, url, options)
442
+ end
443
+
444
+ def perform_cloud_scan(deployment_name, options = {})
445
+ options = options.dup
446
+ url = "/deployments/#{deployment_name}/scans"
447
+
448
+ request_and_track(:post, url, options)
449
+ end
450
+
451
+ def list_problems(deployment_name)
452
+ url = "/deployments/#{deployment_name}/problems"
453
+ get_json(url)
454
+ end
455
+
456
+ def apply_resolutions(deployment_name, resolutions, options = {})
457
+ options = options.dup
458
+
459
+ url = "/deployments/#{deployment_name}/problems"
460
+ options[:content_type] = 'application/json'
461
+ options[:payload] = JSON.generate('resolutions' => resolutions)
462
+
463
+ request_and_track(:put, url, options)
464
+ end
465
+
466
+ def get_current_time
467
+ _, _, headers = get('/info')
468
+ Time.parse(headers[:date]) rescue nil
469
+ end
470
+
471
+ def get_time_difference
472
+ # This includes the round-trip to director
473
+ ctime = get_current_time
474
+ ctime ? Time.now - ctime : 0
475
+ end
476
+
477
+ def get_task(task_id)
478
+ response_code, body = get("/tasks/#{task_id}")
479
+ raise AuthError if response_code == 401
480
+ raise MissingTask, "Task #{task_id} not found" if response_code == 404
481
+
482
+ if response_code != 200
483
+ raise TaskTrackError, "Got HTTP #{response_code} " +
484
+ 'while tracking task state'
485
+ end
486
+
487
+ JSON.parse(body)
488
+ rescue JSON::ParserError
489
+ raise TaskTrackError, 'Cannot parse task JSON, ' +
490
+ 'incompatible director version'
491
+ end
492
+
493
+ def get_task_state(task_id)
494
+ get_task(task_id)['state']
495
+ end
496
+
497
+ def get_task_result(task_id)
498
+ get_task(task_id)['result']
499
+ end
500
+
501
+ def get_task_result_log(task_id)
502
+ log, _ = get_task_output(task_id, 0, 'result')
503
+ log
504
+ end
505
+
506
+ def get_task_output(task_id, offset, log_type = nil)
507
+ uri = "/tasks/#{task_id}/output"
508
+ uri += "?type=#{log_type}" if log_type
509
+
510
+ headers = { 'Range' => "bytes=#{offset}-" }
511
+ response_code, body, headers = get(uri, nil, nil, headers)
512
+
513
+ if response_code == 206 &&
514
+ headers[:content_range].to_s =~ /bytes \d+-(\d+)\/\d+/
515
+ new_offset = $1.to_i + 1
516
+ else
517
+ new_offset = nil
518
+ # Delete the "Byte range unsatisfiable" message
519
+ body = nil if response_code == 416
520
+ end
521
+
522
+ # backward compatible with renaming soap log to cpi log
523
+ if response_code == 204 && log_type == 'cpi'
524
+ get_task_output(task_id, offset, 'soap')
525
+ else
526
+ [body, new_offset]
527
+ end
528
+ end
529
+
530
+ def cancel_task(task_id)
531
+ response_code, body = delete("/task/#{task_id}")
532
+ raise AuthError if response_code == 401
533
+ raise MissingTask, "No task##{task_id} found" if response_code == 404
534
+ [body, response_code]
535
+ end
536
+
537
+ def create_backup
538
+ request_and_track(:post, '/backups', {})
539
+ end
540
+
541
+ def fetch_backup
542
+ _, path, _ = get('/backups', nil, nil, {}, :file => true)
543
+ path
544
+ end
545
+
546
+ [:post, :put, :get, :delete].each do |method_name|
547
+ define_method method_name do |*args|
548
+ request(method_name, *args)
549
+ end
550
+ end
551
+
552
+ # Perform director HTTP request and track director task (if request
553
+ # started one).
554
+ # @param [Symbol] method HTTP method
555
+ # @param [String] uri URI
556
+ # @param [Hash] options Request and tracking options
557
+ def request_and_track(method, uri, options = {})
558
+ options = options.dup
559
+
560
+ content_type = options.delete(:content_type)
561
+ payload = options.delete(:payload)
562
+ track_opts = options
563
+
564
+ http_status, _, headers = request(method, uri, content_type, payload)
565
+ location = headers[:location]
566
+ redirected = [302, 303].include? http_status
567
+ task_id = nil
568
+
569
+ if redirected
570
+ if location =~ /\/tasks\/(\d+)\/?$/ # Looks like we received task URI
571
+ task_id = $1
572
+ if @track_tasks
573
+ tracker = Bosh::Cli::TaskTracker.new(self, task_id, track_opts)
574
+ status = tracker.track
575
+ else
576
+ status = :running
577
+ end
578
+ else
579
+ status = :non_trackable
580
+ end
581
+ else
582
+ status = :failed
583
+ end
584
+
585
+ [status, task_id]
586
+ end
587
+
588
+ def upload_and_track(method, uri, filename, options = {})
589
+ file = FileWithProgressBar.open(filename, 'r')
590
+ request_and_track(method, uri, options.merge(:payload => file))
591
+ ensure
592
+ file.stop_progress_bar if file
593
+ end
594
+
595
+ private
596
+
597
+ def request(method, uri, content_type = nil, payload = nil,
598
+ headers = {}, options = {})
599
+ headers = headers.dup
600
+ tmp_file = nil
601
+
602
+ headers['Content-Type'] = content_type if content_type
603
+
604
+ if options[:file]
605
+ tmp_file = File.open(File.join(Dir.mktmpdir, 'streamed-response'), 'w')
606
+
607
+ response_reader = lambda { |part| tmp_file.write(part) }
608
+ else
609
+ response_reader = nil
610
+ end
611
+
612
+ response = try_to_perform_http_request(method, "#{@scheme}://#{@director_ip}:#{@port}#{uri}", payload, headers, num_retries, retry_wait_interval, &response_reader)
613
+
614
+ if options[:file]
615
+ tmp_file.close
616
+ body = tmp_file.path
617
+ else
618
+ body = response.body
619
+ end
620
+
621
+ if DIRECTOR_HTTP_ERROR_CODES.include?(response.code)
622
+ if response.code == 404
623
+ raise ResourceNotFound, parse_error_message(response.code, body)
624
+ else
625
+ raise DirectorError, parse_error_message(response.code, body)
626
+ end
627
+ end
628
+
629
+ headers = response.headers.inject({}) do |hash, (k, v)|
630
+ # Some HTTP clients symbolize headers, some do not.
631
+ # To make it easier to switch between them, we try
632
+ # to symbolize them ourselves.
633
+ hash[k.to_s.downcase.gsub(/-/, '_').to_sym] = v
634
+ hash
635
+ end
636
+
637
+ [response.code, body, headers]
638
+
639
+
640
+ rescue SystemCallError => e
641
+ raise DirectorError, "System call error while talking to director: #{e}"
642
+ end
643
+
644
+ def parse_error_message(status, body)
645
+ parsed_body = JSON.parse(body.to_s) rescue {}
646
+
647
+ if parsed_body['code'] && parsed_body['description']
648
+ 'Error %s: %s' % [parsed_body['code'],
649
+ parsed_body['description']]
650
+ else
651
+ 'HTTP %s: %s' % [status, body]
652
+ end
653
+ end
654
+
655
+ def perform_http_request(method, uri, payload = nil, headers = {}, &block)
656
+ http_client = HTTPClient.new
657
+
658
+ http_client.send_timeout = API_TIMEOUT
659
+ http_client.receive_timeout = API_TIMEOUT
660
+ http_client.connect_timeout = CONNECT_TIMEOUT
661
+
662
+ http_client.ssl_config.verify_mode = OpenSSL::SSL::VERIFY_NONE
663
+ http_client.ssl_config.verify_callback = Proc.new {}
664
+
665
+ # HTTPClient#set_auth doesn't seem to work properly,
666
+ # injecting header manually instead.
667
+ if @user && @password
668
+ headers['Authorization'] = 'Basic ' +
669
+ Base64.encode64("#{@user}:#{@password}").strip
670
+ end
671
+
672
+ http_client.request(method, uri, :body => payload,
673
+ :header => headers, &block)
674
+
675
+ rescue HTTPClient::BadResponseError => e
676
+ err("Received bad HTTP response from director: #{e}")
677
+ rescue URI::Error, SocketError, SystemCallError
678
+ raise # We handle these upstream
679
+ rescue => e
680
+ # httpclient (sadly) doesn't have a generic exception
681
+ puts "Perform request #{method}, #{uri}, #{headers.inspect}, #{payload.inspect}"
682
+ err("REST API call exception: #{e}")
683
+ end
684
+
685
+ def try_to_perform_http_request(method, uri, payload, headers, num_retries, retry_wait_interval, &response_reader)
686
+ num_retries.downto(1) do |n|
687
+ begin
688
+ return perform_http_request(method, uri, payload, headers, &response_reader)
689
+ rescue URI::Error, SocketError, Errno::ECONNREFUSED => e
690
+ warning("cannot access director, trying #{n-1} more times...") if n != 1
691
+ raise DirectorInaccessible, "cannot access director (#{e.message})" if n == 1
692
+ sleep retry_wait_interval
693
+ end
694
+ end
695
+ end
696
+
697
+ def get_json(url)
698
+ status, body = get_json_with_status(url)
699
+ raise AuthError if status == 401
700
+ raise DirectorError, "Director HTTP #{status}" if status != 200
701
+ body
702
+ end
703
+
704
+ def get_json_with_status(url)
705
+ status, body, _ = get(url, 'application/json')
706
+ body = JSON.parse(body) if status == 200
707
+ [status, body]
708
+ rescue JSON::ParserError
709
+ raise DirectorError, "Cannot parse director response: #{body}"
710
+ end
711
+
712
+ def add_query_string(url, parts)
713
+ if parts.size > 0
714
+ "#{url}?#{parts.join('&')}"
715
+ else
716
+ url
717
+ end
718
+ end
719
+
720
+ attr_reader :num_retries, :retry_wait_interval
721
+ end
722
+ end
723
+ end
724
+ end