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.
- data/README +4 -0
- data/Rakefile +55 -0
- data/bin/bosh +17 -0
- data/lib/cli.rb +76 -0
- data/lib/cli/cache.rb +44 -0
- data/lib/cli/changeset_helper.rb +142 -0
- data/lib/cli/command_definition.rb +52 -0
- data/lib/cli/commands/base.rb +245 -0
- data/lib/cli/commands/biff.rb +300 -0
- data/lib/cli/commands/blob.rb +125 -0
- data/lib/cli/commands/cloudcheck.rb +169 -0
- data/lib/cli/commands/deployment.rb +147 -0
- data/lib/cli/commands/job.rb +42 -0
- data/lib/cli/commands/job_management.rb +117 -0
- data/lib/cli/commands/log_management.rb +81 -0
- data/lib/cli/commands/maintenance.rb +131 -0
- data/lib/cli/commands/misc.rb +240 -0
- data/lib/cli/commands/package.rb +112 -0
- data/lib/cli/commands/property_management.rb +125 -0
- data/lib/cli/commands/release.rb +469 -0
- data/lib/cli/commands/ssh.rb +271 -0
- data/lib/cli/commands/stemcell.rb +184 -0
- data/lib/cli/commands/task.rb +213 -0
- data/lib/cli/commands/user.rb +28 -0
- data/lib/cli/commands/vms.rb +53 -0
- data/lib/cli/config.rb +154 -0
- data/lib/cli/core_ext.rb +145 -0
- data/lib/cli/dependency_helper.rb +62 -0
- data/lib/cli/deployment_helper.rb +263 -0
- data/lib/cli/deployment_manifest_compiler.rb +28 -0
- data/lib/cli/director.rb +633 -0
- data/lib/cli/director_task.rb +64 -0
- data/lib/cli/errors.rb +48 -0
- data/lib/cli/event_log_renderer.rb +351 -0
- data/lib/cli/job_builder.rb +226 -0
- data/lib/cli/package_builder.rb +254 -0
- data/lib/cli/packaging_helper.rb +248 -0
- data/lib/cli/release.rb +176 -0
- data/lib/cli/release_builder.rb +215 -0
- data/lib/cli/release_compiler.rb +178 -0
- data/lib/cli/release_tarball.rb +272 -0
- data/lib/cli/runner.rb +771 -0
- data/lib/cli/stemcell.rb +83 -0
- data/lib/cli/task_log_renderer.rb +40 -0
- data/lib/cli/templates/help_message.erb +75 -0
- data/lib/cli/validation.rb +42 -0
- data/lib/cli/version.rb +7 -0
- data/lib/cli/version_calc.rb +48 -0
- data/lib/cli/versions_index.rb +126 -0
- data/lib/cli/yaml_helper.rb +62 -0
- data/spec/assets/biff/bad_gateway_config.yml +28 -0
- data/spec/assets/biff/good_simple_config.yml +63 -0
- data/spec/assets/biff/good_simple_golden_config.yml +63 -0
- data/spec/assets/biff/good_simple_template.erb +69 -0
- data/spec/assets/biff/multiple_subnets_config.yml +40 -0
- data/spec/assets/biff/network_only_template.erb +34 -0
- data/spec/assets/biff/no_cc_config.yml +27 -0
- data/spec/assets/biff/no_range_config.yml +27 -0
- data/spec/assets/biff/no_subnet_config.yml +16 -0
- data/spec/assets/biff/ok_network_config.yml +30 -0
- data/spec/assets/biff/properties_template.erb +6 -0
- data/spec/assets/deployment.MF +0 -0
- data/spec/assets/plugins/bosh/cli/commands/echo.rb +43 -0
- data/spec/assets/plugins/bosh/cli/commands/ruby.rb +24 -0
- data/spec/assets/release/jobs/cacher.tgz +0 -0
- data/spec/assets/release/jobs/cacher/config/file1.conf +0 -0
- data/spec/assets/release/jobs/cacher/config/file2.conf +0 -0
- data/spec/assets/release/jobs/cacher/job.MF +6 -0
- data/spec/assets/release/jobs/cacher/monit +1 -0
- data/spec/assets/release/jobs/cleaner.tgz +0 -0
- data/spec/assets/release/jobs/cleaner/job.MF +4 -0
- data/spec/assets/release/jobs/cleaner/monit +1 -0
- data/spec/assets/release/jobs/sweeper.tgz +0 -0
- data/spec/assets/release/jobs/sweeper/config/test.conf +1 -0
- data/spec/assets/release/jobs/sweeper/job.MF +5 -0
- data/spec/assets/release/jobs/sweeper/monit +1 -0
- data/spec/assets/release/packages/mutator.tar.gz +0 -0
- data/spec/assets/release/packages/stuff.tgz +0 -0
- data/spec/assets/release/release.MF +17 -0
- data/spec/assets/release_invalid_checksum.tgz +0 -0
- data/spec/assets/release_invalid_jobs.tgz +0 -0
- data/spec/assets/release_no_name.tgz +0 -0
- data/spec/assets/release_no_version.tgz +0 -0
- data/spec/assets/stemcell/image +1 -0
- data/spec/assets/stemcell/stemcell.MF +6 -0
- data/spec/assets/stemcell_invalid_mf.tgz +0 -0
- data/spec/assets/stemcell_no_image.tgz +0 -0
- data/spec/assets/valid_release.tgz +0 -0
- data/spec/assets/valid_stemcell.tgz +0 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/unit/base_command_spec.rb +66 -0
- data/spec/unit/biff_spec.rb +135 -0
- data/spec/unit/cache_spec.rb +36 -0
- data/spec/unit/cli_commands_spec.rb +481 -0
- data/spec/unit/config_spec.rb +139 -0
- data/spec/unit/core_ext_spec.rb +77 -0
- data/spec/unit/dependency_helper_spec.rb +52 -0
- data/spec/unit/deployment_manifest_compiler_spec.rb +63 -0
- data/spec/unit/director_spec.rb +511 -0
- data/spec/unit/director_task_spec.rb +48 -0
- data/spec/unit/event_log_renderer_spec.rb +171 -0
- data/spec/unit/hash_changeset_spec.rb +73 -0
- data/spec/unit/job_builder_spec.rb +454 -0
- data/spec/unit/package_builder_spec.rb +567 -0
- data/spec/unit/release_builder_spec.rb +65 -0
- data/spec/unit/release_spec.rb +66 -0
- data/spec/unit/release_tarball_spec.rb +33 -0
- data/spec/unit/runner_spec.rb +140 -0
- data/spec/unit/ssh_spec.rb +78 -0
- data/spec/unit/stemcell_spec.rb +17 -0
- data/spec/unit/version_calc_spec.rb +27 -0
- data/spec/unit/versions_index_spec.rb +132 -0
- 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
|
data/lib/cli/director.rb
ADDED
|
@@ -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
|