summaryrefslogtreecommitdiff
path: root/lib/leap_cli/commands/vm.rb
diff options
context:
space:
mode:
authorelijah <elijah@riseup.net>2016-07-21 00:55:12 -0700
committerelijah <elijah@riseup.net>2016-08-23 13:37:34 -0700
commit205b61dfe721e6d88fc06b050a0497eeb35f4e02 (patch)
tree518b5799f56d9e224d7ca2d85b3d29ef0c01b3c6 /lib/leap_cli/commands/vm.rb
parent6fab56fb40256fb2e541ee3ad61490f03254d38e (diff)
added 'leap vm' command
Diffstat (limited to 'lib/leap_cli/commands/vm.rb')
-rw-r--r--lib/leap_cli/commands/vm.rb399
1 files changed, 399 insertions, 0 deletions
diff --git a/lib/leap_cli/commands/vm.rb b/lib/leap_cli/commands/vm.rb
new file mode 100644
index 00000000..ec2b6993
--- /dev/null
+++ b/lib/leap_cli/commands/vm.rb
@@ -0,0 +1,399 @@
+module LeapCli; module Commands
+
+ desc "Manage virtual machines."
+ long_desc "This command provides a convenient way to manage virtual machines. " +
+ "FILTER may be a node filter or the ID of a virtual machine."
+
+ command [:vm] do |vm|
+ vm.switch :mock, :desc => "Run as simulation, without actually connecting to a cloud provider. If set, --auth is ignored."
+ vm.switch :wait, :desc => "Wait for servers to start/stop before continuing."
+ vm.flag :auth, :arg_name => 'AUTH',
+ :desc => "Choose which authentication credentials to use from the file cloud.json. "+
+ "If omitted, will default to the node's `vm.auth` property, or the first credentials in cloud.json"
+
+ vm.desc "Allocates a new virtual machine and/or associates it with node NAME. "+
+ "If node configuration file does not yet exist, "+
+ "it is created with the optional SEED values. "+
+ "You can run this command when the virtual machine already exists "+
+ "in order to update the node's `vm.id` property."
+ vm.arg_name 'NODE_NAME [SEED]'
+ vm.command :add do |cmd|
+ #cmd.flag(:image,
+ # :desc => "The image to use to create this virtual machine.",
+ # :arg_name => 'IMAGE'
+ #)
+ cmd.action do |global, options, args|
+ do_vm_add(global, options, args)
+ end
+ end
+
+ vm.desc 'Starts the virtual machine(s)'
+ vm.arg_name 'FILTER', :optional => true
+ vm.command :start do |start|
+ start.action do |global, options, args|
+ do_vm_start(global, options, args)
+ end
+ end
+
+ vm.desc 'Shuts down the virtual machine(s), but keeps the storage allocated (to save resources, run `leap vm rm` instead).'
+ vm.arg_name 'FILTER', :optional => true
+ vm.command :stop do |stop|
+ stop.action do |global, options, args|
+ do_vm_stop(global, options, args)
+ end
+ end
+
+ vm.desc 'Destroys the virtual machine(s)'
+ vm.arg_name 'FILTER', :optional => true
+ vm.command :rm do |rm|
+ rm.action do |global, options, args|
+ do_vm_rm(global, options, args)
+ end
+ end
+
+ vm.desc 'Print the status of virtual machine(s)'
+ vm.arg_name 'FILTER', :optional => true
+ vm.command :status do |status|
+ status.action do |global, options, args|
+ do_vm_status(global, options, args)
+ end
+ end
+
+ vm.desc "Binds a running virtual machine instance to a node configuration. "+
+ "Afterwards, the VM will be assigned a label matching the node name, "+
+ "and the node config will be updated with the instance ID."
+ vm.arg_name 'NODE_NAME INSTANCE_ID'
+ vm.command 'bind' do |cmd|
+ cmd.action do |global, options, args|
+ do_vm_bind(global, options, args)
+ end
+ end
+
+ vm.desc "Registers a SSH public key for use when creating new virtual machines. "+
+ "Note that only people who are creating new VM instances need to "+
+ "have their key registered."
+ vm.command 'key-register' do |cmd|
+ cmd.action do |global, options, args|
+ do_vm_key_register(global, options, args)
+ end
+ end
+
+ vm.desc "Lists the registered SSH public keys for a particular virtual "+
+ "machine provider."
+ vm.command 'key-list' do |cmd|
+ cmd.action do |global, options, args|
+ do_vm_key_list(global, options, args)
+ end
+ end
+
+ #vm.desc 'Saves the current state of the virtual machine as a new snapshot.'
+ #vm.arg_name 'FILTER', :optional => true
+ #vm.command :save do |save|
+ # save.action do |global, options, args|
+ # do_vm_save(global, options, args)
+ # end
+ #end
+
+ #vm.desc 'Resets virtual machine(s) to the last saved snapshot'
+ #vm.arg_name 'FILTER', :optional => true
+ #vm.command :reset do |reset|
+ # reset.action do |global, options, args|
+ # do_vm_reset(global, options, args)
+ # end
+ #end
+
+ #vm.desc 'Lists the available images.'
+ #vm.command 'image-list' do |cmd|
+ # cmd.action do |global, options, args|
+ # do_vm_image_list(global, options, args)
+ # end
+ #end
+ end
+
+ ##
+ ## SHARED UTILITY METHODS
+ ##
+
+ protected
+
+ #
+ # a callback used if we need to upload a new ssh key
+ #
+ def choose_ssh_key_for_upload(cloud)
+ puts
+ bail! unless agree("The cloud provider `#{cloud.name}` does not have "+
+ "your public key. Do you want to upload one? ")
+ key = pick_ssh_key
+ username = ask("username? ", :default => `whoami`.strip)
+ assert!(username && !username.empty? && username =~ /[0-9a-z_-]+/, "Username must consist of one or more letters or numbers")
+ puts
+ return username, key
+ end
+
+ def bind_server_to_node(vm_id, node, options={})
+ cloud = new_cloud_handle(node, options)
+ server = cloud.compute.servers.get(vm_id)
+ assert! server, "Could not find a VM instance with ID '#{vm_id}'"
+ cloud.bind_server_to_node(server)
+ end
+
+ ##
+ ## COMMANDS
+ ##
+
+ protected
+
+ #
+ # entirely removes the vm, not just stopping it.
+ #
+ # This might be additionally called by the 'leap node rm' command.
+ #
+ def do_vm_rm(global, options, args)
+ servers_from_args(global, options, args) do |cloud, server|
+ cloud.unbind_server_from_node(server) if cloud.node
+ destroy_server(server, options[:wait])
+ end
+ end
+
+ private
+
+ def do_vm_status(global, options, args)
+ cloud = new_cloud_handle(nil, options)
+ servers = cloud.compute.servers
+ t = LeapCli::Util::ConsoleTable.new
+ t.table do
+ t.row(color: :cyan) do
+ t.column "ID"
+ t.column "NODE"
+ t.column "STATE"
+ t.column "FLAVOR"
+ t.column "IP"
+ t.column "ZONE"
+ end
+ servers.each do |server|
+ t.row do
+ t.column server.id
+ t.column server.tags["node_name"]
+ t.column server.state, :color => state_color(server.state)
+ t.column server.flavor_id
+ t.column server.public_ip_address
+ t.column server.availability_zone
+ end
+ end
+ end
+ puts
+ t.draw_table
+ end
+
+ def do_vm_add(global, options, args)
+ name = args.first
+ if manager.nodes[name].nil?
+ do_node_add(global, {:ip_address => '0.0.0.0'}.merge(options), args)
+ end
+ node = manager.nodes[name]
+ cloud = new_cloud_handle(node, options)
+ server = cloud.fetch_or_create_server(:choose_ssh_key => method(:choose_ssh_key_for_upload))
+
+ if server
+ cloud.bind_server_to_node(server)
+ end
+ end
+
+ def do_vm_start(global, options, args)
+ servers_from_args(global, options, args) do |cloud, server|
+ start_server(server, options[:wait])
+ end
+ end
+
+ def do_vm_stop(global, options, args)
+ servers_from_args(global, options, args) do |cloud, server|
+ stop_server(server, options[:wait])
+ end
+ end
+
+ def do_vm_key_register(global, options, args)
+ cloud = new_cloud_handle(nil, options)
+ cloud.find_or_create_key_pair(method(:choose_ssh_key_for_upload))
+ end
+
+ def do_vm_key_list(global, options, args)
+ require 'leap_cli/ssh'
+ cloud = new_cloud_handle(nil, options)
+ cloud.compute.key_pairs.each do |key_pair|
+ log key_pair.name, :color => :cyan do
+ log "AWS fingerprint: " + key_pair.fingerprint
+ key_pair, local_key = cloud.match_ssh_key(:key_pair => key_pair)
+ if local_key
+ log "matches local key: " + local_key.filename
+ log 'SSH MD5 fingerprint: ' + local_key.fingerprint(:digest => :md5, :type => :ssh, :encoding => :hex)
+ log 'SSH SHA256 fingerprint: ' + local_key.fingerprint(:digest => :sha256, :type => :ssh, :encoding => :base64)
+ end
+ end
+ end
+ end
+
+ #
+ # update association between node and virtual machine.
+ #
+ # This might additionally be called by the 'leap node mv' command.
+ #
+ def do_vm_bind(global, options, args)
+ node_name = args.first
+ vm_id = args.last
+ assert! node_name, "NODE_NAME is missing"
+ assert! vm_id, "INSTANCE_ID is missing"
+ node = manager.nodes[node_name]
+ assert! node, "No node with name '#{node_name}'"
+ bind_server_to_node(vm_id, node, options)
+ end
+
+ #def do_vm_image_list(global, options, args)
+ # compute = fog_setup(nil, options)
+ # p compute.images.all
+ #end
+
+ ##
+ ## PRIVATE UTILITY METHODS
+ ##
+
+ def stop_server(server, wait=false)
+ if server.state == 'stopped'
+ log :skipping, "virtual machine `#{server.id}` (already stopped)."
+ elsif ['shutting-down', 'terminated'].include?(server.state)
+ log :skipping, "virtual machine `#{server.id}` (being destroyed)."
+ else
+ log :stopping, "virtual machine `#{server.id}` (#{server.flavor_id}, #{server.availability_zone})"
+ server.stop
+ if wait
+ log 'please wait...', :indent => 1
+ server.wait_for { state == 'stopped' }
+ log 'done', :color => :green, :indent => 1
+ end
+ end
+ end
+
+ def start_server(server, wait=false)
+ if server.state == 'running'
+ log :skipping, "virtual machine `#{server.id}` (already running)."
+ elsif ['shutting-down', 'terminated'].include?(server.state)
+ log :skipping, "virtual machine `#{server.id}` (being destroyed)."
+ else
+ log :starting, "virtual machine `#{server.id}` (#{server.flavor_id}, #{server.availability_zone})"
+ server.start
+ if wait
+ log 'please wait...', :indent => 1
+ server.wait_for { ready? }
+ log 'done', :color => :green, :indent => 1
+ end
+ end
+ end
+
+ def destroy_server(server, wait=false)
+ if ['shutting-down', 'terminated'].include?(server.state)
+ log :skipping, "virtual machine `#{server.id}` (already being removed)."
+ else
+ log :terminated, "virtual machine `#{server.id}` (#{server.flavor_id}, #{server.availability_zone})"
+ server.destroy
+ if wait
+ log 'please wait...', :indent => 1
+ server.wait_for { state == 'terminated' }
+ log 'done', :color => :green, :indent => 1
+ end
+ end
+ end
+
+ #
+ # for each server it finds, yields cloud, server
+ #
+ def servers_from_args(global, options, args)
+ nodes = filter_vm_nodes(args)
+ if nodes.any?
+ nodes.each_node do |node|
+ cloud = new_cloud_handle(node, options)
+ server = cloud.fetch_server_for_node(true)
+ yield cloud, server
+ end
+ else
+ instance_id = args.first
+ cloud = new_cloud_handle(nil, options)
+ server = cloud.compute.servers.get(instance_id)
+ if server.nil?
+ bail! :error, "There is no virtual machine with ID `#{instance_id}`."
+ end
+ yield cloud, server
+ end
+ end
+
+ #
+ # returns either:
+ #
+ # * the set of nodes specified by the filter, for this environment
+ # even if the result includes nodes that are not previously tagged with 'vm'
+ # * the list of all vm nodes for this environment, if filter is empty
+ #
+ def filter_vm_nodes(filter)
+ if filter.nil? || filter.empty?
+ return manager.filter(['vm'], :warning => false)
+ elsif filter.is_a? Array
+ return manager.filter(filter + ['+vm'], :warning => false)
+ else
+ raise ArgumentError, 'could not understand filter'
+ end
+ end
+
+ def new_cloud_handle(node, options)
+ require 'leap_cli/cloud'
+
+ config = manager.env.cloud
+ name = nil
+ if options[:mock]
+ Fog.mock!
+ name = 'mock_aws'
+ config['mock_aws'] = {
+ "api" => "aws",
+ "vendor" => "aws",
+ "auth" => {
+ "aws_access_key_id" => "dummy",
+ "aws_secret_access_key" => "dummy",
+ "region" => "us-west-2"
+ },
+ "instance_options" => {
+ "image" => "dummy"
+ }
+ }
+ elsif options[:auth]
+ name = options[:auth]
+ assert! config[name], "The value for --auth does not correspond to any value in cloud.json."
+ elsif node && node['vm.auth']
+ name = node.vm.auth
+ assert! config[name], "The node '#{node.name}' has a value for property 'vm.auth' that does not correspond to any value in cloud.json."
+ elsif config.keys.length == 1
+ name = config.keys.first
+ log :using, "cloud vendor credentials `#{name}`."
+ else
+ bail! "You must specify --mock, --auth, or a node filter."
+ end
+
+ entry = config[name] # entry in cloud.json
+ assert! entry, "cloud.json: could not find cloud resource `#{name}`."
+ assert! entry['vendor'], "cloud.json: property `vendor` is missing from `#{name}` entry."
+ assert! entry['api'], "cloud.json: property `api` is missing from `#{name}` entry. It must be one of #{config.possible_apis.join(', ')}."
+ assert! entry['auth'], "cloud.json: property `auth` is missing from `#{name}` entry."
+ assert! entry['auth']['region'], "cloud.json: property `auth.region` is missing from `#{name}` entry."
+ assert! entry['api'] == 'aws', "cloud.json: currently, only 'aws' is supported for `api`."
+ assert! entry['vendor'] == 'aws', "cloud.json: currently, only 'aws' is supported for `vendor`."
+
+ return LeapCli::Cloud.new(name, entry, node)
+ end
+
+ def state_color(state)
+ case state
+ when 'running'; :green
+ when 'terminated'; :red
+ when 'stopped'; :magenta
+ when 'shutting-down'; :yellow
+ else; :white
+ end
+ end
+
+end; end