bosh_cli 0.19.6 → 1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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