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.
Files changed (113) hide show
  1. data/README +4 -0
  2. data/Rakefile +55 -0
  3. data/bin/bosh +17 -0
  4. data/lib/cli.rb +76 -0
  5. data/lib/cli/cache.rb +44 -0
  6. data/lib/cli/changeset_helper.rb +142 -0
  7. data/lib/cli/command_definition.rb +52 -0
  8. data/lib/cli/commands/base.rb +245 -0
  9. data/lib/cli/commands/biff.rb +300 -0
  10. data/lib/cli/commands/blob.rb +125 -0
  11. data/lib/cli/commands/cloudcheck.rb +169 -0
  12. data/lib/cli/commands/deployment.rb +147 -0
  13. data/lib/cli/commands/job.rb +42 -0
  14. data/lib/cli/commands/job_management.rb +117 -0
  15. data/lib/cli/commands/log_management.rb +81 -0
  16. data/lib/cli/commands/maintenance.rb +131 -0
  17. data/lib/cli/commands/misc.rb +240 -0
  18. data/lib/cli/commands/package.rb +112 -0
  19. data/lib/cli/commands/property_management.rb +125 -0
  20. data/lib/cli/commands/release.rb +469 -0
  21. data/lib/cli/commands/ssh.rb +271 -0
  22. data/lib/cli/commands/stemcell.rb +184 -0
  23. data/lib/cli/commands/task.rb +213 -0
  24. data/lib/cli/commands/user.rb +28 -0
  25. data/lib/cli/commands/vms.rb +53 -0
  26. data/lib/cli/config.rb +154 -0
  27. data/lib/cli/core_ext.rb +145 -0
  28. data/lib/cli/dependency_helper.rb +62 -0
  29. data/lib/cli/deployment_helper.rb +263 -0
  30. data/lib/cli/deployment_manifest_compiler.rb +28 -0
  31. data/lib/cli/director.rb +633 -0
  32. data/lib/cli/director_task.rb +64 -0
  33. data/lib/cli/errors.rb +48 -0
  34. data/lib/cli/event_log_renderer.rb +351 -0
  35. data/lib/cli/job_builder.rb +226 -0
  36. data/lib/cli/package_builder.rb +254 -0
  37. data/lib/cli/packaging_helper.rb +248 -0
  38. data/lib/cli/release.rb +176 -0
  39. data/lib/cli/release_builder.rb +215 -0
  40. data/lib/cli/release_compiler.rb +178 -0
  41. data/lib/cli/release_tarball.rb +272 -0
  42. data/lib/cli/runner.rb +771 -0
  43. data/lib/cli/stemcell.rb +83 -0
  44. data/lib/cli/task_log_renderer.rb +40 -0
  45. data/lib/cli/templates/help_message.erb +75 -0
  46. data/lib/cli/validation.rb +42 -0
  47. data/lib/cli/version.rb +7 -0
  48. data/lib/cli/version_calc.rb +48 -0
  49. data/lib/cli/versions_index.rb +126 -0
  50. data/lib/cli/yaml_helper.rb +62 -0
  51. data/spec/assets/biff/bad_gateway_config.yml +28 -0
  52. data/spec/assets/biff/good_simple_config.yml +63 -0
  53. data/spec/assets/biff/good_simple_golden_config.yml +63 -0
  54. data/spec/assets/biff/good_simple_template.erb +69 -0
  55. data/spec/assets/biff/multiple_subnets_config.yml +40 -0
  56. data/spec/assets/biff/network_only_template.erb +34 -0
  57. data/spec/assets/biff/no_cc_config.yml +27 -0
  58. data/spec/assets/biff/no_range_config.yml +27 -0
  59. data/spec/assets/biff/no_subnet_config.yml +16 -0
  60. data/spec/assets/biff/ok_network_config.yml +30 -0
  61. data/spec/assets/biff/properties_template.erb +6 -0
  62. data/spec/assets/deployment.MF +0 -0
  63. data/spec/assets/plugins/bosh/cli/commands/echo.rb +43 -0
  64. data/spec/assets/plugins/bosh/cli/commands/ruby.rb +24 -0
  65. data/spec/assets/release/jobs/cacher.tgz +0 -0
  66. data/spec/assets/release/jobs/cacher/config/file1.conf +0 -0
  67. data/spec/assets/release/jobs/cacher/config/file2.conf +0 -0
  68. data/spec/assets/release/jobs/cacher/job.MF +6 -0
  69. data/spec/assets/release/jobs/cacher/monit +1 -0
  70. data/spec/assets/release/jobs/cleaner.tgz +0 -0
  71. data/spec/assets/release/jobs/cleaner/job.MF +4 -0
  72. data/spec/assets/release/jobs/cleaner/monit +1 -0
  73. data/spec/assets/release/jobs/sweeper.tgz +0 -0
  74. data/spec/assets/release/jobs/sweeper/config/test.conf +1 -0
  75. data/spec/assets/release/jobs/sweeper/job.MF +5 -0
  76. data/spec/assets/release/jobs/sweeper/monit +1 -0
  77. data/spec/assets/release/packages/mutator.tar.gz +0 -0
  78. data/spec/assets/release/packages/stuff.tgz +0 -0
  79. data/spec/assets/release/release.MF +17 -0
  80. data/spec/assets/release_invalid_checksum.tgz +0 -0
  81. data/spec/assets/release_invalid_jobs.tgz +0 -0
  82. data/spec/assets/release_no_name.tgz +0 -0
  83. data/spec/assets/release_no_version.tgz +0 -0
  84. data/spec/assets/stemcell/image +1 -0
  85. data/spec/assets/stemcell/stemcell.MF +6 -0
  86. data/spec/assets/stemcell_invalid_mf.tgz +0 -0
  87. data/spec/assets/stemcell_no_image.tgz +0 -0
  88. data/spec/assets/valid_release.tgz +0 -0
  89. data/spec/assets/valid_stemcell.tgz +0 -0
  90. data/spec/spec_helper.rb +25 -0
  91. data/spec/unit/base_command_spec.rb +66 -0
  92. data/spec/unit/biff_spec.rb +135 -0
  93. data/spec/unit/cache_spec.rb +36 -0
  94. data/spec/unit/cli_commands_spec.rb +481 -0
  95. data/spec/unit/config_spec.rb +139 -0
  96. data/spec/unit/core_ext_spec.rb +77 -0
  97. data/spec/unit/dependency_helper_spec.rb +52 -0
  98. data/spec/unit/deployment_manifest_compiler_spec.rb +63 -0
  99. data/spec/unit/director_spec.rb +511 -0
  100. data/spec/unit/director_task_spec.rb +48 -0
  101. data/spec/unit/event_log_renderer_spec.rb +171 -0
  102. data/spec/unit/hash_changeset_spec.rb +73 -0
  103. data/spec/unit/job_builder_spec.rb +454 -0
  104. data/spec/unit/package_builder_spec.rb +567 -0
  105. data/spec/unit/release_builder_spec.rb +65 -0
  106. data/spec/unit/release_spec.rb +66 -0
  107. data/spec/unit/release_tarball_spec.rb +33 -0
  108. data/spec/unit/runner_spec.rb +140 -0
  109. data/spec/unit/ssh_spec.rb +78 -0
  110. data/spec/unit/stemcell_spec.rb +17 -0
  111. data/spec/unit/version_calc_spec.rb +27 -0
  112. data/spec/unit/versions_index_spec.rb +132 -0
  113. 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