bosh_cli_plugin_micro 1.5.0.pre.1113

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.
@@ -0,0 +1,442 @@
1
+ require 'pp'
2
+ require 'deployer'
3
+
4
+ module Bosh::Cli::Command
5
+ class Micro < Base
6
+ include Bosh::Deployer::Helpers
7
+
8
+ MICRO_DIRECTOR_PORT = 25555
9
+ DEFAULT_CONFIG_PATH = File.expand_path("~/.bosh_deployer_config")
10
+ MICRO_BOSH_YAML = "micro_bosh.yml"
11
+
12
+ def initialize(runner)
13
+ super(runner)
14
+ options[:config] ||= DEFAULT_CONFIG_PATH #hijack Cli::Config
15
+ end
16
+
17
+ usage "micro"
18
+ desc "show micro bosh sub-commands"
19
+ def micro_help
20
+ say("bosh micro sub-commands:")
21
+ nl
22
+ cmds = Bosh::Cli::Config.commands.values.find_all {|c|
23
+ c.usage =~ /^micro/
24
+ }
25
+ Bosh::Cli::Command::Help.list_commands(cmds)
26
+ end
27
+
28
+ usage "micro deployment"
29
+ desc "Choose micro deployment to work with, or display current deployment"
30
+ def micro_deployment(name=nil)
31
+ if name
32
+ set_current(name)
33
+ else
34
+ show_current
35
+ end
36
+ end
37
+
38
+ def set_current(name)
39
+ manifest_filename = find_deployment(name)
40
+
41
+ if !File.exists?(manifest_filename)
42
+ err "Missing manifest for #{name} (tried '#{manifest_filename}')"
43
+ end
44
+
45
+ manifest = load_yaml_file(manifest_filename)
46
+
47
+ unless manifest.is_a?(Hash)
48
+ err "Invalid manifest format"
49
+ end
50
+
51
+ if manifest["network"].blank?
52
+ err "network is not defined in deployment manifest"
53
+ end
54
+ ip = deployer(manifest_filename).discover_bosh_ip || name
55
+
56
+ if target
57
+ old_director_ip = URI.parse(target).host
58
+ else
59
+ old_director_ip = nil
60
+ end
61
+
62
+ if old_director_ip != ip
63
+ set_target(ip)
64
+ say "#{"WARNING!".make_red} Your target has been changed to `#{target.make_red}'!"
65
+ end
66
+
67
+ say "Deployment set to '#{manifest_filename.make_green}'"
68
+ config.set_deployment(manifest_filename)
69
+ config.save
70
+ end
71
+
72
+ def show_current
73
+ say(deployment ? "Current deployment is '#{deployment.make_green}'" : "Deployment not set")
74
+ end
75
+
76
+ usage "micro status"
77
+ desc "Display micro BOSH deployment status"
78
+ def status
79
+ stemcell_cid = deployer_state(:stemcell_cid)
80
+ stemcell_name = deployer_state(:stemcell_name)
81
+ vm_cid = deployer_state(:vm_cid)
82
+ disk_cid = deployer_state(:disk_cid)
83
+ deployment = config.deployment ? config.deployment.make_green : "not set".make_red
84
+
85
+ say("Stemcell CID".ljust(15) + stemcell_cid)
86
+ say("Stemcell name".ljust(15) + stemcell_name)
87
+ say("VM CID".ljust(15) + vm_cid)
88
+ say("Disk CID".ljust(15) + disk_cid)
89
+ say("Micro BOSH CID".ljust(15) + Bosh::Deployer::Config.uuid)
90
+ say("Deployment".ljust(15) + deployment)
91
+
92
+ update_target
93
+
94
+ target_name = target ? target.make_green : "not set".make_red
95
+ say("Target".ljust(15) + target_name)
96
+ end
97
+
98
+ usage "micro deploy"
99
+ desc "Deploy a micro BOSH instance to the currently selected deployment"
100
+ option "--update", "update existing instance"
101
+ option "--update-if-exists", "create new or update existing instance"
102
+ def perform(stemcell=nil)
103
+ update = !!options[:update]
104
+
105
+ err "No deployment set" unless deployment
106
+
107
+ manifest = load_yaml_file(deployment)
108
+
109
+ if stemcell.nil?
110
+ unless manifest.is_a?(Hash)
111
+ err("Invalid manifest format")
112
+ end
113
+
114
+ stemcell = dig_hash(manifest, "resources", "cloud_properties", "image_id")
115
+
116
+ if stemcell.nil?
117
+ err "No stemcell provided"
118
+ end
119
+ end
120
+
121
+ deployer.check_dependencies
122
+
123
+ rel_path = strip_relative_path(deployment)
124
+
125
+ desc = "`#{rel_path.make_green}' to `#{target_name.make_green}'"
126
+
127
+ if deployer.exists?
128
+ if !options[:update_if_exists] && !update
129
+ err "Instance exists. Did you mean to --update?"
130
+ end
131
+ confirmation = "Updating"
132
+ method = :update_deployment
133
+ else
134
+ prefered_dir = File.dirname(File.dirname(deployment))
135
+
136
+ unless prefered_dir == Dir.pwd
137
+ confirm_deployment("\n#{'No `bosh-deployments.yml` file found in current directory.'.make_red}\n\n" +
138
+ "Conventionally, `bosh-deployments.yml` should be saved in " +
139
+ "#{prefered_dir.make_green}.\n" +
140
+ "Is #{Dir.pwd.make_yellow} a directory where you can save state?")
141
+ end
142
+
143
+ err "No existing instance to update" if update
144
+ confirmation = "Deploying new micro BOSH instance"
145
+ method = :create_deployment
146
+
147
+ # make sure the user knows a persistent disk is required
148
+ unless dig_hash(manifest, "resources", "persistent_disk")
149
+ quit("No persistent disk configured in #{MICRO_BOSH_YAML}".make_red)
150
+ end
151
+ end
152
+
153
+ confirm_deployment("#{confirmation} #{desc}")
154
+
155
+ if is_tgz?(stemcell)
156
+ stemcell_file = Bosh::Cli::Stemcell.new(stemcell, cache)
157
+
158
+ say("\nVerifying stemcell...")
159
+ stemcell_file.validate
160
+ say("\n")
161
+
162
+ unless stemcell_file.valid?
163
+ err("Stemcell is invalid, please fix, verify and upload again")
164
+ end
165
+ end
166
+
167
+ renderer = DeployerRenderer.new
168
+ renderer.start
169
+ deployer.renderer = renderer
170
+
171
+ start_time = Time.now
172
+
173
+ deployer.send(method, stemcell)
174
+
175
+ renderer.finish("done")
176
+
177
+ duration = renderer.duration || (Time.now - start_time)
178
+
179
+ update_target
180
+
181
+ say("Deployed #{desc}, took #{format_time(duration).make_green} to complete")
182
+ end
183
+
184
+ usage "micro delete"
185
+ desc "Delete micro BOSH instance (including persistent disk)"
186
+ def delete
187
+ unless deployer.exists?
188
+ err "No existing instance to delete"
189
+ end
190
+
191
+ name = deployer.state.name
192
+
193
+ say "\nYou are going to delete micro BOSH deployment `#{name}'.\n\n" \
194
+ "THIS IS A VERY DESTRUCTIVE OPERATION AND IT CANNOT BE UNDONE!\n".make_red
195
+
196
+ unless confirmed?
197
+ say "Canceled deleting deployment".make_green
198
+ return
199
+ end
200
+
201
+ renderer = DeployerRenderer.new
202
+ renderer.start
203
+ deployer.renderer = renderer
204
+
205
+ start_time = Time.now
206
+
207
+ deployer.delete_deployment
208
+
209
+ renderer.finish("done")
210
+
211
+ duration = renderer.duration || (Time.now - start_time)
212
+
213
+ say("Deleted deployment '#{name}', took #{format_time(duration).make_green} to complete")
214
+ end
215
+
216
+ usage "micro deployments"
217
+ desc "Show the list of deployments"
218
+ def list
219
+ file = File.join(work_dir, DEPLOYMENTS_FILE)
220
+ if File.exists?(file)
221
+ deployments = load_yaml_file(file)["instances"]
222
+ else
223
+ deployments = []
224
+ end
225
+
226
+ err("No deployments") if deployments.size == 0
227
+
228
+ na = "n/a"
229
+
230
+ deployments_table = table do |t|
231
+ t.headings = [ "Name", "VM name", "Stemcell name" ]
232
+ deployments.each do |r|
233
+ t << [ r[:name], r[:vm_cid] || na, r[:stemcell_cid] || na ]
234
+ end
235
+ end
236
+
237
+ say("\n")
238
+ say(deployments_table)
239
+ say("\n")
240
+ say("Deployments total: %d" % deployments.size)
241
+ end
242
+
243
+ usage "micro agent <args>"
244
+ desc <<-AGENT_HELP
245
+ Send agent messages
246
+
247
+ Message Types:
248
+
249
+ start - Start all jobs on MicroBOSH
250
+
251
+ stop - Stop all jobs on MicroBOSH
252
+
253
+ ping - Check to see if the agent is responding
254
+
255
+ drain TYPE SPEC - Tell the agent to begin draining
256
+ TYPE - One of 'shutdown', 'update' or 'status'.
257
+ SPEC - The drain spec to use.
258
+
259
+ state [full] - Get the state of a system
260
+ full - Get additional information about system vitals
261
+
262
+ list_disk - List disk CIDs mounted on the system
263
+
264
+ migrate_disk OLD NEW - Migrate a disk
265
+ OLD - The CID of the source disk.
266
+ NEW - The CID of the destination disk.
267
+
268
+ mount_disk CID - Mount a disk on the system
269
+ CID - The cloud ID of the disk to mount.
270
+
271
+ unmount_disk CID - Unmount a disk from the system
272
+ CID - The cloud ID of the disk to unmount.
273
+
274
+ AGENT_HELP
275
+ def agent(*args)
276
+ message = args.shift
277
+ args = args.map do |arg|
278
+ if File.exists?(arg)
279
+ load_yaml_file(arg)
280
+ else
281
+ arg
282
+ end
283
+ end
284
+
285
+ say(deployer.agent.send(message.to_sym, *args).pretty_inspect)
286
+ end
287
+
288
+ usage "micro apply"
289
+ desc "Apply spec"
290
+ def apply(spec)
291
+ deployer.apply(Bosh::Deployer::Specification.new(load_yaml_file(spec)))
292
+ end
293
+
294
+ private
295
+
296
+ def deployer(manifest_filename=nil)
297
+ deployment_required unless manifest_filename
298
+
299
+ if @deployer.nil?
300
+ manifest_filename ||= deployment
301
+
302
+ if !File.exists?(manifest_filename)
303
+ err("Cannot find deployment manifest in `#{manifest_filename}'")
304
+ end
305
+
306
+ manifest = load_yaml_file(manifest_filename)
307
+
308
+ manifest["dir"] ||= work_dir
309
+ manifest["logging"] ||= {}
310
+ unless manifest["logging"]["file"]
311
+ log_file = File.join(File.dirname(manifest_filename),
312
+ "bosh_micro_deploy.log")
313
+ manifest["logging"]["file"] = log_file
314
+ end
315
+
316
+ @deployer = Bosh::Deployer::InstanceManager.create(manifest)
317
+ end
318
+
319
+ @deployer
320
+ end
321
+
322
+ def find_deployment(name)
323
+ if File.directory?(name)
324
+ filename = File.join("#{name}", MICRO_BOSH_YAML)
325
+ else
326
+ filename = name
327
+ end
328
+
329
+ File.expand_path(filename, Dir.pwd)
330
+ end
331
+
332
+ def deployment_name
333
+ File.basename(File.dirname(deployment))
334
+ end
335
+
336
+ # set new target and clear out cached values
337
+ # does not persist the new values (set_current() does this)
338
+ def set_target(ip)
339
+ config.target = "https://#{ip}:#{MICRO_DIRECTOR_PORT}"
340
+ config.target_name = nil
341
+ config.target_version = nil
342
+ config.target_uuid = nil
343
+ end
344
+
345
+ def update_target
346
+ if deployer.exists?
347
+ bosh_ip = deployer.discover_bosh_ip
348
+ if URI.parse(target).host != bosh_ip
349
+ set_current(deployment)
350
+ end
351
+
352
+ director = Bosh::Cli::Client::Director.new(target)
353
+
354
+ if options[:director_checks]
355
+ begin
356
+ status = director.get_status
357
+ rescue Bosh::Cli::AuthError
358
+ status = {}
359
+ rescue Bosh::Cli::DirectorError
360
+ err("Cannot talk to director at '#{target}', please set correct target")
361
+ end
362
+ else
363
+ status = { "name" => "Unknown Director", "version" => "n/a" }
364
+ end
365
+ else
366
+ status = {}
367
+ end
368
+
369
+ config.target_name = status["name"]
370
+ config.target_version = status["version"]
371
+ config.target_uuid = status["uuid"]
372
+
373
+ config.save
374
+ end
375
+
376
+ def confirm_deployment(msg)
377
+ unless confirmed?(msg)
378
+ cancel_deployment
379
+ end
380
+ end
381
+
382
+ def deployer_state(column)
383
+ if value = deployer.state.send(column)
384
+ value.make_green
385
+ else
386
+ "n/a".make_red
387
+ end
388
+ end
389
+
390
+ class DeployerRenderer < Bosh::Cli::EventLogRenderer
391
+ attr_accessor :stage, :total, :index
392
+
393
+ DEFAULT_POLL_INTERVAL = 1
394
+
395
+ def interval_poll
396
+ Bosh::Cli::Config.poll_interval || DEFAULT_POLL_INTERVAL
397
+ end
398
+
399
+ def start
400
+ @thread = Thread.new do
401
+ loop do
402
+ refresh
403
+ sleep(interval_poll)
404
+ end
405
+ end
406
+ end
407
+
408
+ def finish(state)
409
+ @thread.kill
410
+ super(state)
411
+ end
412
+
413
+ def enter_stage(stage, total)
414
+ @stage = stage
415
+ @total = total
416
+ @index = 0
417
+ end
418
+
419
+ def parse_event(event)
420
+ event
421
+ end
422
+
423
+ def update(state, task)
424
+ event = {
425
+ "time" => Time.now,
426
+ "stage" => @stage,
427
+ "task" => task,
428
+ "tags" => [],
429
+ "index" => @index+1,
430
+ "total" => @total,
431
+ "state" => state.to_s,
432
+ "progress" => state == :finished ? 100 : 0
433
+ }
434
+
435
+ add_event(event)
436
+
437
+ @index += 1 if state == :finished
438
+ end
439
+ end
440
+
441
+ end
442
+ end
@@ -0,0 +1,157 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ module Bosh; end
4
+
5
+ module Bosh::Deployer
6
+ class Config
7
+
8
+ class << self
9
+
10
+ include Helpers
11
+
12
+ attr_accessor :logger, :db, :uuid, :resources, :cloud_options,
13
+ :spec_properties, :agent_properties, :bosh_ip, :env, :name, :net_conf
14
+
15
+ def configure(config)
16
+ plugin = cloud_plugin(config)
17
+
18
+ config = deep_merge(load_defaults(plugin), config)
19
+
20
+ @base_dir = config["dir"]
21
+ FileUtils.mkdir_p(@base_dir)
22
+
23
+ @name = config["name"]
24
+ @cloud_options = config["cloud"]
25
+ @net_conf = config["network"]
26
+ @bosh_ip = @net_conf["ip"]
27
+ @resources = config["resources"]
28
+ @env = config["env"]
29
+
30
+ @logger = Logger.new(config["logging"]["file"] || STDOUT)
31
+ @logger.level = Logger.const_get(config["logging"]["level"].upcase)
32
+ @logger.formatter = ThreadFormatter.new
33
+
34
+ apply_spec = config["apply_spec"]
35
+ @spec_properties = apply_spec["properties"]
36
+ @agent_properties = apply_spec["agent"]
37
+
38
+ @db = Sequel.sqlite
39
+
40
+ migrate_cpi
41
+
42
+ @db.create_table :instances do
43
+ primary_key :id
44
+ column :name, :text, :unique => true, :null => false
45
+ column :uuid, :text
46
+ column :stemcell_cid, :text
47
+ column :stemcell_name, :text
48
+ column :vm_cid, :text
49
+ column :disk_cid, :text
50
+ end
51
+
52
+ Sequel::Model.plugin :validation_helpers
53
+
54
+ Bosh::Clouds::Config.configure(self)
55
+
56
+ require "deployer/models/instance"
57
+
58
+ @cloud_options["properties"]["agent"]["mbus"] ||=
59
+ "https://vcap:b00tstrap@0.0.0.0:6868"
60
+
61
+ @disk_model = nil
62
+ @cloud = nil
63
+ @networks = nil
64
+ end
65
+
66
+ def cloud
67
+ if @cloud.nil?
68
+ @cloud = Bosh::Clouds::Provider.create(@cloud_options["plugin"],
69
+ @cloud_options["properties"])
70
+ end
71
+ @cloud
72
+ end
73
+
74
+ def agent
75
+ uri = URI.parse(agent_url)
76
+ user, password = uri.userinfo.split(":", 2)
77
+ uri.userinfo = nil
78
+ uri.host = bosh_ip
79
+ Bosh::Agent::HTTPClient.new(uri.to_s,
80
+ { "user" => user,
81
+ "password" => password,
82
+ "reply_to" => uuid })
83
+ end
84
+
85
+ def agent_url
86
+ @cloud_options["properties"]["agent"]["mbus"]
87
+ end
88
+
89
+ def networks
90
+ return @networks if @networks
91
+
92
+ @networks = {
93
+ "bosh" => {
94
+ "cloud_properties" => @net_conf["cloud_properties"],
95
+ "netmask" => @net_conf["netmask"],
96
+ "gateway" => @net_conf["gateway"],
97
+ "ip" => @net_conf["ip"],
98
+ "dns" => @net_conf["dns"],
99
+ "type" => @net_conf["type"],
100
+ "default" => ["dns", "gateway"]
101
+ }
102
+ }
103
+ if @net_conf["vip"]
104
+ @networks["vip"] = {
105
+ "ip" => @net_conf["vip"],
106
+ "type" => "vip",
107
+ "cloud_properties" => {}
108
+ }
109
+ end
110
+
111
+ @networks
112
+ end
113
+
114
+ def task_checkpoint
115
+ # Bosh::Clouds::Config (bosh_cli >= 0.5.1) delegates task_checkpoint
116
+ # method to periodically check if director task is cancelled,
117
+ # so we need to define a void method in Bosh::Deployer::Config to avoid
118
+ # NoMethodError exceptions.
119
+ end
120
+
121
+ private
122
+
123
+ def migrate_cpi
124
+ cpi = @cloud_options["plugin"]
125
+ require_path = File.join("cloud", cpi)
126
+ cpi_path = $LOAD_PATH.find { |p| File.exist?(
127
+ File.join(p, require_path)) }
128
+ migrations = File.expand_path("../db/migrations", cpi_path)
129
+
130
+ if File.directory?(migrations)
131
+ Sequel.extension :migration
132
+ Sequel::TimestampMigrator.new(
133
+ @db, migrations, :table => "#{cpi}_cpi_schema").run
134
+ end
135
+ end
136
+
137
+ def deep_merge(src, dst)
138
+ src.merge(dst) do |key, old, new|
139
+ if new.respond_to?(:blank) && new.blank?
140
+ old
141
+ elsif old.kind_of?(Hash) and new.kind_of?(Hash)
142
+ deep_merge(old, new)
143
+ elsif old.kind_of?(Array) and new.kind_of?(Array)
144
+ old.concat(new).uniq
145
+ else
146
+ new
147
+ end
148
+ end
149
+ end
150
+
151
+ def load_defaults(provider)
152
+ file = File.join(File.dirname(File.expand_path(__FILE__)), "../../config/#{provider}_defaults.yml")
153
+ Psych.load_file(file)
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,115 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ require 'net/ssh'
4
+
5
+ module Bosh::Deployer
6
+
7
+ module Helpers
8
+
9
+ DEPLOYMENTS_FILE = "bosh-deployments.yml"
10
+
11
+ def is_tgz?(path)
12
+ File.extname(path) == ".tgz"
13
+ end
14
+
15
+ def cloud_plugin(config)
16
+ err "No cloud properties defined" if config["cloud"].nil?
17
+ err "No cloud plugin defined" if config["cloud"]["plugin"].nil?
18
+
19
+ config["cloud"]["plugin"]
20
+ end
21
+
22
+ def dig_hash(hash, *path)
23
+ path.inject(hash) do |location, key|
24
+ location.respond_to?(:keys) ? location[key] : nil
25
+ end
26
+ end
27
+
28
+ def process_exists?(pid)
29
+ begin
30
+ Process.kill(0, pid)
31
+ rescue Errno::ESRCH
32
+ false
33
+ end
34
+ end
35
+
36
+ def socket_readable?(ip, port)
37
+ socket = TCPSocket.new(ip, port)
38
+ if IO.select([socket], nil, nil, 5)
39
+ logger.debug("tcp socket #{ip}:#{port} is readable")
40
+ yield
41
+ true
42
+ else
43
+ false
44
+ end
45
+ rescue SocketError => e
46
+ logger.debug("tcp socket #{ip}:#{port} SocketError: #{e.inspect}")
47
+ sleep 1
48
+ false
49
+ rescue SystemCallError => e
50
+ logger.debug("tcp socket #{ip}:#{port} SystemCallError: #{e.inspect}")
51
+ sleep 1
52
+ false
53
+ ensure
54
+ socket.close if socket
55
+ end
56
+
57
+ def remote_tunnel(port)
58
+ @sessions ||= {}
59
+ return if @sessions[port]
60
+
61
+ ip = Config.bosh_ip
62
+
63
+ loop until socket_readable?(ip, @ssh_port) do
64
+ #sshd is up, sleep while host keys are generated
65
+ sleep @ssh_wait
66
+ end
67
+
68
+ if @sessions[port].nil?
69
+ logger.info("Starting SSH session for port forwarding to #{@ssh_user}@#{ip}...")
70
+ loop do
71
+ begin
72
+ @sessions[port] = Net::SSH.start(ip, @ssh_user, :keys => [@ssh_key],
73
+ :paranoid => false)
74
+ logger.debug("ssh #{@ssh_user}@#{ip}: ESTABLISHED")
75
+ break
76
+ rescue => e
77
+ logger.debug("ssh start #{@ssh_user}@#{ip} failed: #{e.inspect}")
78
+ sleep 1
79
+ end
80
+ end
81
+ end
82
+
83
+ lo = "127.0.0.1"
84
+ @sessions[port].forward.remote(port, lo, port)
85
+
86
+ logger.info("SSH forwarding for port #{port} started: OK")
87
+
88
+ Thread.new do
89
+ while @sessions[port]
90
+ begin
91
+ @sessions[port].loop { true }
92
+ rescue IOError => e
93
+ logger.debug("SSH session #{@sessions[port].inspect} forwarding for port #{port} terminated: #{e.inspect}")
94
+ @sessions.delete(port)
95
+ end
96
+ end
97
+ end
98
+
99
+ at_exit do
100
+ status = $!.is_a?(::SystemExit) ? $!.status : nil
101
+ close_ssh_sessions
102
+ exit status if status
103
+ end
104
+ end
105
+
106
+ def close_ssh_sessions
107
+ @sessions.each_value { |s| s.close }
108
+ end
109
+
110
+ def strip_relative_path(path)
111
+ path[/#{Regexp.escape File.join(Dir.pwd, '')}(.*)/, 1] || path
112
+ end
113
+ end
114
+
115
+ end