rudy 0.2
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/CHANGES.txt +21 -0
- data/LICENSE.txt +19 -0
- data/README.rdoc +32 -0
- data/Rakefile +68 -0
- data/bin/rudy +258 -0
- data/lib/drydock.rb +525 -0
- data/lib/rudy.rb +102 -0
- data/lib/rudy/aws.rb +65 -0
- data/lib/rudy/aws/ec2.rb +197 -0
- data/lib/rudy/aws/s3.rb +3 -0
- data/lib/rudy/aws/simpledb.rb +48 -0
- data/lib/rudy/command/addresses.rb +41 -0
- data/lib/rudy/command/base.rb +275 -0
- data/lib/rudy/command/commit.rb +10 -0
- data/lib/rudy/command/disks.rb +61 -0
- data/lib/rudy/command/environment.rb +95 -0
- data/lib/rudy/command/groups.rb +59 -0
- data/lib/rudy/command/images.rb +61 -0
- data/lib/rudy/command/instances.rb +109 -0
- data/lib/rudy/command/metadata.rb +57 -0
- data/lib/rudy/command/release.rb +43 -0
- data/lib/rudy/command/volumes.rb +13 -0
- data/lib/rudy/metadata/disk.rb +142 -0
- data/lib/rudy/metadata/environment.rb +0 -0
- data/lib/rudy/scm/svn.rb +57 -0
- data/lib/rudy/utils.rb +65 -0
- data/lib/storable.rb +268 -0
- data/rudy.gemspec +52 -0
- data/support/rudy-ec2-startup +166 -0
- metadata +87 -0
data/CHANGES.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
RUDY, CHANGES
|
2
|
+
|
3
|
+
#### 0.2 (2009-02-23) ###############################
|
4
|
+
|
5
|
+
NOTE: This is a complete re-write from 0.1
|
6
|
+
|
7
|
+
* UPGRADE: drydock 0.3.3
|
8
|
+
* NEW: Commands: myaddress, addresses, images, instances, disks, connect release
|
9
|
+
* NEW: Metadata storage to SimpleDB
|
10
|
+
* NEW: Creates EBS volumes based on startup from metadata
|
11
|
+
* NEW: Automated release process
|
12
|
+
* NEW: Automated creation of machine images
|
13
|
+
* NEW: Partial support for regions and zones
|
14
|
+
* NEW: Manage system based on security groups.
|
15
|
+
* NEW: "rudy groups" overhaul. Display, creates, destroys groups.
|
16
|
+
* CHANGE: Added Environment variables
|
17
|
+
|
18
|
+
|
19
|
+
#### 0.1 (2009-02-06) ###############################
|
20
|
+
|
21
|
+
* Initial release
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2009 Solutious Inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
= Rudy - v0.2 BETA!
|
2
|
+
|
3
|
+
Rudy is a handy staging and deployment tool for EC2.
|
4
|
+
|
5
|
+
NOTE: Rudy is not ready for general consumption. Come back in Q2 2009!
|
6
|
+
|
7
|
+
== Installation
|
8
|
+
|
9
|
+
# Soon!
|
10
|
+
|
11
|
+
== More Info
|
12
|
+
|
13
|
+
* GitHub[http://github.com/solutious/rudy]
|
14
|
+
* RubyForge[http://rubyforge.org/projects/rudy]
|
15
|
+
* Inspiration[http://www.youtube.com/watch?v=CgaiIW5Rzes]
|
16
|
+
|
17
|
+
|
18
|
+
== Credits
|
19
|
+
|
20
|
+
* Delano Mandelbaum (delano@solutious.com)
|
21
|
+
* Keshia Knight Pulliam (rudy@solutious.com)
|
22
|
+
|
23
|
+
|
24
|
+
== Thanks
|
25
|
+
|
26
|
+
* The Rilli[http://rilli.com/] team, for the initial use case and support.
|
27
|
+
* Alicia Eyre Vasconcelos, for the name ("H-e-l-l-o R.U.D.Y")
|
28
|
+
|
29
|
+
|
30
|
+
== License
|
31
|
+
|
32
|
+
See: LICENSE.txt
|
data/Rakefile
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake/clean'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
require 'hanna/rdoctask'
|
5
|
+
require 'fileutils'
|
6
|
+
include FileUtils
|
7
|
+
|
8
|
+
task :default => :package
|
9
|
+
|
10
|
+
# SPECS ===============================================================
|
11
|
+
|
12
|
+
# None-yet!
|
13
|
+
|
14
|
+
# PACKAGE =============================================================
|
15
|
+
|
16
|
+
name = "rudy"
|
17
|
+
load "#{name}.gemspec"
|
18
|
+
|
19
|
+
version = @spec.version
|
20
|
+
|
21
|
+
Rake::GemPackageTask.new(@spec) do |p|
|
22
|
+
p.need_tar = true if RUBY_PLATFORM !~ /mswin/
|
23
|
+
end
|
24
|
+
|
25
|
+
task :release => [ :rdoc, :package ]
|
26
|
+
|
27
|
+
task :install => [ :rdoc, :package ] do
|
28
|
+
sh %{sudo gem install pkg/#{name}-#{version}.gem}
|
29
|
+
end
|
30
|
+
|
31
|
+
task :uninstall => [ :clean ] do
|
32
|
+
sh %{sudo gem uninstall #{name}}
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
# Rubyforge Release / Publish Tasks ==================================
|
37
|
+
|
38
|
+
desc 'Publish website to rubyforge'
|
39
|
+
task 'publish:rdoc' => 'doc/index.html' do
|
40
|
+
sh "scp -rp doc/* rubyforge.org:/var/www/gforge-projects/#{name}/"
|
41
|
+
end
|
42
|
+
|
43
|
+
desc 'Public release to rubyforge'
|
44
|
+
task 'publish:gem' => [:package] do |t|
|
45
|
+
sh <<-end
|
46
|
+
rubyforge add_release -o Any -a CHANGES.txt -f -n README.rdoc #{name} #{name} #{@spec.version} pkg/#{name}-#{@spec.version}.gem &&
|
47
|
+
rubyforge add_file -o Any -a CHANGES.txt -f -n README.rdoc #{name} #{name} #{@spec.version} pkg/#{name}-#{@spec.version}.tgz
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
Rake::RDocTask.new do |t|
|
53
|
+
t.rdoc_dir = 'doc'
|
54
|
+
t.title = @spec.summary
|
55
|
+
t.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
|
56
|
+
t.options << '--charset' << 'utf-8'
|
57
|
+
t.rdoc_files.include('LICENSE.txt')
|
58
|
+
t.rdoc_files.include('README.rdoc')
|
59
|
+
t.rdoc_files.include('CHANGES.txt')
|
60
|
+
t.rdoc_files.include('bin/*')
|
61
|
+
t.rdoc_files.include('lib/*.rb')
|
62
|
+
t.rdoc_files.include('lib/**/*.rb')
|
63
|
+
end
|
64
|
+
|
65
|
+
CLEAN.include [ 'pkg', '*.gem', '.config', 'doc' ]
|
66
|
+
|
67
|
+
|
68
|
+
|
data/bin/rudy
ADDED
@@ -0,0 +1,258 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
# Rudy -- Your friend in staging and deploying to EC2
|
4
|
+
#
|
5
|
+
# See rudy -h for usage
|
6
|
+
#
|
7
|
+
# AWS commands need the access identifiers. If you don't want
|
8
|
+
# to include them as arguments, you can set the following variables
|
9
|
+
# in your environment (replace **** with the literal value):
|
10
|
+
#
|
11
|
+
|
12
|
+
#
|
13
|
+
# No Ruby 1.9.1 support. Only 1.8.x for now :[
|
14
|
+
unless RUBY_VERSION < "1.9"
|
15
|
+
puts "Sorry! We're using the right_aws gem and it doesn't support Ruby 1.9 yet."
|
16
|
+
exit 1
|
17
|
+
end
|
18
|
+
|
19
|
+
RUDY_HOME = File.join(File.dirname(__FILE__))
|
20
|
+
RUDY_LIB = File.join(RUDY_HOME, '..', 'lib')
|
21
|
+
$:.unshift RUDY_LIB # Put our local lib in first place
|
22
|
+
|
23
|
+
require 'rubygems'
|
24
|
+
require 'openssl'
|
25
|
+
|
26
|
+
require 'drydock'
|
27
|
+
require 'rudy'
|
28
|
+
extend Drydock
|
29
|
+
|
30
|
+
debug :off
|
31
|
+
|
32
|
+
default :commands
|
33
|
+
|
34
|
+
|
35
|
+
global_usage "rudy [global options] COMMAND [command options]"
|
36
|
+
global_option :A, :access_key, "AWS Access Key", String
|
37
|
+
global_option :S, :secret_key, "AWS Secret Access Key", String
|
38
|
+
|
39
|
+
global_option :p, :position, String, "Position in the machine in its group (default: 01)"
|
40
|
+
global_option :u, :user, String, "Provide a username (default: rudy)"
|
41
|
+
global_option :e, :environment, String, "Connect to the specified environment (default: stage)"
|
42
|
+
global_option :r, :role, String, "Connect to a machine with the specified role (defalt: app)"
|
43
|
+
|
44
|
+
global_option :V, :version, "Display version number" do
|
45
|
+
puts "Rudy version: #{Rudy::VERSION}"
|
46
|
+
exit 0
|
47
|
+
end
|
48
|
+
global_option :v, :verbose, "Increase verbosity of output (i.e. -v or -vv or -vvv)" do
|
49
|
+
@verbose ||= 0
|
50
|
+
@verbose += 1
|
51
|
+
end
|
52
|
+
|
53
|
+
command :commands do
|
54
|
+
puts "Rudy can do all of these things:", ""
|
55
|
+
command_names.sort.each do |cmd|
|
56
|
+
puts " %16s" % cmd
|
57
|
+
end
|
58
|
+
puts
|
59
|
+
puts "Try: rudy -h OR rudy COMMAND -h"
|
60
|
+
puts
|
61
|
+
end
|
62
|
+
|
63
|
+
usage "rudy init"
|
64
|
+
command :init => Rudy::Command::Metadata do |obj|
|
65
|
+
obj.setup
|
66
|
+
end
|
67
|
+
|
68
|
+
usage "rudy info"
|
69
|
+
command :info => Rudy::Command::Metadata do |obj|
|
70
|
+
obj.info
|
71
|
+
end
|
72
|
+
|
73
|
+
option :D, :destroy, "Destroy all metadata stored in SimpleDB"
|
74
|
+
option :u, :update, "Update the role or environment metadata for the given instance-IDs"
|
75
|
+
usage "rudy [global options] metadata instance-ID"
|
76
|
+
command :metadata => Rudy::Command::Metadata do |obj, argv|
|
77
|
+
obj.print_header
|
78
|
+
|
79
|
+
if obj.update
|
80
|
+
raise "No instance ID!" if argv.empty?
|
81
|
+
raise "Nothing to change (see global options -r or -e)" unless obj.role || obj.environment
|
82
|
+
obj.update_metadata(argv.first)
|
83
|
+
elsif obj.destroy
|
84
|
+
exit unless you_are_sure?
|
85
|
+
obj.destroy_metadata
|
86
|
+
else
|
87
|
+
obj.print_metadata(argv.first)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
option :i, :instance, String, "Instance ID to associate the id"
|
93
|
+
option :A, :associate, "Associate an address to a running instance"
|
94
|
+
usage "rudy [global options] addresses [-A -i instance ID] [address]"
|
95
|
+
command :addresses => Rudy::Command::Addresses do |obj, argv|
|
96
|
+
obj.print_header
|
97
|
+
|
98
|
+
if obj.associate
|
99
|
+
obj.associate_address(argv.first)
|
100
|
+
else
|
101
|
+
obj.print_addresses
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
|
107
|
+
option :p, :print, "Only print the SSH command, don't connect"
|
108
|
+
usage "rudy [-e env] [-u user] connect [-p]"
|
109
|
+
command :connect => Rudy::Command::Environment do |obj|
|
110
|
+
capture(:stderr) do
|
111
|
+
obj.print_header
|
112
|
+
obj.connect
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
|
118
|
+
option :all, "Display all disk definitions"
|
119
|
+
option :p, :path, String, "The filesystem path to use as the mount point"
|
120
|
+
option :d, :device, String, "The device id (default: /dev/sdh)"
|
121
|
+
option :s, :size, Integer, "The size of disk (in GB)"
|
122
|
+
option :C, :create, "Create a disk definition"
|
123
|
+
option :D, :destroy, "Destroy a disk definition"
|
124
|
+
option :A, :attach, "Attach a disk"
|
125
|
+
usage "rudy [global options] disks [-C -p path -d device -s size] [-A] [-D] [disk name]"
|
126
|
+
command :disks => Rudy::Command::Disks do |obj, argv|
|
127
|
+
#capture(:stderr) do
|
128
|
+
obj.print_header
|
129
|
+
if obj.create
|
130
|
+
raise "No filesystem path specified" unless obj.path
|
131
|
+
raise "No size specified" unless obj.size
|
132
|
+
obj.create_disk
|
133
|
+
elsif obj.destroy
|
134
|
+
raise "No disk specified" if argv.empty?
|
135
|
+
exit unless you_are_sure?
|
136
|
+
obj.destroy_disk(argv.first)
|
137
|
+
elsif obj.attach
|
138
|
+
raise "No disk specified" if argv.empty?
|
139
|
+
obj.attach_disk(argv.first)
|
140
|
+
else
|
141
|
+
obj.print_disks
|
142
|
+
end
|
143
|
+
obj.print_footer
|
144
|
+
#end
|
145
|
+
end
|
146
|
+
|
147
|
+
#command :volumes => Rudy::Command::Volumes do |obj|
|
148
|
+
# obj.print_volumes
|
149
|
+
#end
|
150
|
+
|
151
|
+
option :all, "Display all instances"
|
152
|
+
option :a, :address, String, "Amazon elastic IP"
|
153
|
+
option :i, :image, String, "Amazon machine image ID (ami)"
|
154
|
+
option :v, :volume, String, "Amazon volume ID"
|
155
|
+
option :D, :destroy, "Destroy the given instance IDs. All data will be lost!"
|
156
|
+
option :S, :start, "Start an instance"
|
157
|
+
usage "rudy [global options] instances [-D] [-S -i image ID] [instance ID OR group name]"
|
158
|
+
command :instances => Rudy::Command::Instances do |obj, argv|
|
159
|
+
capture(:stderr) do
|
160
|
+
obj.print_header
|
161
|
+
if obj.destroy
|
162
|
+
exit unless you_are_sure?
|
163
|
+
obj.destroy_instances(argv.first)
|
164
|
+
elsif obj.start
|
165
|
+
exit unless you_are_sure?
|
166
|
+
obj.start_instance
|
167
|
+
else
|
168
|
+
obj.print_instances(argv.first)
|
169
|
+
end
|
170
|
+
obj.print_footer
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
|
175
|
+
option :e, :external, "Display only external IP address"
|
176
|
+
option :i, :internal, "Display only internal IP address"
|
177
|
+
usage "rudy myaddress [-i] [-e]"
|
178
|
+
command :myaddress do |obj|
|
179
|
+
ea = Rudy::Utils::external_ip_address
|
180
|
+
ia = Rudy::Utils::internal_ip_address
|
181
|
+
puts "%10s: %s" % ['Internal', ia] unless obj.external && !obj.internal
|
182
|
+
puts "%10s: %s" % ['External', ea] unless obj.internal && !obj.external
|
183
|
+
end
|
184
|
+
|
185
|
+
option :a, :account, String, "Your Amazon Account Number"
|
186
|
+
option :i, :image_name, String, "The name of the image"
|
187
|
+
option :b, :bucket_name, String, "The name of the bucket that will store the image"
|
188
|
+
option :C, :create, "Create an image"
|
189
|
+
usage "rudy images [-C -i image -b bucket -a account]"
|
190
|
+
command :images => Rudy::Command::Images do |obj|
|
191
|
+
if obj.create
|
192
|
+
puts "Make sure the machine is clean. I don't want archive no crud!"
|
193
|
+
exit unless you_are_sure?
|
194
|
+
obj.create_image
|
195
|
+
else
|
196
|
+
obj.print_images
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
command :release => Rudy::Command::Release do |obj|
|
201
|
+
obj.print_header
|
202
|
+
|
203
|
+
raise "No SCM defined. Set RUDY_SVN_BASE or RUDY_GIT_BASE." unless obj.scm
|
204
|
+
|
205
|
+
#exit unless you_are_sure?
|
206
|
+
obj.create_release
|
207
|
+
|
208
|
+
end
|
209
|
+
|
210
|
+
|
211
|
+
option :all, "Display all security groups"
|
212
|
+
option :r, :protocols, Array, "Comma-separated list of protocols. One of: tcp (default), udp, icmp"
|
213
|
+
option :p, :ports, Array, "List of comma-separated ports to authorize (default: 22,80,443)"
|
214
|
+
option :a, :addresses, Array, "List of comma-separated IP addresses to authorize (default: your external IP)"
|
215
|
+
option :C, :create, "Create a security group"
|
216
|
+
option :D, :destroy, "Destroy a security group"
|
217
|
+
option :M, :modify, "Modify a security group"
|
218
|
+
usage "rudy [global options] groups [-C] [-a IP addresses] [-p ports] [group name]"
|
219
|
+
command :groups => Rudy::Command::Groups do |obj, argv|
|
220
|
+
if obj.create
|
221
|
+
obj.create_group(argv.first)
|
222
|
+
|
223
|
+
elsif obj.modify
|
224
|
+
obj.modify_group(argv.first)
|
225
|
+
|
226
|
+
elsif obj.destroy
|
227
|
+
exit unless you_are_sure?
|
228
|
+
obj.destroy_group(argv.first)
|
229
|
+
|
230
|
+
else
|
231
|
+
obj.print_groups(argv.first)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
|
236
|
+
__END__
|
237
|
+
|
238
|
+
# Create the SimpleDB domain for storing metadata. Check environment variables.
|
239
|
+
rudy init
|
240
|
+
rudy create-group -a address -i ami-1111111 -v vol-2222222
|
241
|
+
|
242
|
+
# Connect to stage-app-01 as root
|
243
|
+
rudy --user root connect
|
244
|
+
|
245
|
+
# Zone, environment, role and position are implied.
|
246
|
+
rudy disks -C -p /rilli/app -d /dev/sdh -s 100
|
247
|
+
rudy disks -D disk-us-east-1b-stage-app-01-rilli-db222
|
248
|
+
|
249
|
+
# Create an image from an existing instance
|
250
|
+
rudy -e prod images -C -b rilli-ami-us -i rilli-app-32-r3
|
251
|
+
|
252
|
+
# Create an instance
|
253
|
+
rudy -e prod instances -S -i ami-11111
|
254
|
+
|
255
|
+
|
256
|
+
rudy release
|
257
|
+
|
258
|
+
|
data/lib/drydock.rb
ADDED
@@ -0,0 +1,525 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
#
|
5
|
+
#
|
6
|
+
module Drydock
|
7
|
+
# The base class for all command objects. There is an instance of this class
|
8
|
+
# for every command defined. Global and command-specific options are added
|
9
|
+
# as attributes to this class dynamically.
|
10
|
+
#
|
11
|
+
# i.e. "example -v date -f yaml"
|
12
|
+
#
|
13
|
+
# global_option :v, :verbose, "I want mooooore!"
|
14
|
+
# option :f, :format, String, "Long date format"
|
15
|
+
# command :date do |obj|
|
16
|
+
# puts obj.verbose #=> true
|
17
|
+
# puts obj.format #=> "yaml"
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# You can inherit from this class to create your own: EatFood < Drydock::Command.
|
21
|
+
# And then specific your class in the command definition:
|
22
|
+
#
|
23
|
+
# command :eat => EatFood do |obj|; ...; end
|
24
|
+
#
|
25
|
+
class Command
|
26
|
+
attr_reader :cmd, :alias
|
27
|
+
attr_accessor :verbose
|
28
|
+
|
29
|
+
# The default constructor sets the short name of the command
|
30
|
+
# and stores a reference to the block (if supplied).
|
31
|
+
# You don't need to override this method to add functionality
|
32
|
+
# to your custom Command classes. Define an +init+ method instead.
|
33
|
+
# It will be called just before the block is executed.
|
34
|
+
# +cmd+ is the short name of this command.
|
35
|
+
# +b+ is the block associated to this command.
|
36
|
+
def initialize(cmd, &b)
|
37
|
+
@cmd = (cmd.kind_of?(Symbol)) ? cmd : cmd.to_sym
|
38
|
+
@b = b
|
39
|
+
@verbose = 0
|
40
|
+
end
|
41
|
+
|
42
|
+
# Execute the block.
|
43
|
+
#
|
44
|
+
# Calls self.init before calling the block. Implement this method when
|
45
|
+
#
|
46
|
+
# +cmd_str+ is the short name used to evoke this command. It will equal @cmd
|
47
|
+
# unless an alias was used used to evoke this command.
|
48
|
+
# +argv+ an array of unnamed arguments. If ignore :options was declared this
|
49
|
+
# will contain the arguments exactly as they were defined on the command-line.
|
50
|
+
# +stdin+ contains the output of stdin do; ...; end otherwise it's a STDIN IO handle.
|
51
|
+
# +global_options+ a hash of the global options specified on the command-line
|
52
|
+
# +options+ a hash of the command-specific options specific on the command-line.
|
53
|
+
def call(cmd_str=nil, argv=[], stdin=[], global_options={}, options={})
|
54
|
+
@alias = cmd_str.nil? ? @cmd : cmd_str
|
55
|
+
global_options.merge(options).each_pair do |n,v|
|
56
|
+
self.send("#{n}=", v)
|
57
|
+
end
|
58
|
+
|
59
|
+
self.init if respond_to? :init
|
60
|
+
|
61
|
+
block_args = [self, argv, stdin] # TODO: review order
|
62
|
+
@b.call(*block_args[0..(@b.arity-1)]) # send only as many args as defined
|
63
|
+
end
|
64
|
+
|
65
|
+
# The name of the command
|
66
|
+
def to_s
|
67
|
+
@cmd.to_s
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
module Drydock
|
73
|
+
class UnknownCommand < RuntimeError
|
74
|
+
attr_reader :name
|
75
|
+
def initialize(name)
|
76
|
+
@name = name || :unknown
|
77
|
+
end
|
78
|
+
def message
|
79
|
+
"Unknown command: #{@name}"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
class NoCommandsDefined < RuntimeError
|
83
|
+
def message
|
84
|
+
"No commands defined"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
class InvalidArgument < RuntimeError
|
88
|
+
attr_accessor :args
|
89
|
+
def initialize(args)
|
90
|
+
@args = args || []
|
91
|
+
end
|
92
|
+
def message
|
93
|
+
"Unknown option: #{@args.join(", ")}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
class MissingArgument < InvalidArgument
|
97
|
+
def message
|
98
|
+
"Option requires a value: #{@args.join(", ")}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Drydock is a DSL for command-line apps.
|
104
|
+
# See bin/example for usage examples.
|
105
|
+
module Drydock
|
106
|
+
extend self
|
107
|
+
|
108
|
+
VERSION = 0.3
|
109
|
+
|
110
|
+
private
|
111
|
+
# Stolen from Sinatra!
|
112
|
+
#def delegate(*args)
|
113
|
+
# args.each do |m|
|
114
|
+
# eval(<<-end_eval, binding, "(__Drydock__)", __LINE__)
|
115
|
+
# def #{m}(*args, &b)
|
116
|
+
# Drydock.#{m}(*args, &b)
|
117
|
+
# end
|
118
|
+
# end_eval
|
119
|
+
# end
|
120
|
+
#end
|
121
|
+
#
|
122
|
+
#delegate :before, :after, :alias_command, :desc
|
123
|
+
#delegate :global_option, :global_usage, :usage, :commands, :command
|
124
|
+
#delegate :debug, :option, :stdin, :default, :ignore, :command_alias
|
125
|
+
|
126
|
+
@@debug = false
|
127
|
+
@@has_run = false
|
128
|
+
@@run = true
|
129
|
+
@@default_command = nil
|
130
|
+
|
131
|
+
public
|
132
|
+
# Enable or disable debug output.
|
133
|
+
#
|
134
|
+
# debug :on
|
135
|
+
# debug :off
|
136
|
+
#
|
137
|
+
# Calling without :on or :off will toggle the value.
|
138
|
+
#
|
139
|
+
def debug(toggle=false)
|
140
|
+
if toggle.is_a? Symbol
|
141
|
+
@@debug = true if toggle == :on
|
142
|
+
@@debug = false if toggle == :off
|
143
|
+
else
|
144
|
+
@@debug = (!@@debug)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
# Returns true if debug output is enabled.
|
148
|
+
def debug?
|
149
|
+
@@debug
|
150
|
+
end
|
151
|
+
|
152
|
+
# Define a default command.
|
153
|
+
#
|
154
|
+
# default :task
|
155
|
+
#
|
156
|
+
def default(cmd)
|
157
|
+
@@default_command = canonize(cmd)
|
158
|
+
end
|
159
|
+
|
160
|
+
# Provide a description for a method
|
161
|
+
def desc(txt)
|
162
|
+
@@command_descriptions ||= []
|
163
|
+
@@command_descriptions << txt
|
164
|
+
end
|
165
|
+
|
166
|
+
# Define a block for processing STDIN before the command is called.
|
167
|
+
# The command block receives the return value of this block in a named argument:
|
168
|
+
#
|
169
|
+
# command :task do |obj, argv, stdin|; ...; end
|
170
|
+
#
|
171
|
+
# If a stdin block isn't defined, +stdin+ above will be the STDIN IO handle.
|
172
|
+
def stdin(&b)
|
173
|
+
@@stdin_block = b
|
174
|
+
end
|
175
|
+
|
176
|
+
# Define a block to be called before the command.
|
177
|
+
# This is useful for opening database connections, etc...
|
178
|
+
def before(&b)
|
179
|
+
@@before_block = b
|
180
|
+
end
|
181
|
+
|
182
|
+
# Define a block to be called after the command.
|
183
|
+
# This is useful for stopping, closing, etc... the stuff in the before block.
|
184
|
+
def after(&b)
|
185
|
+
@@after_block = b
|
186
|
+
end
|
187
|
+
|
188
|
+
# Define the default global usage banner. This is displayed
|
189
|
+
# with "script -h".
|
190
|
+
def global_usage(msg)
|
191
|
+
@@global_options ||= OpenStruct.new
|
192
|
+
global_opts_parser.banner = "USAGE: #{msg}"
|
193
|
+
end
|
194
|
+
|
195
|
+
# Define a command-specific usage banner. This is displayed
|
196
|
+
# with "script command -h"
|
197
|
+
def usage(msg)
|
198
|
+
get_current_option_parser.banner = "USAGE: #{msg}"
|
199
|
+
end
|
200
|
+
|
201
|
+
# Grab the options parser for the current command or create it if it doesn't exist.
|
202
|
+
def get_current_option_parser
|
203
|
+
@@command_opts_parser ||= []
|
204
|
+
@@command_index ||= 0
|
205
|
+
(@@command_opts_parser[@@command_index] ||= OptionParser.new)
|
206
|
+
end
|
207
|
+
|
208
|
+
# Tell the Drydock parser to ignore something.
|
209
|
+
# Drydock will currently only listen to you if you tell it to "ignore :options",
|
210
|
+
# otherwise it will ignore you!
|
211
|
+
#
|
212
|
+
# +what+ the thing to ignore. When it equals :options Drydock will not parse
|
213
|
+
# the command-specific arguments. It will pass the arguments directly to the
|
214
|
+
# Command object. This is useful when you want to parse the arguments in some a way
|
215
|
+
# that's too crazy, dangerous for Drydock to handle automatically.
|
216
|
+
def ignore(what=:nothing)
|
217
|
+
@@command_opts_parser[@@command_index] = :ignore if what == :options || what == :all
|
218
|
+
end
|
219
|
+
|
220
|
+
# Define a global option. See +option+ for more info.
|
221
|
+
def global_option(*args, &b)
|
222
|
+
args.unshift(global_opts_parser)
|
223
|
+
global_option_names << option_parser(args, &b)
|
224
|
+
end
|
225
|
+
|
226
|
+
# Define a command-specific option.
|
227
|
+
#
|
228
|
+
# +args+ is passed directly to OptionParser.on so it can contain anything
|
229
|
+
# that's valid to that method. Some examples:
|
230
|
+
# [:h, :help, "Displays this message"]
|
231
|
+
# [:m, :max, Integer, "Maximum threshold"]
|
232
|
+
# ['-l x,y,z', '--lang=x,y,z', Array, "Requested languages"]
|
233
|
+
# If a class is included, it will tell OptionParser to expect a value
|
234
|
+
# otherwise it assumes a boolean value.
|
235
|
+
#
|
236
|
+
# All calls to +option+ must come before the command they're associated
|
237
|
+
# to. Example:
|
238
|
+
#
|
239
|
+
# option :l, :longname, String, "Description" do; ...; end
|
240
|
+
# command :task do |obj|; ...; end
|
241
|
+
#
|
242
|
+
# When calling your script with a specific command-line option, the value
|
243
|
+
# is available via obj.longname inside the command block.
|
244
|
+
#
|
245
|
+
def option(*args, &b)
|
246
|
+
args.unshift(get_current_option_parser)
|
247
|
+
current_command_option_names << option_parser(args, &b)
|
248
|
+
end
|
249
|
+
|
250
|
+
# Define a command.
|
251
|
+
#
|
252
|
+
# command :task do
|
253
|
+
# ...
|
254
|
+
# end
|
255
|
+
#
|
256
|
+
# A custom command class can be specified using Hash syntax. The class
|
257
|
+
# must inherit from Drydock::Command (class CustomeClass < Drydock::Command)
|
258
|
+
#
|
259
|
+
# command :task => CustomCommand do
|
260
|
+
# ...
|
261
|
+
# end
|
262
|
+
#
|
263
|
+
def command(*cmds, &b)
|
264
|
+
@@command_index ||= 0
|
265
|
+
@@command_opts_parser ||= []
|
266
|
+
@@command_option_names ||= []
|
267
|
+
cmds.each do |cmd|
|
268
|
+
if cmd.is_a? Hash
|
269
|
+
c = cmd.values.first.new(cmd.keys.first, &b)
|
270
|
+
else
|
271
|
+
c = Drydock::Command.new(cmd, &b)
|
272
|
+
end
|
273
|
+
commands[c.cmd] = c
|
274
|
+
command_index_map[c.cmd] = @@command_index
|
275
|
+
@@command_index += 1
|
276
|
+
end
|
277
|
+
|
278
|
+
end
|
279
|
+
|
280
|
+
# Used to create an alias to a defined command.
|
281
|
+
# Here's an example:
|
282
|
+
#
|
283
|
+
# command :task do; ...; end
|
284
|
+
# alias_command :pointer, :task
|
285
|
+
#
|
286
|
+
# Either name can be used on the command-line:
|
287
|
+
#
|
288
|
+
# $ script task [options]
|
289
|
+
# $ script pointer [options]
|
290
|
+
#
|
291
|
+
# Inside of the command definition, you have access to the
|
292
|
+
# command name that was used via obj.alias.
|
293
|
+
def alias_command(aliaz, cmd)
|
294
|
+
return unless commands.has_key? cmd
|
295
|
+
commands[canonize(aliaz)] = commands[cmd]
|
296
|
+
end
|
297
|
+
|
298
|
+
# Identical to +alias_command+ with reversed arguments.
|
299
|
+
# For whatever reason I forget the order so Drydock supports both.
|
300
|
+
# Tip: the argument order matches the method name.
|
301
|
+
def command_alias(cmd, aliaz)
|
302
|
+
return unless commands.has_key? cmd
|
303
|
+
puts "#{canonize(aliaz)} to #{commands[cmd]}"
|
304
|
+
commands[canonize(aliaz)] = commands[cmd]
|
305
|
+
end
|
306
|
+
|
307
|
+
# An array of the currently defined Drydock::Command objects
|
308
|
+
def commands
|
309
|
+
@@commands ||= {}
|
310
|
+
@@commands
|
311
|
+
end
|
312
|
+
|
313
|
+
# An array of the currently defined commands names
|
314
|
+
def command_names
|
315
|
+
@@commands ||= {}
|
316
|
+
@@commands.keys.collect { |cmd| decanonize(cmd); }
|
317
|
+
end
|
318
|
+
|
319
|
+
# Returns true if automatic execution is enabled.
|
320
|
+
def run?
|
321
|
+
@@run
|
322
|
+
end
|
323
|
+
|
324
|
+
# Disable automatic execution (enabled by default)
|
325
|
+
#
|
326
|
+
# Drydock.run = false
|
327
|
+
def run=(v)
|
328
|
+
@@run = (v == true) ? true : false
|
329
|
+
end
|
330
|
+
|
331
|
+
# Return true if a command has been executed.
|
332
|
+
def has_run?
|
333
|
+
@@has_run
|
334
|
+
end
|
335
|
+
|
336
|
+
# Execute the given command.
|
337
|
+
# By default, Drydock automatically executes itself and provides handlers for known errors.
|
338
|
+
# You can override this functionality by calling +Drydock.run!+ yourself. Drydock
|
339
|
+
# will only call +run!+ once.
|
340
|
+
def run!(argv=[], stdin=STDIN)
|
341
|
+
return if has_run?
|
342
|
+
@@has_run = true
|
343
|
+
raise NoCommandsDefined.new if commands.empty?
|
344
|
+
@@global_options, cmd_name, @@command_options, argv = process_arguments(argv)
|
345
|
+
|
346
|
+
cmd_name ||= default_command
|
347
|
+
|
348
|
+
raise UnknownCommand.new(cmd_name) unless command?(cmd_name)
|
349
|
+
|
350
|
+
stdin = (defined? @@stdin_block) ? @@stdin_block.call(stdin, []) : stdin
|
351
|
+
@@before_block.call if defined? @@before_block
|
352
|
+
|
353
|
+
call_command(cmd_name, argv, stdin)
|
354
|
+
|
355
|
+
@@after_block.call if defined? @@after_block
|
356
|
+
|
357
|
+
rescue OptionParser::InvalidOption => ex
|
358
|
+
raise Drydock::InvalidArgument.new(ex.args)
|
359
|
+
rescue OptionParser::MissingArgument => ex
|
360
|
+
raise Drydock::MissingArgument.new(ex.args)
|
361
|
+
end
|
362
|
+
|
363
|
+
private
|
364
|
+
|
365
|
+
# Executes the block associated to +cmd+
|
366
|
+
def call_command(cmd, argv=[], stdin=nil)
|
367
|
+
return unless command?(cmd)
|
368
|
+
get_command(cmd).call(cmd, argv, stdin, @@global_options || {}, @@command_options || {})
|
369
|
+
end
|
370
|
+
|
371
|
+
# Returns the Drydock::Command object with the name +cmd+
|
372
|
+
def get_command(cmd)
|
373
|
+
return unless command?(cmd)
|
374
|
+
@@commands[canonize(cmd)]
|
375
|
+
end
|
376
|
+
|
377
|
+
# Returns true if a command with the name +cmd+ has been defined.
|
378
|
+
def command?(cmd)
|
379
|
+
name = canonize(cmd)
|
380
|
+
(@@commands || {}).has_key? name
|
381
|
+
end
|
382
|
+
|
383
|
+
# Canonizes a string (+cmd+) to the symbol for command names
|
384
|
+
# '-' is replaced with '_'
|
385
|
+
def canonize(cmd)
|
386
|
+
return unless cmd
|
387
|
+
return cmd if cmd.kind_of?(Symbol)
|
388
|
+
cmd.to_s.tr('-', '_').to_sym
|
389
|
+
end
|
390
|
+
|
391
|
+
# Returns a string version of +cmd+, decanonized.
|
392
|
+
# Lowercase, '_' is replaced with '-'
|
393
|
+
def decanonize(cmd)
|
394
|
+
return unless cmd
|
395
|
+
cmd.to_s.tr('_', '-')
|
396
|
+
end
|
397
|
+
|
398
|
+
# Processes calls to option and global_option. Symbols are converted into
|
399
|
+
# OptionParser style strings (:h and :help become '-h' and '--help').
|
400
|
+
def option_parser(args=[], &b)
|
401
|
+
return if args.empty?
|
402
|
+
opts_parser = args.shift
|
403
|
+
|
404
|
+
arg_name = ''
|
405
|
+
symbol_switches = []
|
406
|
+
args.each_with_index do |arg, index|
|
407
|
+
if arg.is_a? Symbol
|
408
|
+
arg_name = arg.to_s if arg.to_s.size > arg_name.size
|
409
|
+
args[index] = (arg.to_s.length == 1) ? "-#{arg.to_s}" : "--#{arg.to_s}"
|
410
|
+
symbol_switches << args[index]
|
411
|
+
elsif arg.kind_of?(Class)
|
412
|
+
symbol_switches.each do |arg|
|
413
|
+
arg << "=S"
|
414
|
+
end
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
if args.size == 1
|
419
|
+
opts_parser.on(args.shift)
|
420
|
+
else
|
421
|
+
opts_parser.on(*args) do |v|
|
422
|
+
block_args = [v, opts_parser]
|
423
|
+
result = (b.nil?) ? v : b.call(*block_args[0..(b.arity-1)])
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
arg_name
|
428
|
+
end
|
429
|
+
|
430
|
+
|
431
|
+
# Split the +argv+ array into global args and command args and
|
432
|
+
# find the command name.
|
433
|
+
# i.e. ./script -H push -f (-H is a global arg, push is the command, -f is a command arg)
|
434
|
+
# returns [global_options, cmd, command_options, argv]
|
435
|
+
def process_arguments(argv=[])
|
436
|
+
global_options = command_options = {}
|
437
|
+
cmd = nil
|
438
|
+
|
439
|
+
global_options = global_opts_parser.getopts(argv)
|
440
|
+
|
441
|
+
cmd_name = (argv.empty?) ? @@default_command : argv.shift
|
442
|
+
raise UnknownCommand.new(cmd_name) unless command?(cmd_name)
|
443
|
+
|
444
|
+
cmd = get_command(cmd_name)
|
445
|
+
|
446
|
+
command_parser = @@command_opts_parser[get_command_index(cmd.cmd)]
|
447
|
+
command_options = {}
|
448
|
+
|
449
|
+
# We only need to parse the options out of the arguments when
|
450
|
+
# there are args available, there is a valid parser, and
|
451
|
+
# we weren't requested to ignore the options.
|
452
|
+
if !argv.empty? && command_parser && command_parser != :ignore
|
453
|
+
command_options = command_parser.getopts(argv)
|
454
|
+
end
|
455
|
+
|
456
|
+
# Add accessors to the Drydock::Command object
|
457
|
+
# for the global and command specific options
|
458
|
+
[global_option_names, (command_option_names[get_command_index(cmd_name)] || [])].flatten.each do |n|
|
459
|
+
unless cmd.respond_to?(n)
|
460
|
+
cmd.class.send(:define_method, n) do
|
461
|
+
instance_variable_get("@#{n}")
|
462
|
+
end
|
463
|
+
end
|
464
|
+
unless cmd.respond_to?("#{n}=")
|
465
|
+
cmd.class.send(:define_method, "#{n}=") do |val|
|
466
|
+
instance_variable_set("@#{n}", val)
|
467
|
+
end
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
[global_options, cmd_name, command_options, argv]
|
472
|
+
end
|
473
|
+
|
474
|
+
def global_option_names
|
475
|
+
@@global_option_names ||= []
|
476
|
+
end
|
477
|
+
|
478
|
+
# Grab the current list of command-specific option names. This is a list of the
|
479
|
+
# long names.
|
480
|
+
def current_command_option_names
|
481
|
+
@@command_option_names ||= []
|
482
|
+
@@command_index ||= 0
|
483
|
+
(@@command_option_names[@@command_index] ||= [])
|
484
|
+
end
|
485
|
+
|
486
|
+
def command_index_map
|
487
|
+
@@command_index_map ||= {}
|
488
|
+
end
|
489
|
+
|
490
|
+
def get_command_index(cmd)
|
491
|
+
command_index_map[canonize(cmd)] || -1
|
492
|
+
end
|
493
|
+
|
494
|
+
def command_option_names
|
495
|
+
@@command_option_names ||= []
|
496
|
+
end
|
497
|
+
|
498
|
+
def global_opts_parser
|
499
|
+
@@global_opts_parser ||= OptionParser.new
|
500
|
+
end
|
501
|
+
|
502
|
+
def default_command
|
503
|
+
@@default_command ||= nil
|
504
|
+
end
|
505
|
+
|
506
|
+
end
|
507
|
+
|
508
|
+
|
509
|
+
|
510
|
+
trap ("SIGINT") do
|
511
|
+
puts "#{$/}Exiting..."
|
512
|
+
exit 1
|
513
|
+
end
|
514
|
+
|
515
|
+
|
516
|
+
at_exit {
|
517
|
+
begin
|
518
|
+
Drydock.run!(ARGV, STDIN) if Drydock.run? && !Drydock.has_run?
|
519
|
+
rescue => ex
|
520
|
+
STDERR.puts "ERROR: #{ex.message}"
|
521
|
+
STDERR.puts ex.backtrace if Drydock.debug?
|
522
|
+
end
|
523
|
+
}
|
524
|
+
|
525
|
+
|