summaryrefslogtreecommitdiff
path: root/lib
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
parent6fab56fb40256fb2e541ee3ad61490f03254d38e (diff)
added 'leap vm' command
Diffstat (limited to 'lib')
-rw-r--r--lib/leap_cli/cloud.rb4
-rw-r--r--lib/leap_cli/cloud/cloud.rb310
-rw-r--r--lib/leap_cli/cloud/dependencies.rb40
-rw-r--r--lib/leap_cli/cloud/image.rb31
-rw-r--r--lib/leap_cli/commands/node.rb42
-rw-r--r--lib/leap_cli/commands/node_init.rb7
-rw-r--r--lib/leap_cli/commands/vm.rb399
-rw-r--r--lib/leap_cli/config/cloud.rb64
-rw-r--r--lib/leap_cli/config/environment.rb10
-rw-r--r--lib/leap_cli/config/filter.rb7
-rw-r--r--lib/leap_cli/config/manager.rb23
-rw-r--r--lib/leap_cli/config/node.rb13
-rw-r--r--lib/leap_cli/load_libraries.rb4
-rw-r--r--lib/leap_cli/log_filter.rb9
-rw-r--r--lib/leap_cli/ssh/backend.rb6
-rw-r--r--lib/leap_cli/ssh/key.rb123
-rw-r--r--lib/leap_cli/ssh/options.rb3
-rw-r--r--lib/leap_cli/ssh/remote_command.rb6
-rw-r--r--lib/leap_cli/ssh/scripts.rb15
-rw-r--r--lib/leap_cli/util/console_table.rb13
20 files changed, 1090 insertions, 39 deletions
diff --git a/lib/leap_cli/cloud.rb b/lib/leap_cli/cloud.rb
new file mode 100644
index 00000000..481dd6e7
--- /dev/null
+++ b/lib/leap_cli/cloud.rb
@@ -0,0 +1,4 @@
+
+require 'fog'
+require_relative 'cloud/cloud.rb'
+require_relative 'cloud/image.rb'
diff --git a/lib/leap_cli/cloud/cloud.rb b/lib/leap_cli/cloud/cloud.rb
new file mode 100644
index 00000000..f0bb45b8
--- /dev/null
+++ b/lib/leap_cli/cloud/cloud.rb
@@ -0,0 +1,310 @@
+#
+# An abstractions in front of Fog, which is an abstraction in front of
+# the AWS api. Oh my!
+#
+# A Cloud object binds a particular node with particular Fog
+# authentication credentials.
+#
+# NOTE: Possible AWS options for creating instances:
+#
+# options = {
+# 'BlockDeviceMapping' => block_device_mapping,
+# 'NetworkInterfaces' => network_interfaces,
+# 'ClientToken' => client_token,
+# 'DisableApiTermination' => disable_api_termination,
+# 'EbsOptimized' => ebs_optimized,
+# 'IamInstanceProfile.Arn' => @iam_instance_profile_arn,
+# 'IamInstanceProfile.Name' => @iam_instance_profile_name,
+# 'InstanceInitiatedShutdownBehavior' => instance_initiated_shutdown_behavior,
+# 'InstanceType' => flavor_id,
+# 'KernelId' => kernel_id,
+# 'KeyName' => key_name,
+# 'Monitoring.Enabled' => monitoring,
+# 'Placement.AvailabilityZone' => availability_zone,
+# 'Placement.GroupName' => placement_group,
+# 'Placement.Tenancy' => tenancy,
+# 'PrivateIpAddress' => private_ip_address,
+# 'RamdiskId' => ramdisk_id,
+# 'SecurityGroup' => groups,
+# 'SecurityGroupId' => security_group_ids,
+# 'SubnetId' => subnet_id,
+# 'UserData' => user_data,
+# }
+#
+
+module LeapCli
+ class Cloud
+ LEAP_SG_NAME = 'leap_default'
+ LEAP_SG_DESC = 'Default security group for LEAP nodes'
+
+ include LeapCli::LogCommand
+
+ attr_reader :compute # Fog::Compute object
+ attr_reader :node # Config::Node, if any
+ attr_reader :options # options for the VMs, if any
+ attr_reader :image # which vm image to use, if any
+ attr_reader :name # name of which entry in cloud.json to use
+
+ def initialize(name, conf, node=nil)
+ @node = node
+ @name = name
+ @conf = conf
+ @compute = nil
+ @options = nil
+ @image = nil
+
+ raise ArgumentError, 'name missing' unless @name
+ raise ArgumentError, 'config missing' unless @conf
+ raise ArgumentError, 'config auth missing' unless @conf["auth"]
+ raise ArgumentError, 'config auth missing' unless @conf["vendor"]
+
+ credentials = @conf["auth"].symbolize_keys
+ credentials[:provider] = @conf["vendor"]
+ @compute = Fog::Compute.new(credentials)
+
+ @options = @conf['default_options'] || {}
+ @image = @conf['default_image'] || aws_image(credentials[:region])
+ if @node
+ @options = node.vm.options if node['vm.options']
+ @image = node.vm.image if node['vm.image']
+ end
+ end
+
+ #
+ # fetches or creates a server for this cloud object.
+ #
+ def fetch_or_create_server(options)
+ fetch_server_for_node || create_new_vm_instance(choose_ssh_key: options[:choose_ssh_key])
+ end
+
+ #
+ # fetches the server for a particular node.
+ #
+ # return nil if this cloud object has no node, or there is no corresponding
+ # server.
+ #
+ def fetch_server_for_node(bail_on_failure=false)
+ server = nil
+ return nil unless @node
+
+ # does an instance exist that matches the node's vm.id?
+ if @node.vm_id?
+ instance_id = @node.vm.id
+ server = @compute.servers.get(instance_id)
+ end
+
+ # does an instance exist that is tagged with this node name?
+ if server.nil?
+ response = @compute.describe_instances({"tag:node_name" => @node.name})
+ # puts JSON.pretty_generate(response.body)
+ if !response.body["reservationSet"].empty?
+ instances = response.body["reservationSet"].first["instancesSet"]
+ if instances.size > 1
+ bail! "There are multiple VMs with the same node name tag! Manually remove one before continuing."
+ elsif instances.size == 1
+ instance_id = instances.first["instanceId"]
+ server = cloud.compute.servers.get(instance_id)
+ end
+ end
+ end
+
+ if server.nil? && bail_on_failure
+ bail! :error, "A virtual machine could not be found for node `#{@node.name}`. Things to try:" do
+ log "check the output of `leap vm status`"
+ log "check the value of `vm.id` in #{@node.name}.json"
+ log "run `leap vm add #{@node.name}` to create a corresponding virtual machine"
+ end
+ end
+
+ return server
+ end
+
+ #
+ # associates a node with a vm
+ #
+ def bind_server_to_node(server)
+ raise ArgumentError, 'no node' unless @node
+
+ # assign tag
+ @compute.create_tags(server.id, {'node_name' => @node.name})
+ log :created, "association between node '%s' and vm '%s'" % [@node.name, server.id]
+
+ # update node json
+ @node.update_json({
+ "ip_address" => server.public_ip_address,
+ "vm"=> {"id"=>server.id}
+ })
+ log "done", :color => :green, :style => :bold
+ end
+
+ #
+ # disassociates a node from a vm
+ #
+ def unbind_server_from_node(server)
+ raise ArgumentError, 'no node' unless @node
+
+ # assign tag
+ @compute.delete_tags(server.id, {'node_name' => @node.name})
+ log :removed, "association between node '%s' and vm '%s'" % [@node.name, server.id]
+
+ # update node json
+ @node.update_json({
+ "ip_address" => '0.0.0.0',
+ "vm"=> {"id" => ""}
+ })
+ end
+
+ #
+ # return an AWS KeyPair object, potentially uploading it to the server
+ # if necessary.
+ #
+ # this is used when initially creating the vm. After the first `node init`, then
+ # all sysadmins should have access to the server.
+ #
+ # NOTE: ssh and aws use different types of fingerprint
+ #
+ def find_or_create_key_pair(pick_ssh_key_method)
+ require 'leap_cli/ssh'
+ key_pair, local_key = match_ssh_key(:user_only => true)
+ if key_pair
+ log :using, "SSH key #{local_key.filename}" do
+ log 'AWS MD5 fingerprint: ' + local_key.fingerprint(:digest => :md5, :type => :der, :encoding => :hex)
+ 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
+ elsif key_pair.nil?
+ username, key = pick_ssh_key_method.call(self)
+ key_pair = upload_ssh_key(username, key)
+ end
+ return key_pair
+ end
+
+ #
+ # checks if there is a match between a local key and a registered key_pair
+ #
+ # options:
+ # :key_pair -- limit comparisons to this key_pair object.
+ # :user_only -- limit comparisons to the user's ~/.ssh directory only
+ #
+ # returns:
+ #
+ # key_pair -- an AWS KeyPair
+ # local_key -- a LeapCLi::SSH::Key
+ #
+ def match_ssh_key(options={})
+ key_pair = options[:key_pair]
+ local_keys_to_check = LeapCli::SSH::Key.my_public_keys
+ unless options[:user_only]
+ local_keys_to_check += LeapCli::SSH::Key.provider_public_keys
+ end
+ fingerprints = Hash[local_keys_to_check.map {|k|
+ [k.fingerprint(:digest => :md5, :type => :der, :encoding => :hex), k]
+ }]
+ key_pair ||= @compute.key_pairs.select {|key_pair|
+ fingerprints.include?(key_pair.fingerprint)
+ }.first
+ if key_pair
+ local_key = fingerprints[key_pair.fingerprint]
+ return key_pair, local_key
+ else
+ return nil, nil
+ end
+ end
+
+ private
+
+ #
+ # Every AWS instance requires a security group, which is just a simple firewall.
+ # In the future, we could create a separate security group for each node,
+ # and set the rules to match what the rules should be for that node.
+ #
+ # However, for now, we just use a security group 'leap_default' that opens
+ # all the ports.
+ #
+ # The default behavior for AWS security groups is:
+ # all ingress traffic is blocked and all egress traffic is allowed.
+ #
+ def find_or_create_security_group
+ group = @compute.security_groups.get(LEAP_SG_NAME)
+ if group.nil?
+ group = @compute.security_groups.create(
+ :name => LEAP_SG_NAME,
+ :description => LEAP_SG_DESC
+ )
+ group.authorize_port_range(0..65535,
+ :ip_protocol => 'tcp',
+ :cidr_ip => '0.0.0.0/0',
+ )
+ group.authorize_port_range(0..65535,
+ :ip_protocol => 'udp',
+ :cidr_ip => '0.0.0.0/0',
+ )
+ end
+ return group
+ end
+
+ #
+ # key - a LeapCli::SSH::Key object
+ # returns -- AWS KeyPair
+ #
+ def upload_ssh_key(username, key)
+ key_name = 'leap_' + username
+ key_pair = @compute.key_pairs.create(
+ :name => key_name,
+ :public_key => key.public_key.to_s
+ )
+ log :registered, "public key" do
+ log 'cloud provider: ' + @name
+ log 'name: ' + key_name
+ log 'AWS MD5 fingerprint: ' + key.fingerprint(:digest => :md5, :type => :der, :encoding => :hex)
+ log 'SSH MD5 fingerprint: ' + key.fingerprint(:digest => :md5, :type => :ssh, :encoding => :hex)
+ log 'SSH SHA256 fingerprint: ' + key.fingerprint(:digest => :sha256, :type => :ssh, :encoding => :base64)
+ end
+ return key_pair
+ end
+
+ def create_new_vm_instance(choose_ssh_key: nil)
+ log :creating, "new vm instance..."
+ assert! @image, "No image found. Specify `default_image` in cloud.json or `vm.image` in node's config."
+ if Fog.mock?
+ options = @options
+ else
+ key_pair = find_or_create_key_pair(choose_ssh_key)
+ security_group = find_or_create_security_group
+ options = @options.merge({
+ 'KeyName' => key_pair.name,
+ 'SecurityGroup' => security_group.name
+ })
+ end
+ response = @compute.run_instances(
+ @image,
+ 1, # min count
+ 1, # max count
+ options
+ )
+ instance_id = response.body["instancesSet"].first["instanceId"]
+ log :created, "vm with instance id #{instance_id}."
+ server = @compute.servers.get(instance_id)
+ if server.nil?
+ bail! :error, "could not query instance '#{instance_id}'."
+ end
+ unless Fog.mock?
+ tries = 0
+ server.wait_for {
+ if tries > 0
+ LeapCli.log :waiting, "for IP address to be assigned..."
+ end
+ tries += 1
+ !public_ip_address.nil?
+ }
+ if options[:wait]
+ log :waiting, "for vm #{instance_id} to start..."
+ server.wait_for { ready? }
+ log :started, "#{instance_id} with #{server.public_ip_address}"
+ end
+ end
+ return server
+ end
+
+ end
+end \ No newline at end of file
diff --git a/lib/leap_cli/cloud/dependencies.rb b/lib/leap_cli/cloud/dependencies.rb
new file mode 100644
index 00000000..fd690e59
--- /dev/null
+++ b/lib/leap_cli/cloud/dependencies.rb
@@ -0,0 +1,40 @@
+#
+# I am not sure this is a good idea, but it might be. Tricky, so disabled for now
+#
+
+=begin
+module LeapCli
+ class Cloud
+
+ def self.check_required_gems
+ begin
+ require "fog"
+ rescue LoadError
+ bail! do
+ log :error, "The 'vm' command requires the gem 'fog-core'. Please run `gem install fog-core` and try again."
+ end
+ end
+
+ fog_gems = @cloud.required_gems
+ if !options[:mock] && fog_gems.empty?
+ bail! do
+ log :warning, "no vm providers are configured in cloud.json."
+ log "You must have credentials for one of: #{@cloud.possible_apis.join(', ')}."
+ end
+ end
+
+ fog_gems.each do |name, gem_name|
+ begin
+ require gem_name.sub('-','/')
+ rescue LoadError
+ bail! do
+ log :error, "The 'vm' command requires the gem '#{gem_name}' (because of what is configured in cloud.json)."
+ log "Please run `sudo gem install #{gem_name}` and try again."
+ end
+ end
+ end
+ end
+
+ end
+end
+=end \ No newline at end of file
diff --git a/lib/leap_cli/cloud/image.rb b/lib/leap_cli/cloud/image.rb
new file mode 100644
index 00000000..1f7f47b9
--- /dev/null
+++ b/lib/leap_cli/cloud/image.rb
@@ -0,0 +1,31 @@
+module LeapCli
+ class Cloud
+
+ #
+ # returns the latest official debian image for
+ # a particular AWS region
+ #
+ # https://wiki.debian.org/Cloud/AmazonEC2Image/Jessie
+ # current list based on Debian 8.4
+ #
+ # might return nil if no image is found.
+ #
+ def self.aws_image(region)
+ image_list = %q[
+ ap-northeast-1 ami-d7d4c5b9
+ ap-northeast-2 ami-9a03caf4
+ ap-southeast-1 ami-73974210
+ ap-southeast-2 ami-09daf96a
+ eu-central-1 ami-ccc021a3
+ eu-west-1 ami-e079f893
+ sa-east-1 ami-d3ae21bf
+ us-east-1 ami-c8bda8a2
+ us-west-1 ami-45374b25
+ us-west-2 ami-98e114f8
+ ]
+ region_to_image = Hash[image_list.strip.split("\n").map{|i| i.split(" ")}]
+ return region_to_image[region]
+ end
+
+ end
+end \ No newline at end of file
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
diff --git a/lib/leap_cli/config/cloud.rb b/lib/leap_cli/config/cloud.rb
new file mode 100644
index 00000000..e3e5c1f1
--- /dev/null
+++ b/lib/leap_cli/config/cloud.rb
@@ -0,0 +1,64 @@
+# encoding: utf-8
+#
+# A class for the cloud.json file
+#
+# Example format:
+#
+# {
+# "my_aws": {
+# "api": "aws",
+# "vendor": "aws",
+# "auth": {
+# "region": "us-west-2",
+# "aws_access_key_id": "xxxxxxxxxxxxxxx",
+# "aws_secret_access_key": "xxxxxxxxxxxxxxxxxxxxxxxxxx"
+# },
+# "default_image": "ami-98e114f8",
+# "default_options": {
+# "InstanceType": "t2.nano"
+# }
+# }
+# }
+#
+
+module LeapCli; module Config
+
+ # http://fog.io/about/supported_services.html
+ VM_APIS = {
+ 'aws' => 'fog-aws',
+ 'google' => 'fog-google',
+ 'libvirt' => 'fog-libvirt',
+ 'openstack' => 'fog-openstack',
+ 'rackspace' => 'fog-rackspace'
+ }
+
+ class Cloud < Hash
+ def initialize(env=nil)
+ end
+
+ #
+ # returns hash, each key is the name of an API that is
+ # needed and the value is the name of the gem.
+ #
+ # only provider APIs that are required because they are present
+ # in cloud.json are included.
+ #
+ def required_gems
+ required = {}
+ self.each do |name, conf|
+ api = conf["api"]
+ required_gems[api] = VM_APIS[api]
+ end
+ return required
+ end
+
+ #
+ # returns an array of all possible providers
+ #
+ def possible_apis
+ VM_APIS.keys
+ end
+
+ end
+
+end; end
diff --git a/lib/leap_cli/config/environment.rb b/lib/leap_cli/config/environment.rb
index 398fd023..dadd9eaf 100644
--- a/lib/leap_cli/config/environment.rb
+++ b/lib/leap_cli/config/environment.rb
@@ -30,10 +30,12 @@ module LeapCli; module Config
# shared, non-inheritable
def nodes; @@nodes; end
def secrets; @@secrets; end
+ def cloud; @@cloud; end
def initialize(manager, name, search_dir, parent, options={})
@@nodes ||= nil
@@secrets ||= nil
+ @@cloud ||= nil
@manager = manager
@name = name
@@ -64,6 +66,7 @@ module LeapCli; module Config
@partials = Config::ObjectList.new
@provider = Config::Provider.new
@common = Config::Object.new
+ @cloud = Config::Cloud.new
return
end
@@ -89,7 +92,9 @@ module LeapCli; module Config
@partials.values.each {|partial| partial.delete('name'); }
#
- # shared: currently non-inheritable
+ # shared
+ #
+ # shared configs are also non-inheritable
# load the first ones we find, and only those.
#
if @@nodes.nil? || @@nodes.empty?
@@ -98,6 +103,9 @@ module LeapCli; module Config
if @@secrets.nil? || @@secrets.empty?
@@secrets = load_json(Path.named_path(:secrets_config, search_dir), Config::Secrets, options)
end
+ if @@cloud.nil? || @@cloud.empty?
+ @@cloud = load_json(Path.named_path(:cloud_config, search_dir), Config::Cloud)
+ end
end
#
diff --git a/lib/leap_cli/config/filter.rb b/lib/leap_cli/config/filter.rb
index 27502577..07424894 100644
--- a/lib/leap_cli/config/filter.rb
+++ b/lib/leap_cli/config/filter.rb
@@ -29,6 +29,7 @@ module LeapCli
# options -- hash, possible keys include
# :nopin -- disregard environment pinning
# :local -- if false, disallow local nodes
+ # :warning -- if false, don't print a warning when no nodes are found.
#
# A nil value in the filters array indicates
# the default environment. This is in order to support
@@ -139,9 +140,11 @@ module LeapCli
return @manager.env('_all_').services[name].node_list
elsif @manager.tags[name]
return @manager.env('_all_').tags[name].node_list
- else
+ elsif @options[:warning] != false
LeapCli.log :warning, "filter '#{name}' does not match any node names, tags, services, or environments."
return Config::ObjectList.new
+ else
+ return Config::ObjectList.new
end
else
node_list = Config::ObjectList.new
@@ -153,7 +156,7 @@ module LeapCli
@environments.each do |env|
node_list.merge!(@manager.env(env).tags[name].node_list)
end
- else
+ elsif @options[:warning] != false
LeapCli.log :warning, "filter '#{name}' does not match any node names, tags, services, or environments."
end
return node_list
diff --git a/lib/leap_cli/config/manager.rb b/lib/leap_cli/config/manager.rb
index 62eaa894..b42f1ae0 100644
--- a/lib/leap_cli/config/manager.rb
+++ b/lib/leap_cli/config/manager.rb
@@ -315,19 +315,20 @@ module LeapCli
end
# inherit from tags
+ node['tags'] = (node['tags'] || []).to_a
if node.vagrant?
- node['tags'] = (node['tags'] || []).to_a + ['local']
+ node['tags'] << 'local'
+ elsif node['vm']
+ node['tags'] << 'vm'
end
- if node['tags']
- node['tags'].to_a.each do |node_tag|
- tag = node_env.tags[node_tag]
- if tag.nil?
- msg = 'in node "%s": the tag "%s" does not exist.' % [node['name'], node_tag]
- log 0, :error, msg
- raise LeapCli::ConfigError.new(node, "error " + msg) if throw_exceptions
- else
- new_node.deep_merge!(tag)
- end
+ node['tags'].each do |node_tag|
+ tag = node_env.tags[node_tag]
+ if tag.nil?
+ msg = 'in node "%s": the tag "%s" does not exist.' % [node['name'], node_tag]
+ log 0, :error, msg
+ raise LeapCli::ConfigError.new(node, "error " + msg) if throw_exceptions
+ else
+ new_node.deep_merge!(tag)
end
end
diff --git a/lib/leap_cli/config/node.rb b/lib/leap_cli/config/node.rb
index 2d76b814..23abdee3 100644
--- a/lib/leap_cli/config/node.rb
+++ b/lib/leap_cli/config/node.rb
@@ -9,11 +9,8 @@ module LeapCli; module Config
class Node < Object
attr_accessor :file_paths
- def initialize(environment=nil) #, name=nil)
+ def initialize(environment=nil)
super(environment)
- #if name
- # self['name'] = name
- #end
@node = self
@file_paths = []
end
@@ -38,6 +35,14 @@ module LeapCli; module Config
return vagrant_range.include?(ip_addr)
end
+ def vm?
+ self['vm']
+ end
+
+ def vm_id?
+ self['vm.id'] && !self['vm.id'].empty?
+ end
+
#
# Return a hash table representation of ourselves, with the key equal to the @node.name,
# and the value equal to the fields specified in *keys.
diff --git a/lib/leap_cli/load_libraries.rb b/lib/leap_cli/load_libraries.rb
index cec3812d..01384f78 100644
--- a/lib/leap_cli/load_libraries.rb
+++ b/lib/leap_cli/load_libraries.rb
@@ -8,14 +8,16 @@ require 'leap_cli/log_filter'
require 'leap_cli/config/object'
require 'leap_cli/config/node'
+require 'leap_cli/config/node_cert'
require 'leap_cli/config/tag'
require 'leap_cli/config/provider'
require 'leap_cli/config/secrets'
+require 'leap_cli/config/cloud'
require 'leap_cli/config/object_list'
require 'leap_cli/config/filter'
require 'leap_cli/config/environment'
require 'leap_cli/config/manager'
require 'leap_cli/util/secret'
-require 'leap_cli/util/x509'
require 'leap_cli/util/vagrant'
+require 'leap_cli/util/console_table'
diff --git a/lib/leap_cli/log_filter.rb b/lib/leap_cli/log_filter.rb
index 0d745cc2..28504e1a 100644
--- a/lib/leap_cli/log_filter.rb
+++ b/lib/leap_cli/log_filter.rb
@@ -99,11 +99,15 @@ module LeapCli
#
TITLE_FORMATTERS = [
# red
- { :match => /error/, :color => :red, :style => :bold },
{ :match => /fatal_error/, :replace => 'fatal error:', :color => :red, :style => :bold },
+ { :match => /error/, :color => :red, :style => :bold },
{ :match => /removed/, :color => :red, :style => :bold },
+ { :match => /removing/, :color => :red, :style => :bold },
+ { :match => /destroyed/, :color => :red, :style => :bold },
+ { :match => /destroying/, :color => :red, :style => :bold },
+ { :match => /terminated/, :color => :red, :style => :bold },
{ :match => /failed/, :replace => 'FAILED', :color => :red, :style => :bold },
- { :match => /bail/, :replace => 'bailing out', :color => :red, :style => :bold },
+ { :match => /bailing/, :replace => 'bailing', :color => :red, :style => :bold },
{ :match => /invalid/, :color => :red, :style => :bold },
# yellow
@@ -115,6 +119,7 @@ module LeapCli
{ :match => /created/, :color => :green, :style => :bold },
{ :match => /completed/, :color => :green, :style => :bold },
{ :match => /ran/, :color => :green, :style => :bold },
+ { :match => /registered/, :color => :green, :style => :bold },
# cyan
{ :match => /note/, :replace => 'NOTE:', :color => :cyan, :style => :bold },
diff --git a/lib/leap_cli/ssh/backend.rb b/lib/leap_cli/ssh/backend.rb
index 80203b61..42e58c15 100644
--- a/lib/leap_cli/ssh/backend.rb
+++ b/lib/leap_cli/ssh/backend.rb
@@ -46,6 +46,12 @@ module LeapCli
Thread.current["sshkit_backend"] = nil
end
+ # if set, all the commands will begin with:
+ # sudo -u #{@user} -- sh -c '<command>'
+ def set_user(user='root')
+ @user = user
+ end
+
#
# like default capture, but gracefully logs failures for us
# last argument can be an options hash.
diff --git a/lib/leap_cli/ssh/key.rb b/lib/leap_cli/ssh/key.rb
index ad1ecf15..76223b7e 100644
--- a/lib/leap_cli/ssh/key.rb
+++ b/lib/leap_cli/ssh/key.rb
@@ -2,12 +2,34 @@
# A wrapper around OpenSSL::PKey::RSA instances to provide a better api for
# dealing with SSH keys.
#
-# NOTE: cipher 'ssh-ed25519' not supported yet because we are waiting
+# NOTES:
+#
+# cipher 'ssh-ed25519' not supported yet because we are waiting
# for support in Net::SSH
#
+# there are many ways to represent an SSH key, since SSH keys can be of
+# a variety of types.
+#
+# To confuse matters more, there are multiple binary representations.
+# So, for example, an RSA key has a native SSH representation
+# (two bignums, e followed by n), and a DER representation.
+#
+# AWS uses fingerprints of the DER representation, but SSH typically reports
+# fingerprints of the SSH representation.
+#
+# Also, SSH public key files are base64 encoded, but with whitespace removed
+# so it all goes on one line.
+#
+# Some useful links:
+#
+# https://stackoverflow.com/questions/3162155/convert-rsa-public-key-to-rsa-der
+# https://net-ssh.github.io/ssh/v2/api/classes/Net/SSH/Buffer.html
+# https://serverfault.com/questions/603982/why-does-my-openssh-key-fingerprint-not-match-the-aws-ec2-console-keypair-finger
+#
require 'net/ssh'
require 'forwardable'
+require 'base64'
module LeapCli
module SSH
@@ -72,6 +94,14 @@ module LeapCli
public_key || private_key
end
+ def self.my_public_keys
+ load_keys_from_paths File.join(ENV['HOME'], '.ssh', '*.pub')
+ end
+
+ def self.provider_public_keys
+ load_keys_from_paths Path.named_path([:user_ssh, '*'])
+ end
+
#
# Picks one key out of an array of keys that we think is the "best",
# based on the order of preference in SUPPORTED_TYPES
@@ -127,19 +157,29 @@ module LeapCli
end
end
+ private
+
+ def self.load_keys_from_paths(key_glob)
+ keys = []
+ Dir.glob(key_glob).each do |file|
+ key = Key.load(file)
+ if key && key.public?
+ keys << key
+ end
+ end
+ return keys
+ end
+
##
## INSTANCE METHODS
##
public
- def initialize(rsa_key)
- @key = rsa_key
+ def initialize(p_key)
+ @key = p_key
end
- def_delegator :@key, :fingerprint, :fingerprint
- def_delegator :@key, :public?, :public?
- def_delegator :@key, :private?, :private?
def_delegator :@key, :ssh_type, :type
def_delegator :@key, :public_encrypt, :public_encrypt
def_delegator :@key, :public_decrypt, :public_decrypt
@@ -156,6 +196,70 @@ module LeapCli
Key.new(@key.private_key)
end
+ def private?
+ @key.respond_to?(:private?) ? @key.private? : @key.private_key?
+ end
+
+ def public?
+ @key.respond_to?(:public?) ? @key.public? : @key.public_key?
+ end
+
+ #
+ # three arguments:
+ #
+ # - digest: one of md5, sha1, sha256, etc. (default sha256)
+ # - encoding: either :hex (default) or :base64
+ # - type: fingerprint type, either :ssh (default) or :der
+ #
+ # NOTE:
+ #
+ # * I am not sure how to make a fingerprint for OpenSSL::PKey::EC::Point
+ #
+ # * AWS reports fingerprints using MD5 digest for uploaded ssh keys,
+ # but SHA1 for keys it created itself.
+ #
+ # * Also, AWS fingerprints are digests on the DER encoding of the key.
+ # But standard SSH fingerprints are digests of SSH encoding of the key.
+ #
+ # * Other tools will sometimes display fingerprints in hex and sometimes
+ # in base64. Arrrgh.
+ #
+ def fingerprint(type: :ssh, digest: :sha256, encoding: :hex)
+ require 'digest'
+
+ digest = digest.to_s.upcase
+ digester = case digest
+ when "MD5" then Digest::MD5.new
+ when "SHA1" then Digest::SHA1.new
+ when "SHA256" then Digest::SHA256.new
+ when "SHA384" then Digest::SHA384.new
+ when "SHA512" then Digest::SHA512.new
+ else raise ArgumentError, "digest #{digest} is unknown"
+ end
+
+ keymatter = nil
+ if type == :der && @key.respond_to?(:to_der)
+ keymatter = @key.to_der
+ else
+ keymatter = self.raw_key.to_s
+ end
+
+ fp = nil
+ if encoding == :hex
+ fp = digester.hexdigest(keymatter)
+ elsif encoding == :base64
+ fp = Base64.encode64(digester.digest(keymatter)).sub(/=$/, '')
+ else
+ raise ArgumentError, "encoding #{encoding} not understood"
+ end
+
+ if digest == "MD5" && encoding == :hex
+ return fp.scan(/../).join(':')
+ else
+ return fp
+ end
+ end
+
#
# not sure if this will always work, but is seems to for now.
#
@@ -175,10 +279,17 @@ module LeapCli
self.type + " " + self.key
end
+ #
+ # base64 encoding of the key, with spaces removed.
+ #
def key
[Net::SSH::Buffer.from(:key, @key).to_s].pack("m*").gsub(/\s/, "")
end
+ def raw_key
+ Net::SSH::Buffer.from(:key, @key)
+ end
+
def ==(other_key)
return false if other_key.nil?
return false if self.class != other_key.class
diff --git a/lib/leap_cli/ssh/options.rb b/lib/leap_cli/ssh/options.rb
index b8266d11..7bc06564 100644
--- a/lib/leap_cli/ssh/options.rb
+++ b/lib/leap_cli/ssh/options.rb
@@ -46,6 +46,9 @@ module LeapCli
if args[:auth_methods]
ssh_options[:auth_methods] = args[:auth_methods]
end
+ if args[:user]
+ ssh_options[:user] = args[:user]
+ end
return ssh_options
end
diff --git a/lib/leap_cli/ssh/remote_command.rb b/lib/leap_cli/ssh/remote_command.rb
index 7195405e..0e9f2d55 100644
--- a/lib/leap_cli/ssh/remote_command.rb
+++ b/lib/leap_cli/ssh/remote_command.rb
@@ -38,6 +38,7 @@ module LeapCli
# :port -- ssh port
# :ip -- ssh ip
# :auth_methods -- e.g. ["pubkey", "password"]
+ # :user -- default 'root'
#
def self.remote_command(nodes, options={}, &block)
CustomCoordinator.new(
@@ -47,6 +48,11 @@ module LeapCli
)
).each do |ssh, host|
LeapCli.log 2, "ssh options for #{host.hostname}: #{host.ssh_options.inspect}"
+ if host.user != 'root'
+ # if the ssh user is not root, we want to make the ssh commands
+ # switch to root before they are run:
+ ssh.set_user('root')
+ end
yield ssh, host
end
end
diff --git a/lib/leap_cli/ssh/scripts.rb b/lib/leap_cli/ssh/scripts.rb
index a15a9edd..9fef6240 100644
--- a/lib/leap_cli/ssh/scripts.rb
+++ b/lib/leap_cli/ssh/scripts.rb
@@ -119,6 +119,21 @@ module LeapCli
end
end
+ #
+ # AWS debian images only allow you to login as admin. This is done with a
+ # custom command in /root/.ssh/authorized_keys, instead of by modifying
+ # /etc/ssh/sshd_config.
+ #
+ # We need to be able to ssh as root for scp and rsync to work.
+ #
+ # This command is run as 'admin', with a sudo wrapper. In order for the
+ # sudo to work, the command must be specified as separate arguments with
+ # no spaces (that is how ssh-kit works).
+ #
+ def allow_root_ssh
+ ssh.execute 'cp', '/home/admin/.ssh/authorized_keys', '/root/.ssh/authorized_keys'
+ end
+
private
def flagize(hsh)
diff --git a/lib/leap_cli/util/console_table.rb b/lib/leap_cli/util/console_table.rb
index 53c5e18a..ccdcc2ab 100644
--- a/lib/leap_cli/util/console_table.rb
+++ b/lib/leap_cli/util/console_table.rb
@@ -3,9 +3,12 @@ module LeapCli; module Util
class ConsoleTable
def table
@rows = []
+ @cell_options = []
+
@row_options = []
@column_widths = []
@column_options = []
+
@current_row = 0
@current_column = 0
yield
@@ -13,6 +16,8 @@ module LeapCli; module Util
def row(options=nil)
@current_column = 0
+ @rows[@current_row] = []
+ @cell_options[@current_row] = []
@row_options[@current_row] ||= options
yield
@current_row += 1
@@ -20,8 +25,8 @@ module LeapCli; module Util
def column(str, options={})
str ||= ""
- @rows[@current_row] ||= []
@rows[@current_row][@current_column] = str
+ @cell_options[@current_row][@current_column] = options
@column_widths[@current_column] = [str.length, options[:min_width]||0, @column_widths[@current_column]||0].max
@column_options[@current_column] ||= options
@current_column += 1
@@ -33,8 +38,10 @@ module LeapCli; module Util
row.each_with_index do |column, j|
align = (@column_options[j]||{})[:align] || "left"
width = @column_widths[j]
- if color
- str = LeapCli.logger.colorize(column, color)
+ cell_color = @cell_options[i][j] && @cell_options[i][j][:color]
+ cell_color ||= color
+ if cell_color
+ str = LeapCli.logger.colorize(column, cell_color)
extra_width = str.length - column.length
else
str = column