tugboat 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,112 @@
1
+ module Tugboat
2
+ module Middleware
3
+ # Check if the client has set-up configuration yet.
4
+ class FindDroplet < Base
5
+ def call(env)
6
+ ocean = env["ocean"]
7
+ user_fuzzy_name = env['user_droplet_fuzzy_name']
8
+ user_droplet_name = env['user_droplet_name']
9
+ user_droplet_id = env['user_droplet_id']
10
+
11
+ # First, if nothing is provided to us, we should quit and
12
+ # let the user know.
13
+ if !user_fuzzy_name && !user_droplet_name && !user_droplet_id
14
+ say "Tugboat attempted to find a droplet with no arguments.", :red
15
+ return
16
+ end
17
+
18
+ # If you were to `tugboat restart foo -n foo-server-001` then we'd use
19
+ # 'foo-server-001' without looking up the fuzzy name.
20
+ #
21
+ # This is why we check in this order.
22
+
23
+ # Easy for us if they provide an id. Just set it to the droplet_id
24
+ if user_droplet_id
25
+ say "Droplet id provided. Finding Droplet...", nil, false
26
+ droplet = ocean.droplets.show user_droplet_id
27
+
28
+ env["droplet_id"] = droplet.id
29
+ env["droplet_name"] = "(#{droplet.name})"
30
+ env["droplet_ip"] = droplet.ip_address
31
+ end
32
+
33
+ # If they provide a name, we need to get the ID for it.
34
+ # This requires a lookup.
35
+ if user_droplet_name && !env["droplet_id"]
36
+ say "Droplet name provided. Finding droplet ID...", nil, false
37
+
38
+ # Look for the droplet by an exact name match.
39
+ ocean.droplets.list.droplets.each do |d|
40
+ if droplet.name == user_droplet_name
41
+ env["droplet_id"] = d.id
42
+ env["droplet_name"] = "(#{d.name})"
43
+ env["droplet_ip"] = d.ip_address
44
+ end
45
+ end
46
+
47
+ # If we coulnd't find it, tell the user and drop out of the
48
+ # sequence.
49
+ if !env["droplet_id"]
50
+ say "Unable to find a droplet named '#{user_droplet_name}'.", :red
51
+ return
52
+ end
53
+ end
54
+
55
+ # We only need to "fuzzy find" a droplet if a fuzzy name is provided,
56
+ # and we don't want to fuzzy search if an id or name is provided
57
+ # with a flag.
58
+ #
59
+ # This requires a lookup.
60
+ if user_fuzzy_name && !env["droplet_id"]
61
+ say "Droplet fuzzy name provided. Finding droplet ID...", nil, false
62
+
63
+ found_droplets = []
64
+ choices = []
65
+
66
+ ocean.droplets.list.droplets.each_with_index do |d, i|
67
+ # Check to see if one of the droplet names have the fuzzy string.
68
+ if d.name.include? user_fuzzy_name
69
+ found_droplets << d
70
+ end
71
+ end
72
+
73
+ # Check to see if we have more then one droplet, and prompt
74
+ # a user to choose otherwise.
75
+ if found_droplets.length == 1
76
+ env["droplet_id"] = found_droplets.first.id
77
+ env["droplet_name"] = "(#{found_droplets.first.name})"
78
+ env["droplet_ip"] = found_droplets.first.ip_address
79
+ elsif found_droplets.length > 1
80
+ # Did we run the multiple questionairre?
81
+ did_run_multiple = true
82
+
83
+ say "Multiple droplets found."
84
+ say
85
+ found_droplets.each_with_index do |d, i|
86
+ say "#{i}) #{d.name} (#{d.id})"
87
+ choices << i.to_s
88
+ end
89
+ say
90
+ choice = ask "Please choose a droplet:", :limited_to => choices
91
+ env["droplet_id"] = found_droplets[choice.to_i].id
92
+ env["droplet_name"] = found_droplets[choice.to_i].name
93
+ env["droplet_ip"] = found_droplets[choice.to_i].ip_address
94
+ end
95
+
96
+ # If we coulnd't find it, tell the user and drop out of the
97
+ # sequence.
98
+ if !env["droplet_id"]
99
+ say "error\nUnable to find a droplet named '#{user_fuzzy_name}'.", :red
100
+ return
101
+ end
102
+ end
103
+
104
+ if !did_run_multiple
105
+ say "done#{CLEAR}, #{env["droplet_id"]} #{env["droplet_name"]}", :green
106
+ end
107
+ @app.call(env)
108
+ end
109
+ end
110
+ end
111
+ end
112
+
@@ -0,0 +1,23 @@
1
+ module Tugboat
2
+ module Middleware
3
+ class HaltDroplet < Base
4
+ def call(env)
5
+ ocean = env["ocean"]
6
+
7
+ say "Queuing shutdown for #{env["droplet_id"]} #{env["droplet_name"]}...", nil, false
8
+
9
+ req = ocean.droplets.shutdown env["droplet_id"]
10
+
11
+ if req.status == "ERROR"
12
+ say req.error_message, :red
13
+ return
14
+ end
15
+
16
+ say "done", :green
17
+
18
+ @app.call(env)
19
+ end
20
+ end
21
+ end
22
+ end
23
+
@@ -0,0 +1,37 @@
1
+ module Tugboat
2
+ module Middleware
3
+ class InfoDroplet < Base
4
+ def call(env)
5
+ ocean = env["ocean"]
6
+
7
+ req = ocean.droplets.show env["droplet_id"]
8
+
9
+ if req.status == "ERROR"
10
+ say "#{req.status}: #{req.error_message}", :red
11
+ return
12
+ end
13
+
14
+ droplet = req.droplet
15
+
16
+ if droplet.status == "active"
17
+ status_color = GREEN
18
+ else
19
+ status_color = RED
20
+ end
21
+
22
+ say
23
+ say "Name: #{droplet.name}"
24
+ say "ID: #{droplet.id}"
25
+ say "Status: #{status_color}#{droplet.status}#{CLEAR}"
26
+ say "IP: #{droplet.ip_address}"
27
+ say "Region ID: #{droplet.region_id}"
28
+ say "Image ID: #{droplet.image_id}"
29
+ say "Size ID: #{droplet.size_id}"
30
+ say "Backups Active: #{droplet.backups_active || false}"
31
+
32
+ @app.call(env)
33
+ end
34
+ end
35
+ end
36
+ end
37
+
@@ -0,0 +1,19 @@
1
+ require 'digital_ocean'
2
+ module Tugboat
3
+ module Middleware
4
+ # Inject the digital ocean client into the environment
5
+ class InjectClient < Base
6
+ def call(env)
7
+ # Sets the digital ocean client into the environment for use
8
+ # later.
9
+ env["ocean"] = DigitalOcean::API.new \
10
+ :client_id => env["config"].client_key,
11
+ :api_key => env["config"].api_key,
12
+ :debug => ENV['DEBUG'] || false
13
+
14
+ @app.call(env)
15
+ end
16
+ end
17
+ end
18
+ end
19
+
@@ -0,0 +1,16 @@
1
+ module Tugboat
2
+ module Middleware
3
+ # Check if the client has set-up configuration yet.
4
+ class InjectConfiguration < Base
5
+ def call(env)
6
+ config = Tugboat::Configuration.instance
7
+ config.reset!
8
+
9
+ env["config"] = config
10
+
11
+ @app.call(env)
12
+ end
13
+ end
14
+ end
15
+ end
16
+
@@ -0,0 +1,24 @@
1
+ module Tugboat
2
+ module Middleware
3
+ # Check if the client has set-up configuration yet.
4
+ class ListDroplets < Base
5
+ def call(env)
6
+ ocean = env["ocean"]
7
+
8
+ ocean.droplets.list.droplets.each do |droplet|
9
+
10
+ if droplet.status == "active"
11
+ status_color = GREEN
12
+ else
13
+ status_color = RED
14
+ end
15
+
16
+ say "#{droplet.name} (ip: #{droplet.ip_address}, status: #{status_color}#{droplet.status}#{CLEAR}, region: #{droplet.region_id}, id: #{droplet.id})"
17
+ end
18
+
19
+ @app.call(env)
20
+ end
21
+ end
22
+ end
23
+ end
24
+
@@ -0,0 +1,29 @@
1
+ module Tugboat
2
+ module Middleware
3
+ class ListImages < Base
4
+ def call(env)
5
+ ocean = env["ocean"]
6
+ my_images = ocean.images.list :filter => "my_images"
7
+ if env["user_show_global_images"]
8
+ global = ocean.images.list :filter => "global"
9
+ end
10
+
11
+ say "My Images:"
12
+ my_images.images.each do |image|
13
+ say "#{image.name} (id: #{image.id}, distro: #{image.distribution})"
14
+ end
15
+
16
+ if env["user_show_global_images"]
17
+ say
18
+ say "Global Images:"
19
+ global.images.each do |image|
20
+ say "#{image.name} (id: #{image.id}, distro: #{image.distribution})"
21
+ end
22
+ end
23
+
24
+ @app.call(env)
25
+ end
26
+ end
27
+ end
28
+ end
29
+
@@ -0,0 +1,23 @@
1
+ module Tugboat
2
+ module Middleware
3
+ class RestartDroplet < Base
4
+ def call(env)
5
+ ocean = env["ocean"]
6
+
7
+ say "Queuing restart for #{env["droplet_id"]} #{env["droplet_name"]}...", nil, false
8
+
9
+ req = ocean.droplets.reboot env["droplet_id"]
10
+
11
+ if req.status == "ERROR"
12
+ say "#{req.status}: #{req.error_message}", :red
13
+ return
14
+ end
15
+
16
+ say "done", :green
17
+
18
+ @app.call(env)
19
+ end
20
+ end
21
+ end
22
+ end
23
+
@@ -0,0 +1,25 @@
1
+ module Tugboat
2
+ module Middleware
3
+ class SnapshotDroplet < Base
4
+ def call(env)
5
+ ocean = env["ocean"]
6
+
7
+ say "Queuing snapshot '#{env["user_snapshot_name"]}' for #{env["droplet_id"]} #{env["droplet_name"]}...", nil, false
8
+
9
+ # Temporary
10
+ req = ocean.droplets.snapshot env["droplet_id"],
11
+ :name => env["user_snapshot_name"]
12
+
13
+ if req.status == "ERROR"
14
+ say "#{req.status}: #{req.error_message}", :red
15
+ return
16
+ end
17
+
18
+ say "done", :green
19
+
20
+ @app.call(env)
21
+ end
22
+ end
23
+ end
24
+ end
25
+
@@ -0,0 +1,26 @@
1
+ module Tugboat
2
+ module Middleware
3
+ class SSHDroplet < Base
4
+ def call(env)
5
+ say "Executing SSH #{env["droplet_name"]}..."
6
+
7
+ options = [
8
+ "-o", "IdentitiesOnly=yes",
9
+ "-o", "LogLevel=ERROR",
10
+ "-o", "StrictHostKeyChecking=no",
11
+ "-o", "UserKnownHostsFile=/dev/null",
12
+ "-i", env["config"].ssh_key_path.to_s]
13
+
14
+ host_string = "#{env["config"].ssh_user}@#{env["droplet_ip"]}"
15
+
16
+ options << host_string
17
+
18
+
19
+ Kernel.exec("ssh", *options)
20
+
21
+ @app.call(env)
22
+ end
23
+ end
24
+ end
25
+ end
26
+
@@ -0,0 +1,132 @@
1
+ require "middleware"
2
+
3
+ module Tugboat
4
+ module Middleware
5
+ autoload :Base, "tugboat/middleware/base"
6
+ autoload :InjectConfiguration, "tugboat/middleware/inject_configuration"
7
+ autoload :CheckConfiguration, "tugboat/middleware/check_configuration"
8
+ autoload :AskForCredentials, "tugboat/middleware/ask_for_credentials"
9
+ autoload :InjectClient, "tugboat/middleware/inject_client"
10
+ autoload :CheckCredentials, "tugboat/middleware/check_credentials"
11
+ autoload :ListDroplets, "tugboat/middleware/list_droplets"
12
+ autoload :FindDroplet, "tugboat/middleware/find_droplet"
13
+ autoload :RestartDroplet, "tugboat/middleware/restart_droplet"
14
+ autoload :HaltDroplet, "tugboat/middleware/halt_droplet"
15
+ autoload :InfoDroplet, "tugboat/middleware/info_droplet"
16
+ autoload :SSHDroplet, "tugboat/middleware/ssh_droplet"
17
+ autoload :CreateDroplet, "tugboat/middleware/create_droplet"
18
+ autoload :DestroyDroplet, "tugboat/middleware/destroy_droplet"
19
+ autoload :ConfirmAction, "tugboat/middleware/confirm_action"
20
+ autoload :SnapshotDroplet, "tugboat/middleware/snapshot_droplet"
21
+ autoload :ListImages, "tugboat/middleware/list_images"
22
+
23
+ # This takes the user through the authorization flow
24
+ def self.sequence_authorize
25
+ ::Middleware::Builder.new do
26
+ use InjectConfiguration
27
+ use AskForCredentials
28
+ use InjectConfiguration
29
+ use CheckConfiguration
30
+ use InjectClient
31
+ use CheckCredentials
32
+ end
33
+ end
34
+
35
+ # This provides a list of droplets
36
+ def self.sequence_list_droplets
37
+ ::Middleware::Builder.new do
38
+ use InjectConfiguration
39
+ use CheckConfiguration
40
+ use InjectClient
41
+ use ListDroplets
42
+ end
43
+ end
44
+
45
+ # This provides a list of droplets
46
+ def self.sequence_list_images
47
+ ::Middleware::Builder.new do
48
+ use InjectConfiguration
49
+ use CheckConfiguration
50
+ use InjectClient
51
+ use ListImages
52
+ end
53
+ end
54
+
55
+ # This restarts a droplet
56
+ def self.sequence_restart_droplet
57
+ ::Middleware::Builder.new do
58
+ use InjectConfiguration
59
+ use CheckConfiguration
60
+ use InjectClient
61
+ use FindDroplet
62
+ use RestartDroplet
63
+ end
64
+ end
65
+
66
+ # This halts a droplet
67
+ def self.sequence_halt_droplet
68
+ ::Middleware::Builder.new do
69
+ use InjectConfiguration
70
+ use CheckConfiguration
71
+ use InjectClient
72
+ use FindDroplet
73
+ use HaltDroplet
74
+ end
75
+ end
76
+
77
+ # This shows a droplet
78
+ def self.sequence_info_droplet
79
+ ::Middleware::Builder.new do
80
+ use InjectConfiguration
81
+ use CheckConfiguration
82
+ use InjectClient
83
+ use FindDroplet
84
+ use InfoDroplet
85
+ end
86
+ end
87
+
88
+ # SSH Into a droplet
89
+ def self.sequence_ssh_droplet
90
+ ::Middleware::Builder.new do
91
+ use InjectConfiguration
92
+ use CheckConfiguration
93
+ use InjectClient
94
+ use FindDroplet
95
+ use SSHDroplet
96
+ end
97
+ end
98
+
99
+ # SSH Into a droplet
100
+ def self.sequence_create_droplet
101
+ ::Middleware::Builder.new do
102
+ use InjectConfiguration
103
+ use CheckConfiguration
104
+ use InjectClient
105
+ use CreateDroplet
106
+ end
107
+ end
108
+
109
+ # SSH Into a droplet
110
+ def self.sequence_destroy_droplet
111
+ ::Middleware::Builder.new do
112
+ use InjectConfiguration
113
+ use CheckConfiguration
114
+ use InjectClient
115
+ use FindDroplet
116
+ use ConfirmAction
117
+ use DestroyDroplet
118
+ end
119
+ end
120
+
121
+ # SSH Into a droplet
122
+ def self.sequence_snapshot_droplet
123
+ ::Middleware::Builder.new do
124
+ use InjectConfiguration
125
+ use CheckConfiguration
126
+ use InjectClient
127
+ use FindDroplet
128
+ use SnapshotDroplet
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,3 @@
1
+ module Tugboat
2
+ VERSION = "0.0.1"
3
+ end
data/lib/tugboat.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'tugboat/cli'
2
+ require "tugboat/config"
3
+ require "tugboat/version"
4
+
5
+ module Tugboat
6
+ end
@@ -0,0 +1,90 @@
1
+ require 'spec_helper'
2
+
3
+ describe Tugboat::Configuration do
4
+ let(:tmp_path) { project_path + "/tmp/tugboat" }
5
+
6
+ after :each do
7
+ # Clean up the temp file.
8
+ File.delete(project_path + "/tmp/tugboat") if File.exist?(project_path + "/tmp/tugboat")
9
+ end
10
+
11
+ it "is a singleton" do
12
+ expect(Tugboat::Configuration).to be_a Class
13
+ expect do
14
+ Tugboat::Configuration.new
15
+ end.to raise_error(NoMethodError, /private method `new' called/)
16
+ end
17
+
18
+ it "has a data attribute" do
19
+ config = Tugboat::Configuration.instance
20
+ expect(config.data).to be
21
+ end
22
+
23
+ it "can set a path" do
24
+ config = Tugboat::Configuration.instance
25
+ config.path = tmp_path
26
+ expect(config.path).to equal(tmp_path)
27
+ end
28
+
29
+ it "properly resets path" do
30
+ config = Tugboat::Configuration.instance
31
+ config.path = tmp_path
32
+ config.reset!
33
+ expect(config.path).to eq(File.join(File.expand_path("~"), '.tugboat'))
34
+ end
35
+
36
+ describe "the file" do
37
+ let(:client_key) { "foo" }
38
+ let(:api_key) { "bar" }
39
+ let(:ssh_user) { "baz" }
40
+ let(:ssh_key_path) { "~/.ssh/id_rsa2.pub" }
41
+
42
+ let(:config) { config = Tugboat::Configuration.instance }
43
+
44
+ before :each do
45
+ # Create a temporary file
46
+ config.path = tmp_path
47
+ config.create_config_file(client_key, api_key, ssh_key_path, ssh_user)
48
+ end
49
+
50
+ it "can be created" do
51
+ expect(File.exist?(tmp_path)).to be_true
52
+ end
53
+
54
+ it "can be loaded" do
55
+ data = config.load_config_file
56
+ expect(data).to_not be_nil
57
+ end
58
+
59
+ describe "the file format"
60
+ let(:data) { YAML.load_file(tmp_path) }
61
+
62
+ it "should have authentication at the top level" do
63
+ expect(data).to have_key("authentication")
64
+ end
65
+
66
+ it "should have ssh at the top level" do
67
+ expect(data).to have_key("ssh")
68
+ end
69
+
70
+ it "should have a client key" do
71
+ auth = data["authentication"]
72
+ expect(auth).to have_key("client_key")
73
+ end
74
+
75
+ it "should have an api key" do
76
+ auth = data["authentication"]
77
+ expect(auth).to have_key("api_key")
78
+ end
79
+
80
+ it "should have an ssh key path" do
81
+ ssh = data["ssh"]
82
+ expect(ssh).to have_key("ssh_key_path")
83
+ end
84
+
85
+ it "should have an ssh user" do
86
+ ssh = data["ssh"]
87
+ expect(ssh).to have_key("ssh_user")
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,11 @@
1
+ require 'tugboat'
2
+
3
+ RSpec.configure do |config|
4
+ # Pretty tests
5
+ config.color_enabled = true
6
+ end
7
+
8
+ def project_path
9
+ File.expand_path("../..", __FILE__)
10
+ end
11
+
data/tugboat.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'tugboat/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "tugboat"
8
+ gem.version = Tugboat::VERSION
9
+ gem.authors = ["Jack Pearkes"]
10
+ gem.email = ["jackpearkes@gmail.com"]
11
+ gem.description = %q{A command line tool for interacting with your DigitalOcean droplets.}
12
+ gem.summary = %q{A command line tool for interacting with your DigitalOcean droplets.}
13
+ gem.homepage = "https://github.com/pearkes/tugboat"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency "thor", "~> 0.18.1"
21
+ gem.add_dependency "digital_ocean", "~> 1.0.1"
22
+ gem.add_dependency "middleware" , "~> 0.1.0"
23
+ gem.add_dependency "activesupport"
24
+ end