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.
Files changed (64) hide show
  1. data/bin/bosh +3 -0
  2. data/lib/cli.rb +15 -5
  3. data/lib/cli/{commands/base.rb → base_command.rb} +38 -44
  4. data/lib/cli/command_discovery.rb +40 -0
  5. data/lib/cli/command_handler.rb +135 -0
  6. data/lib/cli/commands/biff.rb +16 -12
  7. data/lib/cli/commands/blob_management.rb +10 -3
  8. data/lib/cli/commands/cloudcheck.rb +13 -11
  9. data/lib/cli/commands/complete.rb +29 -0
  10. data/lib/cli/commands/deployment.rb +137 -28
  11. data/lib/cli/commands/help.rb +96 -0
  12. data/lib/cli/commands/job.rb +4 -1
  13. data/lib/cli/commands/job_management.rb +36 -23
  14. data/lib/cli/commands/job_rename.rb +11 -12
  15. data/lib/cli/commands/log_management.rb +28 -32
  16. data/lib/cli/commands/maintenance.rb +6 -1
  17. data/lib/cli/commands/misc.rb +129 -87
  18. data/lib/cli/commands/package.rb +6 -65
  19. data/lib/cli/commands/property_management.rb +20 -8
  20. data/lib/cli/commands/release.rb +211 -206
  21. data/lib/cli/commands/ssh.rb +178 -188
  22. data/lib/cli/commands/stemcell.rb +114 -51
  23. data/lib/cli/commands/task.rb +74 -56
  24. data/lib/cli/commands/user.rb +6 -3
  25. data/lib/cli/commands/vms.rb +17 -15
  26. data/lib/cli/config.rb +27 -1
  27. data/lib/cli/core_ext.rb +27 -1
  28. data/lib/cli/deployment_helper.rb +47 -0
  29. data/lib/cli/director.rb +18 -9
  30. data/lib/cli/errors.rb +6 -0
  31. data/lib/cli/job_builder.rb +75 -23
  32. data/lib/cli/job_property_collection.rb +87 -0
  33. data/lib/cli/job_property_validator.rb +130 -0
  34. data/lib/cli/package_builder.rb +32 -5
  35. data/lib/cli/release.rb +2 -0
  36. data/lib/cli/release_builder.rb +9 -13
  37. data/lib/cli/release_compiler.rb +5 -34
  38. data/lib/cli/release_tarball.rb +4 -19
  39. data/lib/cli/runner.rb +118 -694
  40. data/lib/cli/version.rb +1 -1
  41. data/spec/assets/config/swift-hp/config/final.yml +6 -0
  42. data/spec/assets/config/swift-hp/config/private.yml +7 -0
  43. data/spec/assets/config/swift-rackspace/config/final.yml +6 -0
  44. data/spec/assets/config/swift-rackspace/config/private.yml +6 -0
  45. data/spec/spec_helper.rb +0 -5
  46. data/spec/unit/base_command_spec.rb +32 -37
  47. data/spec/unit/biff_spec.rb +11 -10
  48. data/spec/unit/cli_commands_spec.rb +96 -88
  49. data/spec/unit/core_ext_spec.rb +1 -1
  50. data/spec/unit/deployment_manifest_spec.rb +36 -0
  51. data/spec/unit/director_spec.rb +17 -3
  52. data/spec/unit/job_builder_spec.rb +2 -2
  53. data/spec/unit/job_property_collection_spec.rb +111 -0
  54. data/spec/unit/job_property_validator_spec.rb +7 -0
  55. data/spec/unit/job_rename_spec.rb +7 -6
  56. data/spec/unit/package_builder_spec.rb +2 -2
  57. data/spec/unit/release_builder_spec.rb +33 -0
  58. data/spec/unit/release_spec.rb +54 -0
  59. data/spec/unit/release_tarball_spec.rb +2 -7
  60. data/spec/unit/runner_spec.rb +1 -151
  61. data/spec/unit/ssh_spec.rb +15 -9
  62. metadata +41 -12
  63. data/lib/cli/command_definition.rb +0 -52
  64. data/lib/cli/templates/help_message.erb +0 -80
