bosh_cli_plugin_micro 1.5.0.pre.1113

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