diff options
| author | elijah <elijah@riseup.net> | 2016-07-21 00:55:12 -0700 | 
|---|---|---|
| committer | elijah <elijah@riseup.net> | 2016-08-23 13:37:34 -0700 | 
| commit | 205b61dfe721e6d88fc06b050a0497eeb35f4e02 (patch) | |
| tree | 518b5799f56d9e224d7ca2d85b3d29ef0c01b3c6 /lib/leap_cli/commands | |
| parent | 6fab56fb40256fb2e541ee3ad61490f03254d38e (diff) | |
added 'leap vm' command
Diffstat (limited to 'lib/leap_cli/commands')
| -rw-r--r-- | lib/leap_cli/commands/node.rb | 42 | ||||
| -rw-r--r-- | lib/leap_cli/commands/node_init.rb | 7 | ||||
| -rw-r--r-- | lib/leap_cli/commands/vm.rb | 399 | 
3 files changed, 439 insertions, 9 deletions
| diff --git a/lib/leap_cli/commands/node.rb b/lib/leap_cli/commands/node.rb index 15575d4f..60540de9 100644 --- a/lib/leap_cli/commands/node.rb +++ b/lib/leap_cli/commands/node.rb @@ -19,9 +19,14 @@ module LeapCli; module Commands                      "Separate multiple values for a single property with a comma, like so: `leap node add mynode services:webapp,dns`"].join("\n\n")      node.arg_name 'NAME [SEED]' # , :optional => false, :multiple => false      node.command :add do |add| -      add.switch :local, :desc => 'Make a local testing node (by automatically assigning the next available local IP address). Local nodes are run as virtual machines on your computer.', :negatable => false +      add.switch :local, :desc => 'Make a local testing node (by assigning the next available local IP address). Local nodes are run as virtual machines on your computer.', :negatable => false +      add.switch :vm, :desc => 'Make a remote virtual machine for this node. Requires a valid cloud.json configuration.', :negatable => false        add.action do |global_options,options,args| -        add_node(global_options, options, args) +        if options[:vm] +          do_vm_add(global_options, options, args) +        else +          do_node_add(global_options, options, args) +        end        end      end @@ -29,7 +34,7 @@ module LeapCli; module Commands      node.arg_name 'OLD_NAME NEW_NAME'      node.command :mv do |mv|        mv.action do |global_options,options,args| -        move_node(global_options, options, args) +        do_node_move(global_options, options, args)        end      end @@ -37,7 +42,7 @@ module LeapCli; module Commands      node.arg_name 'NAME' #:optional => false #, :multiple => false      node.command :rm do |rm|        rm.action do |global_options,options,args| -        rm_node(global_options, options, args) +        do_node_rm(global_options, options, args)        end      end    end @@ -58,7 +63,10 @@ module LeapCli; module Commands    protected -  def add_node(global, options, args) +  # +  # additionally called by `leap vm add` +  # +  def do_node_add(global, options, args)      name = args.first      unless global[:force]        assert_files_missing! [:node_config, name] @@ -81,7 +89,7 @@ module LeapCli; module Commands    private -  def move_node(global, options, args) +  def do_node_move(global, options, args)      node = get_node_from_args(args, include_disabled: true)      new_name = args.last      Config::Node.validate_name!(new_name, node.vagrant?) @@ -91,14 +99,30 @@ module LeapCli; module Commands      end      remove_directory! [:node_files_dir, node.name]      rename_node_facts(node.name, new_name) +    if node.vm_id? +      node['name'] = new_name +      bind_server_to_node(node.vm.id, node, options) +    end    end -  def rm_node(global, options, args) +  def do_node_rm(global, options, args)      node = get_node_from_args(args, include_disabled: true) -    node.remove_files -    if node.vagrant? +    if node.vm? +      if !node.vm_id? +        log :warning, "The node #{node.name} is missing a 'vm.id' property. "+ +                      "You may have a virtual machine instance that is left "+ +                      "running. Check `leap vm status`" +      else +        msg = "The node #{node.name} appears to be associated with a virtual machine. " + +              "Do you want to also destroy this virtual machine? " +        if global[:yes] || agree(msg) +          do_vm_rm(global, options, args) +        end +      end +    elsif node.vagrant?        vagrant_command("destroy --force", [node.name])      end +    node.remove_files      remove_node_facts(node.name)    end diff --git a/lib/leap_cli/commands/node_init.rb b/lib/leap_cli/commands/node_init.rb index 62a57496..59661295 100644 --- a/lib/leap_cli/commands/node_init.rb +++ b/lib/leap_cli/commands/node_init.rb @@ -37,6 +37,13 @@ module LeapCli; module Commands        update_compiled_ssh_configs        # allow password auth for new nodes:        options[:auth_methods] = ["publickey", "password"] +      if node.vm? +        # new AWS virtual machines will only allow login as 'admin' +        # before we continue, we must enable root access. +        SSH.remote_command(node, options.merge(:user => 'admin')) do |ssh, host| +          ssh.scripts.allow_root_ssh +        end +      end        SSH.remote_command(node, options) do |ssh, host|          if node.vagrant?            ssh.scripts.install_insecure_vagrant_key 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 | 