@@ -3,67 +3,73 @@
3
3
  module Bosh::Cli::Command
4
4
  class Ssh < Base
5
5
  include Bosh::Cli::DeploymentHelper
6
- CMD_UPLOAD = "upload"
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
- def parse_options(args)
15
- options = {}
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
- # 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
22
+ if command.empty?
23
+ if index.nil?
24
+ err("Can't run interactive shell on more than one instance")
23
25
  end
24
- rescue ArgumentError, TypeError
25
- # No index given
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
- %w(public_key gateway_host gateway_user).each do |option|
29
- pos = args.index("--#{option}")
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
- options
53
+ say("Executing file operations on job #{job}")
54
+ perform_operation(upload ? :upload : :download, job, index, args)
37
55
  end
38
56
 
39
- def get_public_key(options)
40
- # Get public key
41
- public_key = nil
42
- if options["public_key"]
43
- unless File.file?(options["public_key"])
44
- err("Please specify a valid public key file")
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
- err("Please specify a public key file") if public_key.nil?
64
- public_key
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
- def setup_ssh(job, index, password, options, &block)
88
- # Get public key
89
- public_key = get_public_key(options)
90
-
91
- # Generate a random user name
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
- # Get deployment name
95
- manifest_name = prepare_deployment_manifest["name"]
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
- say "Target deployment is #{manifest_name}"
98
- results = director.setup_ssh(manifest_name, job, index, user, public_key,
99
- encrypt_password(password))
106
+ unless status == :done
107
+ err("Failed to set up SSH: see task #{task_id} log for details")
108
+ end
100
109
 
101
- unless results && results.kind_of?(Array) && !results.empty?
102
- err("Error setting up ssh, #{results.inspect}, " +
103
- "check task logs for more details")
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
- results.each do |result|
107
- unless result.kind_of?(Hash)
108
- err("Unexpected results #{results.inspect}, " +
109
- "check task logs for more details")
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 block_given?
114
- yield results, user
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
- ensure
117
- if results
130
+
131
+ begin
132
+ yield sessions, user, gateway
133
+ ensure
134
+ nl
118
135
  say("Cleaning up ssh artifacts")
