bosh_cli 0.19.6 → 1.0.rc1
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 +3 -0
- data/lib/cli.rb +15 -5
- data/lib/cli/{commands/base.rb → base_command.rb} +38 -44
- data/lib/cli/command_discovery.rb +40 -0
- data/lib/cli/command_handler.rb +135 -0
- data/lib/cli/commands/biff.rb +16 -12
- data/lib/cli/commands/blob_management.rb +10 -3
- data/lib/cli/commands/cloudcheck.rb +13 -11
- data/lib/cli/commands/complete.rb +29 -0
- data/lib/cli/commands/deployment.rb +137 -28
- data/lib/cli/commands/help.rb +96 -0
- data/lib/cli/commands/job.rb +4 -1
- data/lib/cli/commands/job_management.rb +36 -23
- data/lib/cli/commands/job_rename.rb +11 -12
- data/lib/cli/commands/log_management.rb +28 -32
- data/lib/cli/commands/maintenance.rb +6 -1
- data/lib/cli/commands/misc.rb +129 -87
- data/lib/cli/commands/package.rb +6 -65
- data/lib/cli/commands/property_management.rb +20 -8
- data/lib/cli/commands/release.rb +211 -206
- data/lib/cli/commands/ssh.rb +178 -188
- data/lib/cli/commands/stemcell.rb +114 -51
- data/lib/cli/commands/task.rb +74 -56
- data/lib/cli/commands/user.rb +6 -3
- data/lib/cli/commands/vms.rb +17 -15
- data/lib/cli/config.rb +27 -1
- data/lib/cli/core_ext.rb +27 -1
- data/lib/cli/deployment_helper.rb +47 -0
- data/lib/cli/director.rb +18 -9
- data/lib/cli/errors.rb +6 -0
- data/lib/cli/job_builder.rb +75 -23
- data/lib/cli/job_property_collection.rb +87 -0
- data/lib/cli/job_property_validator.rb +130 -0
- data/lib/cli/package_builder.rb +32 -5
- data/lib/cli/release.rb +2 -0
- data/lib/cli/release_builder.rb +9 -13
- data/lib/cli/release_compiler.rb +5 -34
- data/lib/cli/release_tarball.rb +4 -19
- data/lib/cli/runner.rb +118 -694
- data/lib/cli/version.rb +1 -1
- data/spec/assets/config/swift-hp/config/final.yml +6 -0
- data/spec/assets/config/swift-hp/config/private.yml +7 -0
- data/spec/assets/config/swift-rackspace/config/final.yml +6 -0
- data/spec/assets/config/swift-rackspace/config/private.yml +6 -0
- data/spec/spec_helper.rb +0 -5
- data/spec/unit/base_command_spec.rb +32 -37
- data/spec/unit/biff_spec.rb +11 -10
- data/spec/unit/cli_commands_spec.rb +96 -88
- data/spec/unit/core_ext_spec.rb +1 -1
- data/spec/unit/deployment_manifest_spec.rb +36 -0
- data/spec/unit/director_spec.rb +17 -3
- data/spec/unit/job_builder_spec.rb +2 -2
- data/spec/unit/job_property_collection_spec.rb +111 -0
- data/spec/unit/job_property_validator_spec.rb +7 -0
- data/spec/unit/job_rename_spec.rb +7 -6
- data/spec/unit/package_builder_spec.rb +2 -2
- data/spec/unit/release_builder_spec.rb +33 -0
- data/spec/unit/release_spec.rb +54 -0
- data/spec/unit/release_tarball_spec.rb +2 -7
- data/spec/unit/runner_spec.rb +1 -151
- data/spec/unit/ssh_spec.rb +15 -9
- metadata +41 -12
- data/lib/cli/command_definition.rb +0 -52
- data/lib/cli/templates/help_message.erb +0 -80
data/lib/cli/commands/ssh.rb
CHANGED
@@ -3,67 +3,73 @@
|
|
3
3
|
module Bosh::Cli::Command
|
4
4
|
class Ssh < Base
|
5
5
|
include Bosh::Cli::DeploymentHelper
|
6
|
-
|
7
|
-
CMD_DOWNLOAD = "download"
|
8
|
-
CMD_EXEC = "exec"
|
6
|
+
|
9
7
|
SSH_USER_PREFIX = "bosh_"
|
10
|
-
SSH_DEFAULT_PASSWORD = "bosh"
|
11
8
|
SSH_DSA_PUB = File.expand_path("~/.ssh/id_dsa.pub")
|
12
9
|
SSH_RSA_PUB = File.expand_path("~/.ssh/id_rsa.pub")
|
13
10
|
|
14
|
-
|
15
|
-
|
11
|
+
# bosh ssh
|
12
|
+
usage "ssh"
|
13
|
+
desc "Execute command or start an interactive session"
|
14
|
+
option "--public_key FILE", "Public key"
|
15
|
+
option "--gateway_host HOST", "Gateway host"
|
16
|
+
option "--gateway_user USER", "Gateway user"
|
17
|
+
option "--default_password PASSWORD",
|
18
|
+
"Use default ssh password (NOT RECOMMENDED)"
|
19
|
+
def shell(*args)
|
20
|
+
job, index, command = parse_args(args)
|
16
21
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
if args.size > 0
|
21
|
-
options["index"] = Integer(args[0])
|
22
|
-
args.shift
|
22
|
+
if command.empty?
|
23
|
+
if index.nil?
|
24
|
+
err("Can't run interactive shell on more than one instance")
|
23
25
|
end
|
24
|
-
|
25
|
-
|
26
|
+
setup_interactive_shell(job, index)
|
27
|
+
else
|
28
|
+
say("Executing `#{command.join(" ")}' on #{job}/#{index}")
|
29
|
+
perform_operation(:exec, job, index, command)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# bosh scp
|
34
|
+
usage "scp"
|
35
|
+
desc "upload/download the source file to the given job. " +
|
36
|
+
"Note: for download /path/to/destination is a directory"
|
37
|
+
option "--download", "Download file"
|
38
|
+
option "--upload", "Upload file"
|
39
|
+
option "--public_key FILE", "Public key"
|
40
|
+
option "--gateway_host HOST", "Gateway host"
|
41
|
+
option "--gateway_user USER", "Gateway user"
|
42
|
+
def scp(*args)
|
43
|
+
job, index, args = parse_args(args)
|
44
|
+
upload = options[:upload]
|
45
|
+
download = options[:download]
|
46
|
+
if (upload && download) || (upload.nil? && download.nil?)
|
47
|
+
err("Please specify either --upload or --download")
|
26
48
|
end
|
27
49
|
|
28
|
-
|
29
|
-
|
30
|
-
if pos
|
31
|
-
options[option] = args[pos + 1]
|
32
|
-
args.delete_at(pos + 1)
|
33
|
-
args.delete_at(pos)
|
34
|
-
end
|
50
|
+
if args.size != 2
|
51
|
+
err("Please enter valid source and destination paths")
|
35
52
|
end
|
36
|
-
|
53
|
+
say("Executing file operations on job #{job}")
|
54
|
+
perform_operation(upload ? :upload : :download, job, index, args)
|
37
55
|
end
|
38
56
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
end
|
46
|
-
public_key = File.read(options["public_key"])
|
47
|
-
else
|
48
|
-
# See if ssh-add can be used
|
49
|
-
%x[ssh-add -L 1>/dev/null 2>&1]
|
50
|
-
if $?.exitstatus == 0
|
51
|
-
keys = %x[ssh-add -L]
|
52
|
-
public_key = keys.split("\n")[0]
|
53
|
-
else
|
54
|
-
# Pick up public key from home dir
|
55
|
-
[SSH_DSA_PUB, SSH_RSA_PUB].each do |key_file|
|
56
|
-
if File.file?(key_file)
|
57
|
-
public_key = File.read(key_file)
|
58
|
-
break
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
57
|
+
usage "cleanup ssh"
|
58
|
+
desc "Cleanup SSH artifacts"
|
59
|
+
def cleanup(*args)
|
60
|
+
job, index, args = parse_args(args)
|
61
|
+
if args.size > 0
|
62
|
+
err("SSH cleanup doesn't accept any extra args")
|
62
63
|
end
|
63
|
-
|
64
|
-
|
64
|
+
|
65
|
+
manifest_name = prepare_deployment_manifest["name"]
|
66
|
+
|
67
|
+
say("Cleaning up ssh artifacts from #{job}/#{index}")
|
68
|
+
director.cleanup_ssh(manifest_name, job, "^#{SSH_USER_PREFIX}", [index])
|
65
69
|
end
|
66
70
|
|
71
|
+
private
|
72
|
+
|
67
73
|
def get_salt_charset
|
68
74
|
charset = []
|
69
75
|
charset.concat(("a".."z").to_a)
|
@@ -84,189 +90,173 @@ module Bosh::Cli::Command
|
|
84
90
|
plain_text.crypt(salt)
|
85
91
|
end
|
86
92
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
93
|
+
# @param [String] job
|
94
|
+
# @param [Integer] index
|
95
|
+
# @param [optional,String] password
|
96
|
+
def setup_ssh(job, index, password = nil)
|
97
|
+
public_key = get_public_key
|
92
98
|
user = SSH_USER_PREFIX + rand(36**9).to_s(36)
|
99
|
+
deployment_name = prepare_deployment_manifest["name"]
|
93
100
|
|
94
|
-
|
95
|
-
|
101
|
+
say("Target deployment is `#{deployment_name}'")
|
102
|
+
status, task_id = director.setup_ssh(
|
103
|
+
deployment_name, job, index, user,
|
104
|
+
public_key, encrypt_password(password))
|
96
105
|
|
97
|
-
|
98
|
-
|
99
|
-
|
106
|
+
unless status == :done
|
107
|
+
err("Failed to set up SSH: see task #{task_id} log for details")
|
108
|
+
end
|
100
109
|
|
101
|
-
|
102
|
-
|
103
|
-
|
110
|
+
sessions = JSON.parse(director.get_task_result_log(task_id))
|
111
|
+
|
112
|
+
unless sessions && sessions.kind_of?(Array) && sessions.size > 0
|
113
|
+
err("Error setting up ssh, check task #{task_id} log for more details")
|
104
114
|
end
|
105
115
|
|
106
|
-
|
107
|
-
unless
|
108
|
-
err("Unexpected
|
109
|
-
"check task
|
116
|
+
sessions.each do |session|
|
117
|
+
unless session.kind_of?(Hash)
|
118
|
+
err("Unexpected SSH session info: #{session.inspect}. " +
|
119
|
+
"Please check task #{task_id} log for more details")
|
110
120
|
end
|
111
121
|
end
|
112
122
|
|
113
|
-
if
|
114
|
-
|
123
|
+
if options[:gateway_host]
|
124
|
+
gw_host = options[:gateway_host]
|
125
|
+
gw_user = options[:gateway_user] || ENV["USER"]
|
126
|
+
gateway = Net::SSH::Gateway.new(gw_host, gw_user)
|
127
|
+
else
|
128
|
+
gateway = nil
|
115
129
|
end
|
116
|
-
|
117
|
-
|
130
|
+
|
131
|
+
begin
|
132
|
+
yield sessions, user, gateway
|
133
|
+
ensure
|
134
|
+
nl
|
118
135
|
say("Cleaning up ssh artifacts")
|
119
|
-
|
120
|
-
|
121
|
-
|
136
|
+
indices = sessions.map { |session| session["index"] }
|
137
|
+
director.cleanup_ssh(deployment_name, job, "^#{user}$", indices)
|
138
|
+
gateway.shutdown! if gateway
|
122
139
|
end
|
123
140
|
end
|
124
141
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
socket.close
|
130
|
-
|
131
|
-
# The port could get used in the interim, but unlikely in real life
|
132
|
-
port
|
133
|
-
end
|
134
|
-
|
135
|
-
def setup_interactive_shell(job, password, options)
|
136
|
-
index = options["index"]
|
137
|
-
err("Please specify a job index") if index.nil?
|
142
|
+
# @param [String] job Job name
|
143
|
+
# @param [Integer] index Job index
|
144
|
+
def setup_interactive_shell(job, index)
|
145
|
+
password = options[:default_password]
|
138
146
|
|
139
147
|
if password.nil?
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
password_retries += 1
|
145
|
-
end
|
148
|
+
password = ask(
|
149
|
+
"Enter password (use it to " +
|
150
|
+
"sudo on remote host): ") { |q| q.echo = "*" }
|
151
|
+
|
146
152
|
err("Please provide ssh password") if password.blank?
|
147
153
|
end
|
148
154
|
|
149
|
-
setup_ssh(job, index, password
|
150
|
-
|
151
|
-
|
152
|
-
|
155
|
+
setup_ssh(job, index, password) do |sessions, user, gateway|
|
156
|
+
session = sessions.first
|
157
|
+
|
158
|
+
unless session["status"] == "success" && session["ip"]
|
159
|
+
err("Failed to set up SSH on #{job}/#{index}: #{session.inspect}")
|
153
160
|
end
|
154
161
|
|
155
|
-
say("Starting interactive shell on job #{job}
|
156
|
-
|
157
|
-
if
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
fork do
|
162
|
-
gateway = (options["gateway_user"] ?
|
163
|
-
"#{options["gateway_user"]}@" : "") +
|
164
|
-
options["gateway_host"]
|
165
|
-
# Tunnel will close after 30 seconds,
|
166
|
-
# so no need to worry about cleaning it up
|
167
|
-
exec("ssh -f -L#{local_port}:#{result["ip"]}:22 #{gateway} " +
|
168
|
-
"sleep 30")
|
162
|
+
say("Starting interactive shell on job #{job}/#{index}")
|
163
|
+
|
164
|
+
if gateway
|
165
|
+
port = gateway.open(session["ip"], 22)
|
166
|
+
ssh_session = fork do
|
167
|
+
exec("ssh #{user}@localhost -p #{port}")
|
169
168
|
end
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
169
|
+
Process.waitpid(ssh_session)
|
170
|
+
gateway.close(port)
|
171
|
+
else
|
172
|
+
ssh_session = fork do
|
173
|
+
exec("ssh #{user}@#{session["ip"]}")
|
174
|
+
end
|
175
|
+
Process.waitpid(ssh_session)
|
176
176
|
end
|
177
|
-
Process.waitpid(ssh_session)
|
178
177
|
end
|
179
178
|
end
|
180
179
|
|
181
|
-
def
|
182
|
-
job
|
183
|
-
|
184
|
-
|
180
|
+
def perform_operation(operation, job, index, args)
|
181
|
+
setup_ssh(job, index, nil) do |sessions, user, gateway|
|
182
|
+
sessions.each do |session|
|
183
|
+
unless session["status"] == "success" && session["ip"]
|
184
|
+
err("Failed to set up SSH on #{job}/#{index}: #{session.inspect}")
|
185
|
+
end
|
185
186
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
187
|
+
with_ssh(user, session["ip"], gateway) do |ssh|
|
188
|
+
case operation
|
189
|
+
when :exec
|
190
|
+
nl
|
191
|
+
say("#{job}/#{session["index"]}")
|
192
|
+
say(ssh.exec!(args.join(" ")))
|
193
|
+
when :upload
|
194
|
+
ssh.scp.upload!(args[0], args[1])
|
195
|
+
when :download
|
196
|
+
file = File.basename(args[0])
|
197
|
+
path = "#{args[1]}/#{file}.#{job}.#{session["index"]}"
|
198
|
+
ssh.scp.download!(args[0], path)
|
199
|
+
say("Downloaded file to #{path}".green)
|
200
|
+
else
|
201
|
+
err("Unknown operation #{operation}")
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
191
205
|
end
|
192
206
|
end
|
193
207
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
208
|
+
# @param [Array] args
|
209
|
+
# @return [Array] job, index, command
|
210
|
+
def parse_args(args)
|
211
|
+
job = args.shift
|
212
|
+
job, index = job.split("/", 2)
|
213
|
+
|
214
|
+
if index
|
215
|
+
if index =~ /^\d+$/
|
216
|
+
index = index.to_i
|
217
|
+
else
|
218
|
+
err("Invalid job index, integer number expected")
|
202
219
|
end
|
220
|
+
elsif args[0] =~ /^\d+$/
|
221
|
+
index = args.shift.to_i
|
203
222
|
end
|
204
|
-
end
|
205
223
|
|
206
|
-
|
207
|
-
gateway = Net::SSH::Gateway.new(host, user || ENV['USER']) if host
|
208
|
-
yield(gateway ||= nil)
|
209
|
-
ensure
|
210
|
-
gateway.shutdown! if gateway
|
224
|
+
[job, index, args]
|
211
225
|
end
|
212
226
|
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
ssh.scp.upload!(args[0], args[1])
|
230
|
-
when CMD_DOWNLOAD
|
231
|
-
file = File.basename(args[0])
|
232
|
-
path = "#{args[1]}/#{file}.#{job}.#{result["index"]}"
|
233
|
-
ssh.scp.download!(args[0], path)
|
234
|
-
say("Downloaded file to #{path}")
|
235
|
-
end
|
236
|
-
end
|
227
|
+
# @return [String] Public key
|
228
|
+
def get_public_key
|
229
|
+
public_key_path = options[:public_key]
|
230
|
+
|
231
|
+
if public_key_path
|
232
|
+
unless File.file?(public_key_path)
|
233
|
+
err("Can't find file `#{public_key_path}'")
|
234
|
+
end
|
235
|
+
return File.read(public_key_path)
|
236
|
+
else
|
237
|
+
%x[ssh-add -L 1>/dev/null 2>&1]
|
238
|
+
if $?.exitstatus == 0
|
239
|
+
return %x[ssh-add -L].split.first
|
240
|
+
else
|
241
|
+
[SSH_DSA_PUB, SSH_RSA_PUB].each do |key_file|
|
242
|
+
return File.read(key_file) if File.file?(key_file)
|
237
243
|
end
|
238
244
|
end
|
239
245
|
end
|
240
|
-
end
|
241
246
|
|
242
|
-
|
243
|
-
job = args.shift
|
244
|
-
options = parse_options(args)
|
245
|
-
upload = args.delete("--upload")
|
246
|
-
download = args.delete("--download")
|
247
|
-
if upload.nil? && download.nil?
|
248
|
-
err("Please specify one of --upload or --download")
|
249
|
-
end
|
250
|
-
|
251
|
-
if args.empty? || args.size < 2
|
252
|
-
err("Please enter valid source and destination paths")
|
253
|
-
end
|
254
|
-
say("Executing file operations on job #{job}")
|
255
|
-
execute_command(upload ? CMD_UPLOAD : CMD_DOWNLOAD, job, options, args)
|
247
|
+
err("Please specify a public key file")
|
256
248
|
end
|
257
249
|
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
if
|
264
|
-
|
265
|
-
|
250
|
+
# @param [String] user
|
251
|
+
# @param [String] ip
|
252
|
+
# @param [optional, Net::SSH::Gateway] gateway
|
253
|
+
# @yield [Net::SSH]
|
254
|
+
def with_ssh(user, ip, gateway = nil)
|
255
|
+
if gateway
|
256
|
+
gateway.ssh(ip, user) { |ssh| yield ssh }
|
257
|
+
else
|
258
|
+
Net::SSH.start(ip, user) { |ssh| yield ssh }
|
266
259
|
end
|
267
|
-
say "Cleaning up ssh artifacts from job #{job}, index #{options["index"]}"
|
268
|
-
director.cleanup_ssh(manifest_name, job, "^#{SSH_USER_PREFIX}",
|
269
|
-
[options["index"]])
|
270
260
|
end
|
271
261
|
end
|
272
262
|
end
|
@@ -8,35 +8,47 @@ module Bosh::Cli::Command
|
|
8
8
|
PUBLIC_STEMCELL_INDEX = "public_stemcells_index.yml"
|
9
9
|
|
10
10
|
# The URL of the public stemcell index.
|
11
|
-
PUBLIC_STEMCELL_INDEX_URL =
|
12
|
-
|
13
|
-
|
11
|
+
PUBLIC_STEMCELL_INDEX_URL =
|
12
|
+
"https://blob.cfblob.com/rest/objects/4e4e78bca2" +
|
13
|
+
"1e121204e4e86ee151bc04f6a19ce46b22?uid=bb6a0c89ef4048a8a0f814e2538" +
|
14
|
+
"5d1c5/user1&expires=1893484800&signature=NJuAr9c8eOid7dKFmOEN7bmzAlI="
|
14
15
|
|
16
|
+
DEFAULT_PUB_STEMCELL_TAG = "stable"
|
17
|
+
ALL_STEMCELLS_TAG = "all"
|
18
|
+
|
19
|
+
usage "verify stemcell"
|
20
|
+
desc "Verify stemcell"
|
15
21
|
def verify(tarball_path)
|
16
22
|
stemcell = Bosh::Cli::Stemcell.new(tarball_path, cache)
|
17
23
|
|
18
|
-
|
24
|
+
nl
|
25
|
+
say("Verifying stemcell...")
|
19
26
|
stemcell.validate
|
20
|
-
|
27
|
+
nl
|
21
28
|
|
22
29
|
if stemcell.valid?
|
23
|
-
say("'
|
30
|
+
say("`#{tarball_path}' is a valid stemcell".green)
|
24
31
|
else
|
25
|
-
say("
|
26
|
-
|
32
|
+
say("Validation errors:".red)
|
33
|
+
stemcell.errors.each do |error|
|
27
34
|
say("- %s" % [error])
|
28
35
|
end
|
36
|
+
err("`#{tarball_path}' is not a valid stemcell")
|
29
37
|
end
|
30
38
|
end
|
31
39
|
|
40
|
+
# bosh upload stemcell
|
41
|
+
usage "upload stemcell"
|
42
|
+
desc "Upload stemcell"
|
32
43
|
def upload(tarball_path)
|
33
44
|
auth_required
|
34
45
|
|
35
46
|
stemcell = Bosh::Cli::Stemcell.new(tarball_path, cache)
|
36
47
|
|
37
|
-
|
48
|
+
nl
|
49
|
+
say("Verifying stemcell...")
|
38
50
|
stemcell.validate
|
39
|
-
|
51
|
+
nl
|
40
52
|
|
41
53
|
unless stemcell.valid?
|
42
54
|
err("Stemcell is invalid, please fix, verify and upload again")
|
@@ -46,24 +58,25 @@ module Bosh::Cli::Command
|
|
46
58
|
name = stemcell.manifest["name"]
|
47
59
|
version = stemcell.manifest["version"]
|
48
60
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
if existing.empty?
|
54
|
-
say("No")
|
61
|
+
if exists?(name, version)
|
62
|
+
err("Stemcell `#{name}/#{version}' already exists, " +
|
63
|
+
"increment the version if it has changed")
|
55
64
|
else
|
56
|
-
|
57
|
-
"increment the version if it has changed")
|
65
|
+
say("No")
|
58
66
|
end
|
59
67
|
|
60
|
-
|
68
|
+
nl
|
69
|
+
say("Uploading stemcell...")
|
70
|
+
nl
|
61
71
|
|
62
72
|
status, _ = director.upload_stemcell(stemcell.stemcell_file)
|
63
73
|
|
64
74
|
task_report(status, "Stemcell uploaded and created")
|
65
75
|
end
|
66
76
|
|
77
|
+
# bosh stemcells
|
78
|
+
usage "stemcells"
|
79
|
+
desc "Show the list of available stemcells"
|
67
80
|
def list
|
68
81
|
auth_required
|
69
82
|
stemcells = director.list_stemcells.sort do |sc1, sc2|
|
@@ -72,7 +85,7 @@ module Bosh::Cli::Command
|
|
72
85
|
sc1["name"] <=> sc2["name"]
|
73
86
|
end
|
74
87
|
|
75
|
-
err("No stemcells") if stemcells.
|
88
|
+
err("No stemcells") if stemcells.empty?
|
76
89
|
|
77
90
|
stemcells_table = table do |t|
|
78
91
|
t.headings = "Name", "Version", "CID"
|
@@ -81,58 +94,59 @@ module Bosh::Cli::Command
|
|
81
94
|
end
|
82
95
|
end
|
83
96
|
|
84
|
-
|
97
|
+
nl
|
85
98
|
say(stemcells_table)
|
86
|
-
|
99
|
+
nl
|
87
100
|
say("Stemcells total: %d" % stemcells.size)
|
88
101
|
end
|
89
102
|
|
90
|
-
# Grabs the index file for the publicly available stemcells.
|
91
|
-
# @return [Hash] The index file YAML as a hash.
|
92
|
-
def get_public_stemcell_list
|
93
|
-
@http_client = HTTPClient.new
|
94
|
-
response = @http_client.get(PUBLIC_STEMCELL_INDEX_URL)
|
95
|
-
status_code = response.http_header.status_code
|
96
|
-
if status_code != HTTP::Status::OK
|
97
|
-
err("Received HTTP #{status_code} from #{PUBLIC_STEMCELL_INDEX_URL}.")
|
98
|
-
end
|
99
|
-
YAML.load(response.body)
|
100
|
-
end
|
101
|
-
|
102
103
|
# Prints out the publicly available stemcells.
|
103
|
-
|
104
|
-
|
104
|
+
usage "public stemcells"
|
105
|
+
desc "Show the list of publicly available stemcells for download."
|
106
|
+
option "--full", "show the full download url"
|
107
|
+
option "--tags tag1,tag2...", Array, "filter by tag"
|
108
|
+
def list_public
|
109
|
+
full = !!options[:full]
|
110
|
+
tags = options[:tags] || [DEFAULT_PUB_STEMCELL_TAG]
|
111
|
+
|
105
112
|
yaml = get_public_stemcell_list
|
106
113
|
stemcells_table = table do |t|
|
107
|
-
t.headings = "Name", "Url"
|
114
|
+
t.headings = full ? ["Name", "Url", "Tags"] : ["Name", "Tags"]
|
108
115
|
yaml.keys.sort.each do |key|
|
109
116
|
if key != PUBLIC_STEMCELL_INDEX
|
110
|
-
url =
|
111
|
-
|
117
|
+
url = yaml[key]["url"]
|
118
|
+
yaml_tags = yaml[key]["tags"]
|
119
|
+
next if skip_this_tag?(yaml_tags, tags)
|
120
|
+
|
121
|
+
yaml_tags = yaml_tags ? yaml_tags.join(", ") : ""
|
122
|
+
t << (full ? [key, url, yaml_tags] : [key, yaml_tags])
|
112
123
|
end
|
113
124
|
end
|
114
125
|
end
|
115
|
-
|
116
|
-
|
126
|
+
|
127
|
+
say(stemcells_table)
|
128
|
+
|
129
|
+
say("To download use `bosh download public stemcell <stemcell_name>'. " +
|
117
130
|
"For full url use --full.")
|
118
131
|
end
|
119
132
|
|
120
133
|
# Downloads one of the publicly available stemcells.
|
134
|
+
usage "download public stemcell"
|
135
|
+
desc "Downloads a stemcell from the public blobstore"
|
121
136
|
# @param [String] stemcell_name The name of the stemcell, as seen in the
|
122
|
-
#
|
137
|
+
# public stemcell index file.
|
123
138
|
def download_public(stemcell_name)
|
124
139
|
yaml = get_public_stemcell_list
|
125
140
|
yaml.delete(PUBLIC_STEMCELL_INDEX) if yaml.has_key?(PUBLIC_STEMCELL_INDEX)
|
126
141
|
|
127
142
|
unless yaml.has_key?(stemcell_name)
|
128
143
|
available_stemcells = yaml.map { |k| k }.join(", ")
|
129
|
-
|
130
|
-
return
|
144
|
+
err("'#{stemcell_name}' not found in '#{available_stemcells}'.")
|
131
145
|
end
|
132
146
|
|
133
147
|
if File.exists?(stemcell_name) &&
|
134
|
-
|
135
|
-
|
148
|
+
!confirmed?("Overwrite existing file `#{stemcell_name}'?")
|
149
|
+
err("File `#{stemcell_name}' already exists")
|
136
150
|
end
|
137
151
|
|
138
152
|
url = yaml[stemcell_name]["url"]
|
@@ -140,25 +154,36 @@ module Bosh::Cli::Command
|
|
140
154
|
sha1 = yaml[stemcell_name]["sha"]
|
141
155
|
progress_bar = ProgressBar.new(stemcell_name, size)
|
142
156
|
progress_bar.file_transfer_mode
|
143
|
-
|
157
|
+
|
158
|
+
File.open("#{stemcell_name}", "w") do |file|
|
144
159
|
@http_client.get(url) do |chunk|
|
145
160
|
file.write(chunk)
|
146
161
|
progress_bar.inc(chunk.size)
|
147
162
|
end
|
148
|
-
|
163
|
+
end
|
149
164
|
progress_bar.finish
|
165
|
+
|
150
166
|
file_sha1 = Digest::SHA1.file(stemcell_name).hexdigest
|
151
167
|
if file_sha1 != sha1
|
152
|
-
err("The downloaded file sha1
|
153
|
-
"expected sha1
|
168
|
+
err("The downloaded file sha1 `#{file_sha1}' does not match the " +
|
169
|
+
"expected sha1 `#{sha1}'")
|
154
170
|
else
|
155
|
-
|
171
|
+
say("Download complete".green)
|
156
172
|
end
|
157
173
|
end
|
158
174
|
|
175
|
+
# bosh delete stemcell
|
176
|
+
usage "delete stemcell"
|
177
|
+
desc "Delete stemcell"
|
159
178
|
def delete(name, version)
|
160
179
|
auth_required
|
161
180
|
|
181
|
+
say("Checking if stemcell exists...")
|
182
|
+
|
183
|
+
unless exists?(name, version)
|
184
|
+
err("Stemcell `#{name}/#{version}' does not exist")
|
185
|
+
end
|
186
|
+
|
162
187
|
say("You are going to delete stemcell `#{name}/#{version}'".red)
|
163
188
|
|
164
189
|
unless confirmed?
|
@@ -170,5 +195,43 @@ module Bosh::Cli::Command
|
|
170
195
|
|
171
196
|
task_report(status, "Deleted stemcell `#{name}/#{version}'")
|
172
197
|
end
|
198
|
+
|
199
|
+
private
|
200
|
+
|
201
|
+
def skip_this_tag?(yaml_tags, requested_tags)
|
202
|
+
if requested_tags == [ALL_STEMCELLS_TAG]
|
203
|
+
return false
|
204
|
+
end
|
205
|
+
unless yaml_tags
|
206
|
+
return true
|
207
|
+
end
|
208
|
+
requested_tags.each do |tag|
|
209
|
+
unless yaml_tags.include?(tag)
|
210
|
+
return true
|
211
|
+
end
|
212
|
+
end
|
213
|
+
return false
|
214
|
+
end
|
215
|
+
|
216
|
+
def exists?(name, version)
|
217
|
+
existing = director.list_stemcells.select do |sc|
|
218
|
+
sc["name"] == name && sc["version"] == version
|
219
|
+
end
|
220
|
+
|
221
|
+
!existing.empty?
|
222
|
+
end
|
223
|
+
|
224
|
+
# Grabs the index file for the publicly available stemcells.
|
225
|
+
# @return [Hash] The index file YAML as a hash.
|
226
|
+
def get_public_stemcell_list
|
227
|
+
@http_client = HTTPClient.new
|
228
|
+
response = @http_client.get(PUBLIC_STEMCELL_INDEX_URL)
|
229
|
+
status_code = response.http_header.status_code
|
230
|
+
if status_code != HTTP::Status::OK
|
231
|
+
err("Received HTTP #{status_code} from #{PUBLIC_STEMCELL_INDEX_URL}.")
|
232
|
+
end
|
233
|
+
YAML.load(response.body)
|
234
|
+
end
|
235
|
+
|
173
236
|
end
|
174
237
|
end
|