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.
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ *.gem
2
+ *.rbc
3
+ *~
4
+ *.swp
5
+ *.swo
6
+ .bundle
7
+ .rvmrc
8
+ Gemfile.lock
9
+ doc/*
10
+ log/*
11
+ pkg/*
12
+ tmp/*
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,7 @@
1
+ # Contributing
2
+
3
+ 1. Fork it
4
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
5
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
6
+ 4. Push to the branch (`git push origin my-new-feature`)
7
+ 5. Create new Pull Request
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in tugboat.gemspec
4
+ gemspec
data/LICENSE.md ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Jack Pearkes
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,122 @@
1
+ # Tugboat
2
+
3
+ A command line tool for interacting with your [DigitalOcean](https://www.digitalocean.com/) droplets.
4
+
5
+ ## Installation
6
+
7
+ gem install tugboat
8
+
9
+ ## Configuration
10
+
11
+ Run the configuration utility, `tugboat authorize`. You can grab your keys
12
+ [here](https://www.digitalocean.com/api_access).
13
+
14
+ $ tugboat authorize
15
+ Enter your client key: foo
16
+ Enter your API key: bar
17
+ Enter your SSH key path (optional, defaults to ~/.ssh/id_rsa):
18
+ Enter your SSH user (optional, defaults to jack):
19
+ Authentication with DigitalOcean was successful!
20
+
21
+ ## Usage
22
+
23
+ ### Retrieve a list of your droplets
24
+
25
+ $ tugboat droplets
26
+ pearkes-web-001 (ip: 30.30.30.1, status: active, region: 1, id: 13231511)
27
+ pearkes-admin-001 (ip: 30.30.30.3, status: active, region: 1, id: 13231512)
28
+ pearkes-api-001 (ip: 30.30.30.5, status: active, region: 1, id: 13231513)
29
+
30
+ ### Fuzzy name matching
31
+
32
+ You can pass a unique fragment of a droplets name for interactions
33
+ throughout `tugboat`.
34
+
35
+ $ tugboat restart admin
36
+ Droplet fuzzy name provided. Finding droplet ID...done, 13231512 (pearkes-admin-001)
37
+ Queuing restart for 13231512 (pearkes-admin-001)...done
38
+
39
+ tugboat handles multiple matches as well:
40
+
41
+ $ tugboat restart pearkes
42
+ Droplet fuzzy name provided. Finding droplet ID...Multiple droplets found.
43
+
44
+ 0) pearkes-web-001 (13231511)
45
+ 1) pearkes-admin-001 (13231512)
46
+ 2) pearkes-api-001 (13231513)
47
+
48
+ Please choose a droplet: ["0", "1", "2"] 0
49
+ Queuing restart for 13231511 (pearkes-web-001)...done
50
+
51
+ ### SSH into a droplet
52
+
53
+ You can configure a SSH username and key path in `tugboat authorize`.
54
+
55
+ This lets you ssh into a droplet by providing it's name, or a partial
56
+ match.
57
+
58
+ $ tugboat ssh admin
59
+ Droplet fuzzy name provided. Finding droplet ID...done, 13231512 (pearkes-admin-001)
60
+ Executing SSH (pearkes-admin-001)...
61
+ Welcome to Ubuntu 12.10 (GNU/Linux 3.5.0-17-generic x86_64)
62
+ pearkes@pearkes-admin-001:~#
63
+
64
+ ### Create a droplet
65
+
66
+ $ tugboat create pearkes-www-002 -s 64 -i 2676 -r 2
67
+ Queueing creation of droplet 'pearkes-www-002'...done
68
+
69
+ ### Info about a droplet
70
+
71
+ $ tugboat info admin
72
+ Droplet fuzzy name provided. Finding droplet ID...done, 13231512 (pearkes-admin-001)
73
+
74
+ Name: pearkes-admin-001
75
+ ID: 13231512
76
+ Status: active
77
+ IP: 30.30.30.3
78
+ Region ID: 1
79
+ Image ID: 25489
80
+ Size ID: 66
81
+ Backups Active: false
82
+
83
+ ### Destroy a droplet
84
+
85
+ $ tugboat destroy pearkes-www-002
86
+ Droplet fuzzy name provided. Finding droplet ID...done, 13231515 (pearkes-www-002)
87
+ Warning! Potentially destructive action. Please confirm [y/n]: y
88
+ Queuing destroy for 13231515 (pearkes-www-002)...done
89
+
90
+ ### Restart a droplet
91
+
92
+ $ tugboat restart admin
93
+ Droplet fuzzy name provided. Finding droplet ID...done, 13231512 (pearkes-admin-001)
94
+ Queuing restart for 13231512 (pearkes-admin-001)...done
95
+
96
+ ### Shutdown a droplet
97
+
98
+ $ tugboat halt admin
99
+ Droplet fuzzy name provided. Finding droplet ID...done, 13231512 (pearkes-admin-001)
100
+ Queuing shutdown for 13231512 (pearkes-admin-001)...done
101
+
102
+ ### Snapshot a droplet
103
+
104
+ $ tugboat snapshot admin test-admin-snaphot
105
+ Queuing snapshot 'test' for 13231512 (pearkes-admin-001)...done
106
+
107
+ ## Help
108
+
109
+ If you're curious about command flags for a specific command, you can
110
+ ask tugboat about it.
111
+
112
+ $ tugboat help restart
113
+
114
+
115
+ For a complete overview of all of the available commands, run:
116
+
117
+ $ tugboat help
118
+
119
+ ## Contributing
120
+
121
+ See the [contributing guide](CONTRIBUTING.md).
122
+
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/tugboat ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require "tugboat"
3
+
4
+ Tugboat::CLI.start(ARGV)
@@ -0,0 +1,185 @@
1
+ require 'thor'
2
+
3
+ module Tugboat
4
+ autoload :Middleware, "tugboat/middleware"
5
+
6
+ class CLI < Thor
7
+ include Thor::Actions
8
+ ENV['THOR_COLUMNS'] = '120'
9
+
10
+ !check_unknown_options
11
+
12
+ desc "help [COMMAND]", "Describe commands or a specific command"
13
+ def help(meth=nil)
14
+ super
15
+ if !meth
16
+ say "To learn more or to contribute, please see github.com/pearkes/tugboat"
17
+ end
18
+ end
19
+
20
+ desc "authorize", "Authorize a DigitalOcean account with tugboat"
21
+ long_desc "This takes you through a workflow for adding configuration
22
+ details to tugboat. First, you are asked for your API and Client keys,
23
+ which are stored in ~/.tugboat.
24
+
25
+ You can retrieve your credentials from digitalocean.com/api_access.
26
+
27
+ Optionally, you can configure the default SSH key path and username
28
+ used for `tugboat ssh`. These default to '~/.ssh/id_rsa' and the
29
+ $USER environment variable.
30
+ "
31
+ def authorize
32
+ Middleware.sequence_authorize.call({})
33
+ end
34
+
35
+ desc "droplets", "Retrieve a list of your droplets"
36
+ def droplets
37
+ Middleware.sequence_list_droplets.call({})
38
+ end
39
+
40
+ desc "images", "Retrieve a list of your images"
41
+ method_option "global",
42
+ :type => :boolean,
43
+ :default => false,
44
+ :aliases => "-g",
45
+ :desc => "Show global images"
46
+ def images
47
+ Middleware.sequence_list_images.call({
48
+ "user_show_global_images" => options[:global],
49
+ })
50
+ end
51
+
52
+ desc "ssh FUZZY_NAME", "SSH into a droplet"
53
+ method_option "id",
54
+ :type => :string,
55
+ :aliases => "-i",
56
+ :desc => "The ID of the droplet."
57
+ method_option "name",
58
+ :type => :string,
59
+ :aliases => "-n",
60
+ :desc => "The exact name of the droplet"
61
+ def ssh(name=nil)
62
+ Middleware.sequence_ssh_droplet.call({
63
+ "user_droplet_id" => options[:id],
64
+ "user_droplet_name" => options[:name],
65
+ "user_droplet_fuzzy_name" => name
66
+ })
67
+ end
68
+
69
+ desc "create NAME", "Create a droplet."
70
+ method_option "size",
71
+ :type => :numeric,
72
+ :aliases => "-s",
73
+ :default => 64,
74
+ :desc => "The size_id of the droplet"
75
+ method_option "image",
76
+ :type => :numeric,
77
+ :aliases => "-i",
78
+ :default => 2676,
79
+ :desc => "The image_id of the droplet"
80
+ method_option "region",
81
+ :type => :numeric,
82
+ :aliases => "-r",
83
+ :default => 1,
84
+ :desc => "The region_id of the droplet"
85
+ def create(name)
86
+ Middleware.sequence_create_droplet.call({
87
+ "create_droplet_size_id" => options[:size],
88
+ "create_droplet_image_id" => options[:image],
89
+ "create_droplet_region_id" => options[:region],
90
+ "create_droplet_name" => name
91
+ })
92
+ end
93
+
94
+ desc "destroy FUZZY_NAME", "Destroy a droplet"
95
+ method_option "id",
96
+ :type => :string,
97
+ :aliases => "-i",
98
+ :desc => "The ID of the droplet."
99
+ method_option "name",
100
+ :type => :string,
101
+ :aliases => "-n",
102
+ :desc => "The exact name of the droplet"
103
+ def destroy(name=nil)
104
+ Middleware.sequence_destroy_droplet.call({
105
+ "user_droplet_id" => options[:id],
106
+ "user_droplet_name" => options[:name],
107
+ "user_droplet_fuzzy_name" => name
108
+ })
109
+ end
110
+
111
+ desc "restart FUZZY_NAME", "Restart a droplet"
112
+ method_option "id",
113
+ :type => :string,
114
+ :aliases => "-i",
115
+ :desc => "The ID of the droplet."
116
+ method_option "name",
117
+ :type => :string,
118
+ :aliases => "-n",
119
+ :desc => "The exact name of the droplet"
120
+ def restart(name=nil)
121
+ Middleware.sequence_restart_droplet.call({
122
+ "user_droplet_id" => options[:id],
123
+ "user_droplet_name" => options[:name],
124
+ "user_droplet_fuzzy_name" => name
125
+ })
126
+ end
127
+
128
+ desc "halt FUZZY_NAME", "Shutdown a droplet"
129
+ method_option "id",
130
+ :type => :string,
131
+ :aliases => "-i",
132
+ :desc => "The ID of the droplet."
133
+ method_option "name",
134
+ :type => :string,
135
+ :aliases => "-n",
136
+ :desc => "The exact name of the droplet"
137
+ def halt(name=nil)
138
+ Middleware.sequence_halt_droplet.call({
139
+ "user_droplet_id" => options[:id],
140
+ "user_droplet_name" => options[:name],
141
+ "user_droplet_fuzzy_name" => name
142
+ })
143
+ end
144
+
145
+ desc "info FUZZY_NAME [OPTIONS]", "Show a droplet's information"
146
+ method_option "id",
147
+ :type => :string,
148
+ :aliases => "-i",
149
+ :desc => "The ID of the droplet."
150
+ method_option "name",
151
+ :type => :string,
152
+ :aliases => "-n",
153
+ :desc => "The exact name of the droplet"
154
+ def info(name=nil)
155
+ Middleware.sequence_info_droplet.call({
156
+ "user_droplet_id" => options[:id],
157
+ "user_droplet_name" => options[:name],
158
+ "user_droplet_fuzzy_name" => name
159
+ })
160
+ end
161
+
162
+ desc "snapshot FUZZY_NAME [OPTIONS]", "Queue a snapshot of the droplet."
163
+ method_option "id",
164
+ :type => :string,
165
+ :aliases => "-i",
166
+ :desc => "The ID of the droplet."
167
+ method_option "name",
168
+ :type => :string,
169
+ :aliases => "-n",
170
+ :desc => "The exact name of the droplet"
171
+ method_option "snapshot",
172
+ :type => :string,
173
+ :aliases => "-s",
174
+ :desc => "The name of the snapshot"
175
+ def snapshot(name=nil, snapshot_name)
176
+ Middleware.sequence_snapshot_droplet.call({
177
+ "user_droplet_id" => options[:id],
178
+ "user_droplet_name" => options[:name],
179
+ "user_droplet_fuzzy_name" => name,
180
+ "user_snapshot_name" => snapshot_name
181
+ })
182
+ end
183
+ end
184
+ end
185
+
@@ -0,0 +1,76 @@
1
+ require 'singleton'
2
+
3
+ module Tugboat
4
+ # This is the configuration object. It reads in configuration
5
+ # from a .tugboat file located in the user's home directory
6
+
7
+ class Configuration
8
+ include Singleton
9
+ attr_reader :data
10
+ attr_reader :path
11
+
12
+ FILE_NAME = '.tugboat'
13
+ DEFAULT_SSH_KEY_PATH = '.ssh/id_rsa'
14
+
15
+ def initialize
16
+ @path = File.join(File.expand_path("~"), FILE_NAME)
17
+ @data = self.load_config_file
18
+ end
19
+
20
+ # If we can't load the config file, self.data is nil, which we can
21
+ # check for in CheckConfiguration
22
+ def load_config_file
23
+ require 'yaml'
24
+ YAML.load_file(@path)
25
+ rescue Errno::ENOENT
26
+ return
27
+ end
28
+
29
+ def client_key
30
+ @data['authentication']['client_key']
31
+ end
32
+
33
+ def api_key
34
+ @data['authentication']['api_key']
35
+ end
36
+
37
+ def ssh_key_path
38
+ @data['ssh']['ssh_key_path']
39
+ end
40
+
41
+ def ssh_user
42
+ @data['ssh']['ssh_user']
43
+ end
44
+
45
+ # Allow the path to be set.
46
+ def path=(path)
47
+ @path = path
48
+ path
49
+ end
50
+
51
+ # Re-runs initialize
52
+ def reset!
53
+ self.send(:initialize)
54
+ end
55
+
56
+ # Writes a config file
57
+ def create_config_file(client, api, ssh_key_path, ssh_user)
58
+ # Default SSH Key path
59
+ if ssh_key_path.empty?
60
+ ssh_key_path = File.join(File.expand_path("~"), DEFAULT_SSH_KEY_PATH)
61
+ end
62
+
63
+ if ssh_user.empty?
64
+ ssh_user = ENV['USER']
65
+ end
66
+
67
+ require 'yaml'
68
+ File.open(@path, File::RDWR|File::TRUNC|File::CREAT, 0600) do |file|
69
+ data = {"authentication" => { "client_key" => client, "api_key" => api },
70
+ "ssh" => { "ssh_user" => ssh_user, "ssh_key_path" => ssh_key_path }}
71
+ file.write data.to_yaml
72
+ end
73
+ end
74
+
75
+ end
76
+ end
@@ -0,0 +1,21 @@
1
+ module Tugboat
2
+ module Middleware
3
+ # Ask for user credentials from the command line, then write them out.
4
+ class AskForCredentials < Base
5
+ def call(env)
6
+ say "Note: You can get this information from digitalocean.com/api_access", :yellow
7
+ say
8
+ client_key = ask "Enter your client key:"
9
+ api_key = ask "Enter your API key:"
10
+ ssh_key_path = ask "Enter your SSH key path (optional, defaults to ~/.ssh/id_rsa):"
11
+ ssh_user = ask "Enter your SSH user (optional, defaults to #{ENV['USER']}):"
12
+
13
+ # Write the config file.
14
+ env['config'].create_config_file(client_key, api_key, ssh_key_path, ssh_user)
15
+
16
+ @app.call(env)
17
+ end
18
+ end
19
+ end
20
+ end
21
+
@@ -0,0 +1,28 @@
1
+ module Tugboat
2
+ module Middleware
3
+ # A base middleware class to initalize.
4
+ class Base
5
+ # Some colors for making things pretty.
6
+ CLEAR = "\e[0m"
7
+ RED = "\e[31m"
8
+ GREEN = "\e[32m"
9
+ YELLOW = "\e[33m"
10
+
11
+ # We want access to all of the fun thor cli helper methods,
12
+ # like say, yes?, ask, etc.
13
+ include Thor::Shell
14
+
15
+ def initialize(app)
16
+ @app = app
17
+ # This resets the color to "clear" on the user's terminal.
18
+ say "", :clear, false
19
+ end
20
+
21
+ def call(env)
22
+ @app.call(env)
23
+ end
24
+
25
+ end
26
+ end
27
+ end
28
+
@@ -0,0 +1,18 @@
1
+ module Tugboat
2
+ module Middleware
3
+ # Check if the client has set-up configuration yet.
4
+ class CheckConfiguration < Base
5
+ def call(env)
6
+ config = env["config"]
7
+
8
+ if !config || !config.data || !config.api_key || !config.client_key
9
+ say "You must run `tugboat authorize` in order to connect to DigitalOcean", :red
10
+ return
11
+ end
12
+
13
+ @app.call(env)
14
+ end
15
+ end
16
+ end
17
+ end
18
+
@@ -0,0 +1,24 @@
1
+ require 'thor'
2
+
3
+ module Tugboat
4
+ module Middleware
5
+ # Check if the client can connect to the ocean
6
+ class CheckCredentials < Base
7
+ def call(env)
8
+ # We use a harmless API call to check if the authentication will
9
+ # work.
10
+ begin
11
+ env["ocean"].droplets.list
12
+ rescue
13
+ say "Authentication with DigitalOcean failed. Run `tugboat authorize`", :red
14
+ return
15
+ end
16
+
17
+ say "Authentication with DigitalOcean was successful.", :green
18
+
19
+ @app.call(env)
20
+ end
21
+ end
22
+ end
23
+ end
24
+
@@ -0,0 +1,18 @@
1
+ module Tugboat
2
+ module Middleware
3
+ class ConfirmAction < Base
4
+ def call(env)
5
+ response = yes? "Warning! Potentially destructive action. Please confirm [y/n]:"
6
+
7
+ if !response
8
+ say "Aborted due to user request.", :red
9
+ # Quit
10
+ return
11
+ end
12
+
13
+ @app.call(env)
14
+ end
15
+ end
16
+ end
17
+ end
18
+
@@ -0,0 +1,26 @@
1
+ module Tugboat
2
+ module Middleware
3
+ class CreateDroplet < Base
4
+ def call(env)
5
+ ocean = env["ocean"]
6
+
7
+ say "Queueing creation of droplet '#{env["create_droplet_name"]}'...", nil, false
8
+
9
+ req = ocean.droplets.create :name => env["create_droplet_name"],
10
+ :size_id => env["create_droplet_size_id"],
11
+ :image_id => env["create_droplet_image_id"],
12
+ :region_id => env["create_droplet_region_id"]
13
+
14
+ if req.status == "ERROR"
15
+ say req.error_message, :red
16
+ return
17
+ end
18
+
19
+ say "done", :green
20
+
21
+ @app.call(env)
22
+ end
23
+ end
24
+ end
25
+ end
26
+
@@ -0,0 +1,23 @@
1
+ module Tugboat
2
+ module Middleware
3
+ class DestroyDroplet < Base
4
+ def call(env)
5
+ ocean = env["ocean"]
6
+
7
+ say "Queuing destroy for #{env["droplet_id"]} #{env["droplet_name"]}...", nil, false
8
+
9
+ req = ocean.droplets.delete 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
+