bosh_cli 0.16

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