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.
- data/bin/bosh +0 -9
- data/lib/cli.rb +69 -64
- data/lib/cli/backup_destination_path.rb +33 -0
- data/lib/cli/base_command.rb +57 -56
- data/lib/cli/blob_manager.rb +12 -12
- data/lib/cli/changeset_helper.rb +6 -7
- data/lib/cli/client/director.rb +724 -0
- data/lib/cli/command_handler.rb +6 -7
- data/lib/cli/commands/backup.rb +39 -0
- data/lib/cli/commands/biff.rb +42 -21
- data/lib/cli/commands/blob_management.rb +1 -1
- data/lib/cli/commands/cloudcheck.rb +11 -13
- data/lib/cli/commands/deployment.rb +53 -37
- data/lib/cli/commands/help.rb +3 -2
- data/lib/cli/commands/job_management.rb +67 -103
- data/lib/cli/commands/job_rename.rb +6 -8
- data/lib/cli/commands/log_management.rb +78 -55
- data/lib/cli/commands/maintenance.rb +36 -30
- data/lib/cli/commands/misc.rb +72 -51
- data/lib/cli/commands/package.rb +2 -2
- data/lib/cli/commands/property_management.rb +10 -12
- data/lib/cli/commands/release.rb +236 -133
- data/lib/cli/commands/snapshot.rb +93 -0
- data/lib/cli/commands/ssh.rb +216 -213
- data/lib/cli/commands/stemcell.rb +46 -34
- data/lib/cli/commands/task.rb +2 -2
- data/lib/cli/commands/user.rb +27 -3
- data/lib/cli/commands/vm.rb +28 -0
- data/lib/cli/commands/vms.rb +81 -23
- data/lib/cli/config.rb +6 -2
- data/lib/cli/core_ext.rb +31 -30
- data/lib/cli/deployment_helper.rb +134 -159
- data/lib/cli/deployment_manifest.rb +66 -0
- data/lib/cli/deployment_manifest_compiler.rb +0 -3
- data/lib/cli/event_log_renderer.rb +10 -10
- data/lib/cli/file_with_progress_bar.rb +52 -0
- data/lib/cli/job_builder.rb +1 -1
- data/lib/cli/job_command_args.rb +23 -0
- data/lib/cli/job_property_collection.rb +4 -7
- data/lib/cli/job_property_validator.rb +22 -12
- data/lib/cli/job_state.rb +54 -0
- data/lib/cli/line_wrap.rb +54 -0
- data/lib/cli/packaging_helper.rb +10 -10
- data/lib/cli/release.rb +18 -15
- data/lib/cli/release_builder.rb +9 -4
- data/lib/cli/release_compiler.rb +9 -9
- data/lib/cli/release_tarball.rb +3 -6
- data/lib/cli/resurrection.rb +31 -0
- data/lib/cli/runner.rb +56 -30
- data/lib/cli/stemcell.rb +25 -10
- data/lib/cli/task_log_renderer.rb +1 -1
- data/lib/cli/task_tracker.rb +10 -9
- data/lib/cli/validation.rb +3 -1
- data/lib/cli/version.rb +1 -1
- data/lib/cli/version_calc.rb +5 -18
- data/lib/cli/versions_index.rb +1 -1
- data/lib/cli/vm_state.rb +43 -0
- data/lib/cli/yaml_helper.rb +26 -35
- metadata +75 -208
- data/Rakefile +0 -56
- data/lib/cli/director.rb +0 -628
- data/spec/assets/biff/bad_gateway_config.yml +0 -28
- data/spec/assets/biff/good_simple_config.yml +0 -63
- data/spec/assets/biff/good_simple_golden_config.yml +0 -63
- data/spec/assets/biff/good_simple_template.erb +0 -69
- data/spec/assets/biff/ip_out_of_range.yml +0 -63
- data/spec/assets/biff/multiple_subnets_config.yml +0 -40
- data/spec/assets/biff/network_only_template.erb +0 -34
- data/spec/assets/biff/no_cc_config.yml +0 -27
- data/spec/assets/biff/no_range_config.yml +0 -27
- data/spec/assets/biff/no_subnet_config.yml +0 -16
- data/spec/assets/biff/ok_network_config.yml +0 -30
- data/spec/assets/biff/properties_template.erb +0 -6
- data/spec/assets/config/atmos/config/final.yml +0 -6
- data/spec/assets/config/atmos/config/private.yml +0 -4
- data/spec/assets/config/bad-providers/config/final.yml +0 -5
- data/spec/assets/config/bad-providers/config/private.yml +0 -4
- data/spec/assets/config/deprecation/config/final.yml +0 -5
- data/spec/assets/config/deprecation/config/private.yml +0 -2
- data/spec/assets/config/local/config/final.yml +0 -5
- data/spec/assets/config/local/config/private.yml +0 -1
- data/spec/assets/config/s3/config/final.yml +0 -5
- data/spec/assets/config/s3/config/private.yml +0 -5
- data/spec/assets/config/swift-hp/config/final.yml +0 -6
- data/spec/assets/config/swift-hp/config/private.yml +0 -7
- data/spec/assets/config/swift-rackspace/config/final.yml +0 -6
- data/spec/assets/config/swift-rackspace/config/private.yml +0 -6
- data/spec/assets/deployment.MF +0 -0
- data/spec/assets/plugins/bosh/cli/commands/echo.rb +0 -43
- data/spec/assets/plugins/bosh/cli/commands/ruby.rb +0 -24
- 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 +0 -6
- data/spec/assets/release/jobs/cacher/monit +0 -1
- data/spec/assets/release/jobs/cleaner.tgz +0 -0
- data/spec/assets/release/jobs/cleaner/job.MF +0 -4
- data/spec/assets/release/jobs/cleaner/monit +0 -1
- data/spec/assets/release/jobs/sweeper.tgz +0 -0
- data/spec/assets/release/jobs/sweeper/config/test.conf +0 -1
- data/spec/assets/release/jobs/sweeper/job.MF +0 -5
- data/spec/assets/release/jobs/sweeper/monit +0 -1
- 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 +0 -17
- 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 +0 -1
- data/spec/assets/stemcell/stemcell.MF +0 -6
- 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 +0 -28
- data/spec/unit/base_command_spec.rb +0 -87
- data/spec/unit/biff_spec.rb +0 -172
- data/spec/unit/blob_manager_spec.rb +0 -288
- data/spec/unit/cache_spec.rb +0 -36
- data/spec/unit/cli_commands_spec.rb +0 -356
- data/spec/unit/config_spec.rb +0 -125
- data/spec/unit/core_ext_spec.rb +0 -81
- data/spec/unit/dependency_helper_spec.rb +0 -52
- data/spec/unit/deployment_manifest_compiler_spec.rb +0 -63
- data/spec/unit/deployment_manifest_spec.rb +0 -153
- data/spec/unit/director_spec.rb +0 -471
- data/spec/unit/director_task_spec.rb +0 -48
- data/spec/unit/event_log_renderer_spec.rb +0 -171
- data/spec/unit/hash_changeset_spec.rb +0 -73
- data/spec/unit/job_builder_spec.rb +0 -455
- data/spec/unit/job_property_collection_spec.rb +0 -111
- data/spec/unit/job_property_validator_spec.rb +0 -7
- data/spec/unit/job_rename_spec.rb +0 -200
- data/spec/unit/package_builder_spec.rb +0 -593
- data/spec/unit/release_builder_spec.rb +0 -120
- data/spec/unit/release_spec.rb +0 -173
- data/spec/unit/release_tarball_spec.rb +0 -29
- data/spec/unit/runner_spec.rb +0 -7
- data/spec/unit/ssh_spec.rb +0 -84
- data/spec/unit/stemcell_spec.rb +0 -17
- data/spec/unit/task_tracker_spec.rb +0 -131
- data/spec/unit/version_calc_spec.rb +0 -27
- data/spec/unit/versions_index_spec.rb +0 -144
data/lib/cli/changeset_helper.rb
CHANGED
|
@@ -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: ".
|
|
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}: ".
|
|
64
|
+
out << indent + "added #{k}: ".make_yellow + v.new.to_s
|
|
65
65
|
when :removed
|
|
66
|
-
out << indent + "removed #{k}: ".
|
|
66
|
+
out << indent + "removed #{k}: ".make_red + v.old.to_s
|
|
67
67
|
when :changed
|
|
68
|
-
out << indent + "changed #{k}: ".
|
|
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}".
|
|
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}".
|
|
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
|