tugboat 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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