tugboat 0.0.1

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