bosh_cli 1.0.3 → 1.5.0.pre.1113

Sign up to get free protection for your applications and to get access to all the features.
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