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,271 @@
|
|
|
1
|
+
# Copyright (c) 2009-2012 VMware, Inc.
|
|
2
|
+
|
|
3
|
+
module Bosh::Cli::Command
|
|
4
|
+
class Ssh < Base
|
|
5
|
+
include Bosh::Cli::DeploymentHelper
|
|
6
|
+
CMD_UPLOAD = "upload"
|
|
7
|
+
CMD_DOWNLOAD = "download"
|
|
8
|
+
CMD_EXEC = "exec"
|
|
9
|
+
SSH_USER_PREFIX = "bosh_"
|
|
10
|
+
SSH_DEFAULT_PASSWORD = "bosh"
|
|
11
|
+
SSH_DSA_PUB = File.expand_path("~/.ssh/id_dsa.pub")
|
|
12
|
+
SSH_RSA_PUB = File.expand_path("~/.ssh/id_rsa.pub")
|
|
13
|
+
|
|
14
|
+
def parse_options(args)
|
|
15
|
+
options = {}
|
|
16
|
+
|
|
17
|
+
# Check if index is supplied on the command line
|
|
18
|
+
begin
|
|
19
|
+
# Ruby 1.8.7 treats Integer(nil) as 0, hence the if check
|
|
20
|
+
if args.size > 0
|
|
21
|
+
options["index"] = Integer(args[0])
|
|
22
|
+
args.shift
|
|
23
|
+
end
|
|
24
|
+
rescue ArgumentError, TypeError
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
["public_key", "gateway_host", "gateway_user"].each do |option|
|
|
28
|
+
pos = args.index("--#{option}")
|
|
29
|
+
if pos
|
|
30
|
+
options[option] = args[pos + 1]
|
|
31
|
+
args.delete_at(pos + 1)
|
|
32
|
+
args.delete_at(pos)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
options
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def get_public_key(options)
|
|
39
|
+
# Get public key
|
|
40
|
+
public_key = nil
|
|
41
|
+
if options["public_key"]
|
|
42
|
+
unless File.file?(options["public_key"])
|
|
43
|
+
err("Please specify a valid public key file")
|
|
44
|
+
end
|
|
45
|
+
public_key = File.read(options["public_key"])
|
|
46
|
+
else
|
|
47
|
+
# See if ssh-add can be used
|
|
48
|
+
%x[ssh-add -L 1>/dev/null 2>&1]
|
|
49
|
+
if $?.exitstatus == 0
|
|
50
|
+
keys = %x[ssh-add -L]
|
|
51
|
+
public_key = keys.split("\n")[0]
|
|
52
|
+
else
|
|
53
|
+
# Pick up public key from home dir
|
|
54
|
+
[SSH_DSA_PUB, SSH_RSA_PUB].each do |key_file|
|
|
55
|
+
if File.file?(key_file)
|
|
56
|
+
public_key = File.read(key_file)
|
|
57
|
+
break
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
err("Please specify a public key file") if public_key.nil?
|
|
63
|
+
public_key
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def get_salt_charset
|
|
67
|
+
charset = []
|
|
68
|
+
charset.concat(("a".."z").to_a)
|
|
69
|
+
charset.concat(("A".."Z").to_a)
|
|
70
|
+
charset.concat(("0".."9").to_a)
|
|
71
|
+
charset << "."
|
|
72
|
+
charset << "/"
|
|
73
|
+
charset
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def encrypt_password(plain_text)
|
|
77
|
+
return unless plain_text
|
|
78
|
+
@salt_charset ||= get_salt_charset
|
|
79
|
+
salt = ""
|
|
80
|
+
8.times do
|
|
81
|
+
salt << @salt_charset[rand(@salt_charset.size)]
|
|
82
|
+
end
|
|
83
|
+
plain_text.crypt(salt)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def setup_ssh(job, index, password, options, &block)
|
|
87
|
+
# Get public key
|
|
88
|
+
public_key = get_public_key(options)
|
|
89
|
+
|
|
90
|
+
# Generate a random user name
|
|
91
|
+
user = SSH_USER_PREFIX + rand(36**9).to_s(36)
|
|
92
|
+
|
|
93
|
+
# Get deployment name
|
|
94
|
+
manifest_name = prepare_deployment_manifest["name"]
|
|
95
|
+
|
|
96
|
+
say "Target deployment is #{manifest_name}"
|
|
97
|
+
results = director.setup_ssh(manifest_name, job, index, user, public_key,
|
|
98
|
+
encrypt_password(password))
|
|
99
|
+
|
|
100
|
+
unless results && results.kind_of?(Array) && !results.empty?
|
|
101
|
+
err("Error setting up ssh, #{results.inspect}, " \
|
|
102
|
+
"check task logs for more details")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
results.each do |result|
|
|
106
|
+
unless result.kind_of?(Hash)
|
|
107
|
+
err("Unexpected results #{results.inspect}, " \
|
|
108
|
+
"check task logs for more details")
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
if block_given?
|
|
113
|
+
yield results, user
|
|
114
|
+
end
|
|
115
|
+
ensure
|
|
116
|
+
if results
|
|
117
|
+
say("Cleaning up ssh artifacts")
|
|
118
|
+
indexes = results.map {|result| result["index"]}
|
|
119
|
+
# Cleanup only this one 'user'
|
|
120
|
+
director.cleanup_ssh(manifest_name, job, "^#{user}$", indexes)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def get_free_port
|
|
125
|
+
socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
|
|
126
|
+
socket.bind(Addrinfo.tcp("127.0.0.1", 0))
|
|
127
|
+
port = socket.local_address.ip_port
|
|
128
|
+
socket.close
|
|
129
|
+
|
|
130
|
+
# The port could get used in the interim, but unlikely in real life
|
|
131
|
+
port
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def setup_interactive_shell(job, password, options)
|
|
135
|
+
index = options["index"]
|
|
136
|
+
err("Please specify a job index") if index.nil?
|
|
137
|
+
|
|
138
|
+
if password.nil?
|
|
139
|
+
password_retries = 0
|
|
140
|
+
while password.blank? && password_retries < 3
|
|
141
|
+
password = ask("Enter password " +
|
|
142
|
+
"(use it to sudo on remote host): ") { |q| q.echo = "*" }
|
|
143
|
+
password_retries += 1
|
|
144
|
+
end
|
|
145
|
+
err("Please provide ssh password") if password.blank?
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
setup_ssh(job, index, password, options) do |results, user|
|
|
149
|
+
result = results.first
|
|
150
|
+
unless result["status"] && result["status"] == "success" && result["ip"]
|
|
151
|
+
err("Failed to setup ssh on index #{index} #{results.inspect}")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
say("Starting interactive shell on job #{job}, index #{index}")
|
|
155
|
+
# Start interactive session
|
|
156
|
+
if options["gateway_host"]
|
|
157
|
+
local_port = get_free_port
|
|
158
|
+
say("Connecting to local port #{local_port}")
|
|
159
|
+
# Create the ssh tunnel
|
|
160
|
+
fork do
|
|
161
|
+
gateway = (options["gateway_user"] ?
|
|
162
|
+
"#{options["gateway_user"]}@" : "") +
|
|
163
|
+
options["gateway_host"]
|
|
164
|
+
# Tunnel will close after 30 seconds,
|
|
165
|
+
# so no need to worry about cleaning it up
|
|
166
|
+
exec("ssh -f -L#{local_port}:#{result["ip"]}:22 #{gateway} " +
|
|
167
|
+
"sleep 30")
|
|
168
|
+
end
|
|
169
|
+
result["ip"] = "localhost -p #{local_port}"
|
|
170
|
+
# Wait for tunnel to get established
|
|
171
|
+
sleep 3
|
|
172
|
+
end
|
|
173
|
+
ssh_session = fork do
|
|
174
|
+
exec("ssh #{user}@#{result["ip"]}")
|
|
175
|
+
end
|
|
176
|
+
Process.waitpid(ssh_session)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def shell(*args)
|
|
181
|
+
job = args.shift
|
|
182
|
+
password = args.delete("--default_password") && SSH_DEFAULT_PASSWORD
|
|
183
|
+
options = parse_options(args)
|
|
184
|
+
|
|
185
|
+
if args.size == 0
|
|
186
|
+
setup_interactive_shell(job, password, options)
|
|
187
|
+
else
|
|
188
|
+
say("Executing command '#{args.join(" ")}' on job #{job}")
|
|
189
|
+
execute_command(CMD_EXEC, job, options, args)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def with_ssh(gateway, ip, user, &block)
|
|
194
|
+
if gateway
|
|
195
|
+
gateway.ssh(ip, user) do |ssh|
|
|
196
|
+
yield(ssh)
|
|
197
|
+
end
|
|
198
|
+
else
|
|
199
|
+
Net::SSH.start(ip, user) do |ssh|
|
|
200
|
+
yield(ssh)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def with_gateway(host, user, &block)
|
|
206
|
+
gateway = Net::SSH::Gateway.new(host, user || ENV['USER']) if host
|
|
207
|
+
yield(gateway ||= nil)
|
|
208
|
+
ensure
|
|
209
|
+
gateway.shutdown! if gateway
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def execute_command(command, job, options, args)
|
|
213
|
+
setup_ssh(job, options["index"], nil, options) do |results, user|
|
|
214
|
+
with_gateway(options["gateway_host"],
|
|
215
|
+
options["gateway_user"]) do |gateway|
|
|
216
|
+
results.each do | result|
|
|
217
|
+
unless result["status"] && result["status"] == "success" &&
|
|
218
|
+
result["ip"]
|
|
219
|
+
err("Failed to setup ssh on index #{options["index"]}, " +
|
|
220
|
+
"error: #{result.inspect}")
|
|
221
|
+
end
|
|
222
|
+
with_ssh(gateway, result["ip"], user) do |ssh|
|
|
223
|
+
case command
|
|
224
|
+
when CMD_EXEC
|
|
225
|
+
say("\nJob #{job} index #{result["index"]}")
|
|
226
|
+
puts ssh.exec!(args.join(" "))
|
|
227
|
+
when CMD_UPLOAD
|
|
228
|
+
ssh.scp.upload!(args[0], args[1])
|
|
229
|
+
when CMD_DOWNLOAD
|
|
230
|
+
file = File.basename(args[0])
|
|
231
|
+
path = "#{args[1]}/#{file}.#{job}.#{result["index"]}"
|
|
232
|
+
ssh.scp.download!(args[0], path)
|
|
233
|
+
say("Downloaded file to #{path}")
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def scp(*args)
|
|
242
|
+
job = args.shift
|
|
243
|
+
options = parse_options(args)
|
|
244
|
+
upload = args.delete("--upload")
|
|
245
|
+
download = args.delete("--download")
|
|
246
|
+
if upload.nil? && download.nil?
|
|
247
|
+
err("Please specify one of --upload or --download")
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
if args.empty? || args.size < 2
|
|
251
|
+
err("Please enter valid source and destination paths")
|
|
252
|
+
end
|
|
253
|
+
say("Executing file operations on job #{job}")
|
|
254
|
+
execute_command(upload ? CMD_UPLOAD : CMD_DOWNLOAD, job, options, args)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def cleanup(*args)
|
|
258
|
+
job = args.shift
|
|
259
|
+
options = parse_options(args)
|
|
260
|
+
manifest_name = prepare_deployment_manifest["name"]
|
|
261
|
+
results = nil
|
|
262
|
+
if options["index"]
|
|
263
|
+
results = []
|
|
264
|
+
results << { "index" => options["index"] }
|
|
265
|
+
end
|
|
266
|
+
say "Cleaning up ssh artifacts from job #{job}, index #{options["index"]}"
|
|
267
|
+
director.cleanup_ssh(manifest_name, job, "^#{SSH_USER_PREFIX}",
|
|
268
|
+
[options["index"]])
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Copyright (c) 2009-2012 VMware, Inc.
|
|
2
|
+
|
|
3
|
+
module Bosh::Cli::Command
|
|
4
|
+
class Stemcell < Base
|
|
5
|
+
include Bosh::Cli::VersionCalc
|
|
6
|
+
|
|
7
|
+
# The filename of the public stemcell index.
|
|
8
|
+
PUBLIC_STEMCELL_INDEX = "public_stemcells_index.yml"
|
|
9
|
+
|
|
10
|
+
# The URL of the public stemcell index.
|
|
11
|
+
PUBLIC_STEMCELL_INDEX_URL = "https://blob.cfblob.com/rest/objects/4e4e78bca2" +
|
|
12
|
+
"1e121204e4e86ee151bc04f6a19ce46b22?uid=bb6a0c89ef4048a8a0f814e2538" +
|
|
13
|
+
"5d1c5/user1&expires=1893484800&signature=NJuAr9c8eOid7dKFmOEN7bmzAlI="
|
|
14
|
+
|
|
15
|
+
def verify(tarball_path)
|
|
16
|
+
stemcell = Bosh::Cli::Stemcell.new(tarball_path, cache)
|
|
17
|
+
|
|
18
|
+
say("\nVerifying stemcell...")
|
|
19
|
+
stemcell.validate
|
|
20
|
+
say("\n")
|
|
21
|
+
|
|
22
|
+
if stemcell.valid?
|
|
23
|
+
say("'%s' is a valid stemcell" % [tarball_path])
|
|
24
|
+
else
|
|
25
|
+
say("'%s' is not a valid stemcell:" % [tarball_path])
|
|
26
|
+
for error in stemcell.errors
|
|
27
|
+
say("- %s" % [error])
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def upload(tarball_path)
|
|
33
|
+
auth_required
|
|
34
|
+
|
|
35
|
+
stemcell = Bosh::Cli::Stemcell.new(tarball_path, cache)
|
|
36
|
+
|
|
37
|
+
say("\nVerifying stemcell...")
|
|
38
|
+
stemcell.validate
|
|
39
|
+
say("\n")
|
|
40
|
+
|
|
41
|
+
unless stemcell.valid?
|
|
42
|
+
err("Stemcell is invalid, please fix, verify and upload again")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
say("Checking if stemcell already exists...")
|
|
46
|
+
name = stemcell.manifest["name"]
|
|
47
|
+
version = stemcell.manifest["version"]
|
|
48
|
+
|
|
49
|
+
existing = director.list_stemcells.select do |sc|
|
|
50
|
+
sc["name"] == name and sc["version"] == version
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if existing.empty?
|
|
54
|
+
say("No")
|
|
55
|
+
else
|
|
56
|
+
err("Stemcell \"#{name}\":\"#{version}\" already exists, " +
|
|
57
|
+
"increment the version if it has changed")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
say("\nUploading stemcell...\n")
|
|
61
|
+
|
|
62
|
+
status, message = director.upload_stemcell(stemcell.stemcell_file)
|
|
63
|
+
|
|
64
|
+
responses = {
|
|
65
|
+
:done => "Stemcell uploaded and created",
|
|
66
|
+
:non_trackable => "Uploaded stemcell but director at '#{target}' " +
|
|
67
|
+
"doesn't support creation tracking",
|
|
68
|
+
:track_timeout => "Uploaded stemcell but timed out out " +
|
|
69
|
+
"while tracking status",
|
|
70
|
+
:error => "Uploaded stemcell but received an error " +
|
|
71
|
+
"while tracking status",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
say(responses[status] || "Cannot upload stemcell: #{message}")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def list
|
|
78
|
+
auth_required
|
|
79
|
+
stemcells = director.list_stemcells.sort do |sc1, sc2|
|
|
80
|
+
sc1["name"] == sc2["name"] ?
|
|
81
|
+
version_cmp(sc1["version"], sc2["version"]) :
|
|
82
|
+
sc1["name"] <=> sc2["name"]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
err("No stemcells") if stemcells.size == 0
|
|
86
|
+
|
|
87
|
+
stemcells_table = table do |t|
|
|
88
|
+
t.headings = "Name", "Version", "CID"
|
|
89
|
+
stemcells.each do |sc|
|
|
90
|
+
t << [sc["name"], sc["version"], sc["cid"]]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
say("\n")
|
|
95
|
+
say(stemcells_table)
|
|
96
|
+
say("\n")
|
|
97
|
+
say("Stemcells total: %d" % stemcells.size)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Grabs the index file for the publicly available stemcells.
|
|
101
|
+
# @return [Hash] The index file YAML as a hash.
|
|
102
|
+
def get_public_stemcell_list
|
|
103
|
+
@http_client = HTTPClient.new
|
|
104
|
+
response = @http_client.get(PUBLIC_STEMCELL_INDEX_URL)
|
|
105
|
+
status_code = response.http_header.status_code
|
|
106
|
+
if status_code != HTTP::Status::OK
|
|
107
|
+
raise "Received HTTP #{status_code} from #{index_url}."
|
|
108
|
+
end
|
|
109
|
+
YAML.load(response.body)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Prints out the publicly available stemcells.
|
|
113
|
+
def list_public
|
|
114
|
+
yaml = get_public_stemcell_list
|
|
115
|
+
stemcells_table = table do |t|
|
|
116
|
+
t.headings = "Name", "Url"
|
|
117
|
+
yaml.each do |name, value|
|
|
118
|
+
if name != PUBLIC_STEMCELL_INDEX
|
|
119
|
+
t << [name, value["url"]]
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
puts(stemcells_table)
|
|
124
|
+
puts("To download use 'bosh download public stemcell <stemcell_name>'.")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Downloads one of the publicly available stemcells.
|
|
128
|
+
# @param [String] stemcell_name The name of the stemcell, as seen in the
|
|
129
|
+
# public stemcell index file.
|
|
130
|
+
def download_public(stemcell_name)
|
|
131
|
+
yaml = get_public_stemcell_list
|
|
132
|
+
yaml.delete(PUBLIC_STEMCELL_INDEX) if yaml.has_key?(PUBLIC_STEMCELL_INDEX)
|
|
133
|
+
|
|
134
|
+
unless yaml.has_key?(stemcell_name)
|
|
135
|
+
available_stemcells = yaml.map { |k| k }.join(", ")
|
|
136
|
+
puts("'#{stemcell_name}' not found in '#{available_stemcells}'.".red)
|
|
137
|
+
return
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
if File.exists?(stemcell_name) &&
|
|
141
|
+
!agree("#{stemcell_name} exists locally. Overwrite it? [yn]")
|
|
142
|
+
return
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
url = yaml[stemcell_name]["url"]
|
|
146
|
+
size = yaml[stemcell_name]["size"]
|
|
147
|
+
pBar = ProgressBar.new(stemcell_name, size)
|
|
148
|
+
pBar.file_transfer_mode
|
|
149
|
+
File.open("#{stemcell_name}", "w") { |file|
|
|
150
|
+
@http_client.get(url) do |chunk|
|
|
151
|
+
file.write(chunk)
|
|
152
|
+
pBar.inc(chunk.size)
|
|
153
|
+
end
|
|
154
|
+
}
|
|
155
|
+
pBar.finish
|
|
156
|
+
puts("Download complete.")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def delete(name, version)
|
|
160
|
+
auth_required
|
|
161
|
+
|
|
162
|
+
say("You are going to delete stemcell `#{name} (#{version})'".red)
|
|
163
|
+
|
|
164
|
+
unless confirmed?
|
|
165
|
+
say("Canceled deleting stemcell".green)
|
|
166
|
+
return
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
status, message = director.delete_stemcell(name, version)
|
|
170
|
+
|
|
171
|
+
responses = {
|
|
172
|
+
:done => "Deleted stemcell #{name} (#{version})",
|
|
173
|
+
:non_trackable => "Stemcell delete in progress but director " +
|
|
174
|
+
"at '#{target}' doesn't support task tracking",
|
|
175
|
+
:track_timeout => "Timed out out while tracking " +
|
|
176
|
+
"stemcell deletion progress",
|
|
177
|
+
:error => "Attempted to delete stemcell but received an error " +
|
|
178
|
+
"while tracking status",
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
say(responses[status] || "Cannot delete stemcell: #{message}")
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|