119
- indexes = results.map {|result| result["index"]}
120
- # Cleanup only this one 'user'
121
- director.cleanup_ssh(manifest_name, job, "^#{user}$", indexes)
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
- def get_free_port
126
- socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
127
- socket.bind(Addrinfo.tcp("127.0.0.1", 0))
128
- port = socket.local_address.ip_port
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
- password_retries = 0
141
- while password.blank? && password_retries < 3
142
- password = ask("Enter password " +
143
- "(use it to sudo on remote host): ") { |q| q.echo = "*" }
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, options) do |results, user|
150
- result = results.first
151
- unless result["status"] && result["status"] == "success" && result["ip"]
152
- err("Failed to setup ssh on index #{index} #{results.inspect}")
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}, index #{index}")
156
- # Start interactive session
157
- if options["gateway_host"]
158
- local_port = get_free_port
159
- say("Connecting to local port #{local_port}")
160
- # Create the ssh tunnel
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
- result["ip"] = "localhost -p #{local_port}"
171
- # Wait for tunnel to get established
172
- sleep 3
173
- end
174
- ssh_session = fork do
175
- exec("ssh #{user}@#{result["ip"]}")
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 shell(*args)
182
- job = args.shift
183
- password = args.delete("--default_password") && SSH_DEFAULT_PASSWORD
184
- options = parse_options(args)
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
- if args.size == 0
187
- setup_interactive_shell(job, password, options)
188
- else
189
- say("Executing command '#{args.join(" ")}' on job #{job}")
190
- execute_command(CMD_EXEC, job, options, args)
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
- def with_ssh(gateway, ip, user, &block)
195
- if gateway
196
- gateway.ssh(ip, user) do |ssh|
197
- yield(ssh)
198
- end
199
- else
200
- Net::SSH.start(ip, user) do |ssh|
201
- yield(ssh)
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
- def with_gateway(host, user, &block)
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
- def execute_command(command, job, options, args)
214
- setup_ssh(job, options["index"], nil, options) do |results, user|
215
- with_gateway(options["gateway_host"],
216
- options["gateway_user"]) do |gateway|
217
- results.each do | result|
218
- unless result["status"] && result["status"] == "success" &&
219
- result["ip"]
220
- err("Failed to setup ssh on index #{options["index"]}, " +
221
- "error: #{result.inspect}")
222
- end
223
- with_ssh(gateway, result["ip"], user) do |ssh|
224
- case command
225
- when CMD_EXEC
226
- say("\nJob #{job} index #{result["index"]}")
227
- puts ssh.exec!(args.join(" "))
228
- when CMD_UPLOAD
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
- def scp(*args)
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
- def cleanup(*args)
259
- job = args.shift
260
- options = parse_options(args)
261
- manifest_name = prepare_deployment_manifest["name"]
262
- results = nil
263
- if options["index"]
264
- results = []
265
- results << { "index" => options["index"] }
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 = "https://blob.cfblob.com/rest/objects/4e4e78bca2" +
12
- "1e121204e4e86ee151bc04f6a19ce46b22?uid=bb6a0c89ef4048a8a0f814e2538" +
13
- "5d1c5/user1&expires=1893484800&signature=NJuAr9c8eOid7dKFmOEN7bmzAlI="
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
- say("\nVerifying stemcell...")
24
+ nl
25
+ say("Verifying stemcell...")
19
26
  stemcell.validate
20
- say("\n")
27
+ nl
21
28
 
22
29
  if stemcell.valid?
23
- say("'%s' is a valid stemcell" % [tarball_path])
30
+ say("`#{tarball_path}' is a valid stemcell".green)
24
31
  else
25
- say("'%s' is not a valid stemcell:" % [tarball_path])
26
- for error in stemcell.errors
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
- say("\nVerifying stemcell...")
48
+ nl
49
+ say("Verifying stemcell...")
38
50
  stemcell.validate
39
- say("\n")
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
- 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")
61
+ if exists?(name, version)
62
+ err("Stemcell `#{name}/#{version}' already exists, " +
63
+ "increment the version if it has changed")
55
64
  else
56
- err("Stemcell \"#{name}\":\"#{version}\" already exists, " +
57
- "increment the version if it has changed")
65
+ say("No")
58
66
  end
59
67
 
60
- say("\nUploading stemcell...\n")
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.size == 0
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
- say("\n")
97
+ nl
85
98
  say(stemcells_table)
86
- say("\n")
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
- def list_public(*args)
104
- full = args.include?("--full")
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 = full ? yaml[key]["url"] : "#{yaml[key]["url"][0..49]}..."
111
- t << [key, url]
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
- puts(stemcells_table)
116
- puts("To download use 'bosh download public stemcell <stemcell_name>'." +
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
- # public stemcell index file.
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
- puts("'#{stemcell_name}' not found in '#{available_stemcells}'.".red)
130
- return
144
+ err("'#{stemcell_name}' not found in '#{available_stemcells}'.")
131
145
  end
132
146
 
133
147
  if File.exists?(stemcell_name) &&
134
- !agree("#{stemcell_name} exists locally. Overwrite it? [yn]")
135
- return
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
- File.open("#{stemcell_name}", "w") { |file|
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 '#{file_sha1}' does not match the " +
153
- "expected sha1 '#{sha1}'.")
168
+ err("The downloaded file sha1 `#{file_sha1}' does not match the " +
169
+ "expected sha1 `#{sha1}'")
154
170
  else
155
- puts("Download complete.")
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