diff --git a/lib/tugboat/cli.rb b/lib/tugboat/cli.rb index 222c608..7d337d1 100644 --- a/lib/tugboat/cli.rb +++ b/lib/tugboat/cli.rb @@ -139,6 +139,48 @@ def ssh(name = nil) 'user_quiet' => options[:quiet]) end + desc 'scp FUZZY_NAME FROM_PATH TO_PATH', 'scp files to a droplet' + method_option 'id', + type: :string, + aliases: '-i', + desc: 'The ID of the droplet.' + method_option 'from_path', + type: :string, + aliases: '-from', + desc: 'The path of the local file' + method_option 'to_path', + type: :string, + aliases: '-to', + desc: 'The path to copy to on the remote droplet' + method_option 'name', + type: :string, + aliases: '-n', + desc: 'The exact name of the droplet' + method_option 'ssh_user', + type: :string, + aliases: '-u', + desc: 'Specifies which user to log in as' + method_option 'scp_command', + type: :string, + aliases: ['-c'], + desc: 'Command to run to copy the file, eg scp, rsync (defaults to scp)' + method_option 'wait', + type: :boolean, + aliases: '-w', + desc: 'Wait for droplet to become active before trying to SSH' + def scp(name = nil, from_path, to_path) + Middleware.sequence_scp_droplet.call('tugboat_action' => __method__, + 'user_droplet_id' => options[:id], + 'user_droplet_name' => options[:name], + 'user_droplet_fuzzy_name' => name, + 'user_from_file' => from_path, + 'user_to_file' => to_path, + 'user_droplet_ssh_user' => options[:ssh_user], + 'user_scp_command' => options[:scp_command], + 'user_droplet_ssh_wait' => options[:wait], + 'user_quiet' => options[:quiet]) + end + desc 'create NAME', 'Create a droplet.' method_option 'size', type: :string, diff --git a/lib/tugboat/middleware.rb b/lib/tugboat/middleware.rb index c81782a..5070e69 100644 --- a/lib/tugboat/middleware.rb +++ b/lib/tugboat/middleware.rb @@ -32,6 +32,7 @@ module Middleware autoload :RestartDroplet, 'tugboat/middleware/restart_droplet' autoload :SnapshotDroplet, 'tugboat/middleware/snapshot_droplet' autoload :SSHDroplet, 'tugboat/middleware/ssh_droplet' + autoload :SCPDroplet, 'tugboat/middleware/scp_droplet' autoload :StartDroplet, 'tugboat/middleware/start_droplet' autoload :WaitForState, 'tugboat/middleware/wait_for_state' @@ -147,6 +148,18 @@ def self.sequence_ssh_droplet end end + # SSH into a droplet + def self.sequence_scp_droplet + ::Middleware::Builder.new do + use InjectConfiguration + use CheckConfiguration + use InjectClient + use FindDroplet + use CheckDropletActive + use SCPDroplet + end + end + # Create a droplet def self.sequence_create_droplet ::Middleware::Builder.new do diff --git a/lib/tugboat/middleware/scp_droplet.rb b/lib/tugboat/middleware/scp_droplet.rb new file mode 100644 index 0000000..d1863bb --- /dev/null +++ b/lib/tugboat/middleware/scp_droplet.rb @@ -0,0 +1,34 @@ +module Tugboat + module Middleware + class SCPDroplet < Base + def call(env) + say "Executing SCP on Droplet #{env['droplet_name']}..." + + identity = File.expand_path(env['config'].ssh_key_path.to_s).strip + + ssh_user = env['user_droplet_ssh_user'] || env['config'].ssh_user + + scp_command = env['user_scp_command'] || 'scp' + + host_ip = env['droplet_ip'] + + host_string = "#{ssh_user}@#{host_ip}" + + if env['user_droplet_ssh_wait'] + say 'Wait flag given, waiting for droplet to become active' + wait_for_state(env['droplet_id'], 'active', env['barge']) + end + + identity_string = "-i #{identity}" + + scp_command_string = [scp_command, identity_string, env['user_from_file'], "#{host_string}:#{env['user_to_file']}"].join(' ') + + say "Attempting SCP with `#{scp_command_string}`" + + Kernel.exec(scp_command_string) + + @app.call(env) + end + end + end +end diff --git a/spec/cli/scp_cli_spec.rb b/spec/cli/scp_cli_spec.rb new file mode 100644 index 0000000..ba2bbd2 --- /dev/null +++ b/spec/cli/scp_cli_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +describe Tugboat::CLI do + include_context 'spec' + + describe 'scp' do + it "tries to fetch the droplet's IP from the API" do + stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). + with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). + to_return(status: 200, body: fixture('show_droplets'), headers: {}) + + stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=200'). + to_return(headers: { 'Content-Type' => 'application/json' }, status: 200, body: fixture('show_droplets')) + allow(Kernel).to receive(:exec).with("scp -i #{Dir.home}/.ssh/id_rsa2 /tmp/foo baz@104.236.32.182:/tmp/bar") + +expected_string = <<-eos +Droplet fuzzy name provided. Finding droplet ID...done\e[0m, 6918990 (example.com) +Executing SCP on Droplet (example.com)... +Attempting SCP with `scp -i #{Dir.home}/.ssh/id_rsa2 /tmp/foo baz@104.236.32.182:/tmp/bar` + eos + + expect { cli.scp('example.com', '/tmp/foo', '/tmp/bar') }.to output(expected_string).to_stdout + end + + it "runs with rsync if given at the command line" do + stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). + with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). + to_return(status: 200, body: fixture('show_droplets'), headers: {}) + + stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=200'). + to_return(headers: { 'Content-Type' => 'application/json' }, status: 200, body: fixture('show_droplets')) + allow(Kernel).to receive(:exec).with("rsync -i #{Dir.home}/.ssh/id_rsa2 /tmp/foo baz@104.236.32.182:/tmp/bar") + +expected_string = <<-eos +Droplet fuzzy name provided. Finding droplet ID...done\e[0m, 6918990 (example.com) +Executing SCP on Droplet (example.com)... +Attempting SCP with `rsync -i #{Dir.home}/.ssh/id_rsa2 /tmp/foo baz@104.236.32.182:/tmp/bar` + eos + + cli.options = cli.options.merge(scp_command: 'rsync') + + expect { cli.scp('example.com', '/tmp/foo', '/tmp/bar') }.to output(expected_string).to_stdout + end + + it "wait's until droplet active if -w command is given and droplet eventually active" do + stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=1'). + with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). + to_return(status: 200, body: '', headers: {}) + + stub_request(:get, 'https://api.digitalocean.com/v2/droplets/6918990?per_page=200'). + with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer foo', 'Content-Type' => 'application/json', 'User-Agent' => 'Faraday v0.9.2' }). + to_return( + { status: 200, body: fixture('show_droplet_inactive'), headers: {} }, + status: 200, body: fixture('show_droplet'), headers: {} + ) + + stub_request(:get, 'https://api.digitalocean.com/v2/droplets?page=1&per_page=200'). + to_return(headers: { 'Content-Type' => 'application/json' }, status: 200, body: fixture('show_droplets')) + allow(Kernel).to receive(:exec).with("scp -i #{Dir.home}/.ssh/id_rsa2 /tmp/foo baz@104.236.32.182:/tmp/bar") + + cli.options = cli.options.merge(wait: true) + + expected_string = <<-eos +Droplet fuzzy name provided. Finding droplet ID...done\e[0m, 6918990 (example.com) +Executing SCP on Droplet (example.com)... +Wait flag given, waiting for droplet to become active +..done\e[0m (2s) +Attempting SCP with `scp -i #{Dir.home}/.ssh/id_rsa2 /tmp/foo baz@104.236.32.182:/tmp/bar` + eos + + expect { cli.scp('example.com', '/tmp/foo', '/tmp/bar') }.to output(expected_string).to_stdout + end + + end +end