diff --git a/README.md b/README.md index c12aba3..ff40700 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,19 @@ Optionally, list images provided by DigitalOcean as well. pearkes (id: 10501) ... +### Wait for Droplet State + +Sometimes you want to wait for a droplet to enter some state, for +example "off". + + $ tugboat wait admin --state off + Droplet fuzzy name provided. Finding droplet ID...done, 13231512 (pearkes-admin-001) + Waiting for droplet to become off.... + ... + +This will simply block until the droplet returns a state of "off". +A period will be printed after each request. + ## Help If you're curious about command flags for a specific command, you can diff --git a/lib/tugboat/cli.rb b/lib/tugboat/cli.rb index 7340e26..75aca6d 100644 --- a/lib/tugboat/cli.rb +++ b/lib/tugboat/cli.rb @@ -9,7 +9,9 @@ class CLI < Thor !check_unknown_options - map "--version" => :version, "-v" => :version + map "--version" => :version, + "-v" => :version, + "password-reset" => :password_reset desc "help [COMMAND]", "Describe commands or a specific command" def help(meth=nil) @@ -273,6 +275,46 @@ def resize(name=nil) "user_droplet_fuzzy_name" => name }) end + + desc "password-reset FUZZY_NAME", "Reset root password" + method_option "id", + :type => :numeric, + :aliases => "-i", + :desc => "The ID of the droplet." + method_option "name", + :type => :string, + :aliases => "-n", + :desc => "The exact name of the droplet" + def password_reset(name=nil) + Middleware.sequence_password_reset.call({ + "user_droplet_id" => options[:id], + "user_droplet_name" => options[:name], + "user_droplet_fuzzy_name" => name + }) + end + + desc "wait FUZZY_NAME", "Wait for a droplet to reach a state" + method_option "id", + :type => :numeric, + :aliases => "-i", + :desc => "The ID of the droplet." + method_option "name", + :type => :string, + :aliases => "-n", + :desc => "The exact name of the droplet" + method_option "state", + :type => :string, + :aliases => "-s", + :default => "active", + :desc => "The state of the droplet to wait for" + def wait(name=nil) + Middleware.sequence_wait.call({ + "user_droplet_id" => options[:id], + "user_droplet_name" => options[:name], + "user_droplet_desired_state" => options[:state], + "user_droplet_fuzzy_name" => name + }) + end end end diff --git a/lib/tugboat/middleware.rb b/lib/tugboat/middleware.rb index b2b409d..3d54e5a 100644 --- a/lib/tugboat/middleware.rb +++ b/lib/tugboat/middleware.rb @@ -26,6 +26,8 @@ module Middleware autoload :ListSizes, "tugboat/middleware/list_sizes" autoload :CheckDropletActive, "tugboat/middleware/check_droplet_active" autoload :CheckDropletInactive, "tugboat/middleware/check_droplet_inactive" + autoload :PasswordReset, "tugboat/middleware/password_reset" + autoload :WaitForState, "tugboat/middleware/wait_for_state" # Start the authorization flow. # This writes a ~/.tugboat file, which can be edited manually. @@ -192,5 +194,27 @@ def self.sequence_resize_droplet use ResizeDroplet end end + + # Reset root password + def self.sequence_password_reset + ::Middleware::Builder.new do + use InjectConfiguration + use CheckConfiguration + use InjectClient + use FindDroplet + use PasswordReset + end + end + + # Wait for a droplet to enter a desired state + def self.sequence_wait + ::Middleware::Builder.new do + use InjectConfiguration + use CheckConfiguration + use InjectClient + use FindDroplet + use WaitForState + end + end end end diff --git a/lib/tugboat/middleware/password_reset.rb b/lib/tugboat/middleware/password_reset.rb new file mode 100644 index 0000000..e5d979c --- /dev/null +++ b/lib/tugboat/middleware/password_reset.rb @@ -0,0 +1,23 @@ +module Tugboat + module Middleware + class PasswordReset < Base + def call(env) + ocean = env["ocean"] + + say "Queuing password reset for #{env["droplet_id"]} #{env["droplet_name"]}...", nil, false + res = ocean.droplets.password_reset env["droplet_id"] + + if res.status == "ERROR" + say res.error_message, :red + exit 1 + end + + say "done", :green + say "Your new root password will be emailed to you" + + @app.call(env) + end + end + end +end + diff --git a/lib/tugboat/middleware/wait_for_state.rb b/lib/tugboat/middleware/wait_for_state.rb new file mode 100644 index 0000000..9224199 --- /dev/null +++ b/lib/tugboat/middleware/wait_for_state.rb @@ -0,0 +1,35 @@ +module Tugboat + module Middleware + class WaitForState < Base + def call(env) + ocean = env["ocean"] + + say "Waiting for droplet to become #{env["user_droplet_desired_state"]}.", nil, false + + start_time = Time.now + + req = ocean.droplets.show env["droplet_id"] + + say ".", nil, false + + if req.status == "ERROR" + say req.error_message, :red + exit 1 + end + + while req.droplet.status != env["user_droplet_desired_state"] do + sleep 2 + req = ocean.droplets.show env["droplet_id"] + say ".", nil, false + end + + total_time = (Time.now - start_time).to_i + + say "done#{CLEAR} (#{total_time}s)", :green + + @app.call(env) + end + end + end +end + diff --git a/spec/cli/password_reset_cli_spec.rb b/spec/cli/password_reset_cli_spec.rb new file mode 100644 index 0000000..2bd9d7a --- /dev/null +++ b/spec/cli/password_reset_cli_spec.rb @@ -0,0 +1,83 @@ +require 'spec_helper' + +describe Tugboat::CLI do + include_context "spec" + + describe "passwordreset" do + it "resets the root password given a fuzzy name" do + stub_request(:get, "https://api.digitalocean.com/droplets?api_key=#{api_key}&client_id=#{client_key}"). + to_return(:status => 200, :body => fixture("show_droplets")) + stub_request(:post, "https://api.digitalocean.com/droplets/100823/password_reset?api_key=#{api_key}&client_id=#{client_key}"). + to_return(:status => 200, :body => '{"status":"OK","event_id":456}') + + @cli.password_reset("foo") + + expect($stdout.string).to eq <<-eos +Droplet fuzzy name provided. Finding droplet ID...done\e[0m, 100823 (foo) +Queuing password reset for 100823 (foo)...done +Your new root password will be emailed to you + eos + + expect(a_request(:get, "https://api.digitalocean.com/droplets?api_key=#{api_key}&client_id=#{client_key}")). + to have_been_made + expect(a_request(:post, "https://api.digitalocean.com/droplets/100823/password_reset?api_key=#{api_key}&client_id=#{client_key}")). + to have_been_made + end + + it "resets the root password given an id" do + stub_request(:get, "https://api.digitalocean.com/droplets/100823?api_key=#{api_key}&client_id=#{client_key}"). + to_return(:status => 200, :body => fixture("show_droplet")) + stub_request(:post, "https://api.digitalocean.com/droplets/100823/password_reset?api_key=#{api_key}&client_id=#{client_key}"). + to_return(:status => 200, :body => '{"status":"OK","event_id":456}') + + @cli.options = @cli.options.merge(:id => 100823) + @cli.password_reset + + expect($stdout.string).to eq <<-eos +Droplet id provided. Finding Droplet...done\e[0m, 100823 (foo) +Queuing password reset for 100823 (foo)...done +Your new root password will be emailed to you + eos + + expect(a_request(:get, "https://api.digitalocean.com/droplets/100823?api_key=#{api_key}&client_id=#{client_key}")). + to have_been_made + expect(a_request(:post, "https://api.digitalocean.com/droplets/100823/password_reset?api_key=#{api_key}&client_id=#{client_key}")). + to have_been_made + end + + it "resets the root password given a name" do + stub_request(:get, "https://api.digitalocean.com/droplets?api_key=#{api_key}&client_id=#{client_key}"). + to_return(:status => 200, :body => fixture("show_droplets")) + stub_request(:post, "https://api.digitalocean.com/droplets/100823/password_reset?api_key=#{api_key}&client_id=#{client_key}"). + to_return(:status => 200, :body => '{"status":"OK","event_id":456}') + + @cli.options = @cli.options.merge(:name => "foo") + @cli.password_reset + + expect($stdout.string).to eq <<-eos +Droplet name provided. Finding droplet ID...done\e[0m, 100823 (foo) +Queuing password reset for 100823 (foo)...done +Your new root password will be emailed to you + eos + + expect(a_request(:get, "https://api.digitalocean.com/droplets?api_key=#{api_key}&client_id=#{client_key}")). + to have_been_made + expect(a_request(:post, "https://api.digitalocean.com/droplets/100823/password_reset?api_key=#{api_key}&client_id=#{client_key}")). + to have_been_made + end + + it "raises SystemExit when a request fails" do + stub_request(:get, "https://api.digitalocean.com/droplets?api_key=#{api_key}&client_id=#{client_key}"). + to_return(:status => 200, :body => fixture("show_droplets")) + stub_request(:post, "https://api.digitalocean.com/droplets/100823/password_reset?api_key=#{api_key}&client_id=#{client_key}"). + to_return(:status => 500, :body => '{"status":"ERROR","message":"Some error"}') + + expect { @cli.password_reset("foo") }.to raise_error(SystemExit) + + expect(a_request(:get, "https://api.digitalocean.com/droplets?api_key=#{api_key}&client_id=#{client_key}")). + to have_been_made + expect(a_request(:post, "https://api.digitalocean.com/droplets/100823/password_reset?api_key=#{api_key}&client_id=#{client_key}")). + to have_been_made + end + end +end diff --git a/spec/cli/wait_cli_spec.rb b/spec/cli/wait_cli_spec.rb new file mode 100644 index 0000000..b9c061f --- /dev/null +++ b/spec/cli/wait_cli_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe Tugboat::CLI do + include_context "spec" + + describe "wait" do + it "waits for a droplet with a fuzzy name" do + stub_request(:get, "https://api.digitalocean.com/droplets?api_key=#{api_key}&client_id=#{client_key}"). + to_return(:status => 200, :body => fixture("show_droplets")) + + stub_request(:get, "https://api.digitalocean.com/droplets/100823?api_key=#{api_key}&client_id=#{client_key}"). + to_return(:status => 200, :body => fixture("show_droplet")) + + @cli.options = @cli.options.merge(:state => "active") + @cli.wait("foo") + + expect($stdout.string).to eq <<-eos +Droplet fuzzy name provided. Finding droplet ID...done\e[0m, 100823 (foo) +Waiting for droplet to become active..done\e[0m (0s) +eos + + expect(a_request(:get, "https://api.digitalocean.com/droplets?api_key=#{api_key}&client_id=#{client_key}")).to have_been_made + expect(a_request(:get, "https://api.digitalocean.com/droplets/100823?api_key=#{api_key}&client_id=#{client_key}")).to have_been_made + end + + it "waits for a droplet with an id" do + stub_request(:get, "https://api.digitalocean.com/droplets/#{droplet_id}?api_key=#{api_key}&client_id=#{client_key}"). + to_return(:status => 200, :body => fixture("show_droplet")) + + stub_request(:get, "https://api.digitalocean.com/droplets/100823?api_key=#{api_key}&client_id=#{client_key}"). + to_return(:status => 200, :body => fixture("show_droplet")) + + @cli.options = @cli.options.merge(:id => droplet_id, :state => "active") + @cli.wait + + expect($stdout.string).to eq <<-eos +Droplet id provided. Finding Droplet...done\e[0m, 100823 (foo) +Waiting for droplet to become active..done\e[0m (0s) + eos + + expect(a_request(:get, "https://api.digitalocean.com/droplets/100823?api_key=#{api_key}&client_id=#{client_key}")).to have_been_made + expect(a_request(:get, "https://api.digitalocean.com/droplets/#{droplet_id}?api_key=#{api_key}&client_id=#{client_key}")).to have_been_made + end + + it "waits for a droplet with a name" do + stub_request(:get, "https://api.digitalocean.com/droplets?api_key=#{api_key}&client_id=#{client_key}"). + to_return(:status => 200, :body => fixture("show_droplets")) + + stub_request(:get, "https://api.digitalocean.com/droplets/100823?api_key=#{api_key}&client_id=#{client_key}"). + to_return(:status => 200, :body => fixture("show_droplet")) + + @cli.options = @cli.options.merge(:name => droplet_name, :state => "active") + @cli.wait + + expect($stdout.string).to eq <<-eos +Droplet name provided. Finding droplet ID...done\e[0m, 100823 (foo) +Waiting for droplet to become active..done\e[0m (0s) +eos + + expect(a_request(:get, "https://api.digitalocean.com/droplets/100823?api_key=#{api_key}&client_id=#{client_key}")).to have_been_made + expect(a_request(:get, "https://api.digitalocean.com/droplets?api_key=#{api_key}&client_id=#{client_key}")).to have_been_made + end + + end + +end