adamwiggins-sumo 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (8) hide show
  1. data/README.rdoc +122 -0
  2. data/Rakefile +23 -0
  3. data/VERSION +1 -0
  4. data/bin/sumo +173 -0
  5. data/lib/sumo.rb +268 -0
  6. data/spec/base.rb +21 -0
  7. data/spec/sumo_spec.rb +31 -0
  8. metadata +60 -0
data/README.rdoc ADDED
@@ -0,0 +1,122 @@
1
+ = Tired of wrestling with server provisioning? Sumo!
2
+
3
+ Want to fire up a one-off EC2 instance, pronto? ec2-run-instances got you down? Try Sumo.
4
+
5
+ $ sumo launch
6
+ ---> Launching instance... i-4f809c26 (1.5s)
7
+ ---> Acquiring hostname... ec2-67-202-17-178.compute-1.amazonaws.com (26.7s)
8
+
9
+ Logging you in via ssh. Type 'exit' or Ctrl-D to return to your local system.
10
+ ------------------------------------------------------------------------------
11
+ Linux domU-12-31-39-04-31-37 2.6.21.7-2.fc8xen #1 SMP Fri Feb 15 12:39:36 EST 2008 i686
12
+ ...
13
+ root@domU-12-31-39-04-31-37:~#
14
+
15
+ Later...
16
+
17
+ $ sumo terminate
18
+ ec2-67-202-17-178.compute-1.amazonaws.com scheduled for termination
19
+
20
+ You can manage multiple instances via "sumo list" and specifying hostname or instance id as arguments to the ssh or terminate commands.
21
+
22
+ == Service installation with Chef
23
+
24
+ The launch command takes an argument, which is a server role (from roles/#{role}.json inside your cookbooks repo):
25
+
26
+ $ sumo launch redis
27
+ ---> Launch instance... i-b96c73d0 (1.3s)
28
+ ---> Acquire hostname... ec2-75-101-191-220.compute-1.amazonaws.com (36.1s)
29
+ ---> Wait for ssh... done (9.0s)
30
+ ---> Bootstrap chef... done (61.3s)
31
+ ---> Setup redis... done (11.9s)
32
+ ---> Opening firewall... ports 6379 (5.2s)
33
+
34
+ Your instance is exporting the following resources:
35
+ Redis: redis://:8452cdd98f428c972f08@ec2-75-101-191-220.compute-1.amazonaws.com:6379/0
36
+
37
+ The instance can assume multiple roles if you like:
38
+
39
+ $ sumo launch redis,solr,couchdb
40
+
41
+ == Setup
42
+
43
+ Dependencies:
44
+
45
+ $ sudo gem install amazon-ec2 thor
46
+
47
+ Then create ~/.sumo/config.yml containing:
48
+
49
+ ---
50
+ access_id: <your amazon access key id>
51
+ access_secret: <your amazon secret access key>
52
+
53
+ Optional config you can include any of the following in your config.yml:
54
+
55
+ user: root
56
+ ami: ami-ed46a784
57
+ availability_zone: us-east-1b
58
+ cookbooks_url: git://github.com/adamwiggins/chef-cookbooks.git
59
+
60
+ You'll need Bacon and Mocha if you want to run the specs, and Jewler if you want to create gems.
61
+
62
+ == Managing volumes
63
+
64
+ Create and attach a volume to your running instance:
65
+
66
+ $ sumo create_volume
67
+ ---> Create 5MB volume... vol-8a9c6ae3 (1.1s)
68
+ $ sumo volumes
69
+ vol-8a9c6ae3 5MB available
70
+ $ sumo attach
71
+ ---> Attach vol-8a9c6ae3 to i-bc32cbd4 as /dev/sdc1... done (0.6s)
72
+
73
+ Log in to format and mount the volume:
74
+
75
+ $ sumo ssh
76
+ root@ip-10-251-122-175:~# mkfs.ext3 /dev/sdc1
77
+ mke2fs 1.41.4 (27-Jan-2009)
78
+ Filesystem label=
79
+ OS type: Linux
80
+ Block size=4096 (log=2)
81
+ ...
82
+ $ mkdir /myvol
83
+ $ mount /dev/sdc1 /myvol
84
+ $ echo "I'm going to persist to a volume" > /myvol/hello.txt
85
+
86
+ To detach from a running instance (perhaps so you can attach elsewhere):
87
+
88
+ $ sumo detatch
89
+ ---> Detach vol-8a9c6ae3... done (0.6s)
90
+
91
+ Destroy it if you no longer want the data stored on it:
92
+
93
+ $ sumo destroy_volume
94
+ ---> Destroy volume... done (0.8s)
95
+
96
+ == Some details you might want to know
97
+
98
+ Sumo creates its own keypair named sumo, which is stored in ~/.ssh/keypair.pem. Amazon doesn't let you upload your own ssh public key, which is lame, so this is the best option for making the launch-and-connect process a single step.
99
+
100
+ It will also create an Amazon security group called sumo, so that it can lower the firewall for services you configure via cookbook roles.
101
+
102
+ If you run any production machines from your EC2 account, I recommend setting up a separate account for use with Sumo. It does not prompt for confirmation when terminating an instance or differentiate between instances started by it vs. instances started by other tools.
103
+
104
+ == Anti-features
105
+
106
+ Sumo is not a cloud management tool, a monitor tool, or anything more than a way to get an instance up right quick. If you're looking for a way to manage a cluster of production instances, try one of these fine tools.
107
+
108
+ * Pool Party
109
+ * RightScale
110
+ * Engine Yard Cloud
111
+ * Cloudkick
112
+
113
+ == Meta
114
+
115
+ Created by Adam Wiggins
116
+
117
+ Patches contributed by Orion Henry, Blake Mizerany, and Jesse Newland
118
+
119
+ Released under the MIT License: http://www.opensource.org/licenses/mit-license.php
120
+
121
+ http://github.com/adamwiggins/sumo
122
+
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ require 'jeweler'
2
+
3
+ Jeweler::Tasks.new do |s|
4
+ s.name = "sumo"
5
+ s.description = "A no-hassle way to launch one-off EC2 instances from the command line"
6
+ s.summary = s.description
7
+ s.author = "Adam Wiggins"
8
+ s.email = "adam@heroku.com"
9
+ s.homepage = "http://github.com/adamwiggins/sumo"
10
+ s.rubyforge_project = "sumo"
11
+ s.files = FileList["[A-Z]*", "{bin,lib,spec}/**/*"]
12
+ s.executables = %w(sumo)
13
+ end
14
+
15
+ Jeweler::RubyforgeTasks.new
16
+
17
+ desc 'Run specs'
18
+ task :spec do
19
+ sh 'bacon -s spec/*_spec.rb'
20
+ end
21
+
22
+ task :default => :spec
23
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
data/bin/sumo ADDED
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.dirname(__FILE__) + '/../lib/sumo'
4
+
5
+ require 'thor'
6
+
7
+ class CLI < Thor
8
+ desc "launch [<role>]", "launch an instance as role, or omit to ssh to vanilla instance"
9
+ def launch(role=nil)
10
+ id = task("Launch instance") { sumo.launch }
11
+ host = task("Acquire hostname") { sumo.wait_for_hostname(id) }
12
+ task("Wait for ssh") { sumo.wait_for_ssh(host) }
13
+
14
+ if role
15
+ task("Bootstrap chef") { sumo.bootstrap_chef(host) }
16
+ role.split(',').each do |role|
17
+ task("Setup #{role}") { sumo.setup_role(host, role) }
18
+ end
19
+
20
+ resources = sumo.resources(host)
21
+ unless resources.empty?
22
+ task("Open firewall") do
23
+ ports = resources.map { |r| r.match(/:(\d+)\//)[1] }
24
+ ports.each { |port| sumo.open_firewall(port) }
25
+ "ports " + ports.join(", ")
26
+ end
27
+ end
28
+
29
+ puts
30
+ display_resources(host)
31
+ else
32
+ puts "\nLogging you in via ssh. Type 'exit' or Ctrl-D to return to your local system."
33
+ puts '-' * 78
34
+ connect_ssh(host)
35
+ end
36
+ end
37
+
38
+ desc "ssh [<instance_id or hostname>]", "ssh to a specified instance or first available"
39
+ def ssh(id=nil)
40
+ inst = sumo.find(id) || sumo.running.first || abort("No running instances")
41
+ hostname = inst[:hostname] || wait_for_hostname(inst[:instance_id])
42
+ connect_ssh hostname
43
+ end
44
+
45
+ desc "resources [<instance_id or hostname>]", "show resources exported by an instance"
46
+ def resources(id=nil)
47
+ inst = sumo.find(id) || sumo.running.first || abort("No running instances")
48
+ hostname = inst[:hostname] || wait_for_hostname(inst[:instance_id])
49
+ display_resources(inst[:hostname])
50
+ end
51
+
52
+ desc "bootstrap", "bootstrap chef and cookbooks"
53
+ def bootstrap(id=nil)
54
+ inst = sumo.find(id) || sumo.running.first || abort("No running instances")
55
+ task "Bootstrap chef" do
56
+ sumo.bootstrap_chef(inst[:hostname])
57
+ end
58
+ end
59
+
60
+ desc "role", "setup instance as a role"
61
+ def role(role, id=nil)
62
+ inst = sumo.find(id) || sumo.running.first || abort("No running instances")
63
+ task "Setup #{role}" do
64
+ sumo.setup_role(inst[:hostname], role)
65
+ end
66
+ end
67
+
68
+ desc "list", "list running instances"
69
+ def list
70
+ sumo.list.each do |inst|
71
+ printf "%-50s %-12s %s\n", inst[:hostname], inst[:instance_id], inst[:status]
72
+ end
73
+ end
74
+
75
+ desc "console [<instance_id or hostname>]", "get console output for instance or first available"
76
+ def console(id=nil)
77
+ inst = sumo.find(id) || (sumo.running | sumo.pending).first || abort("No running or pending instances")
78
+
79
+ puts sumo.console_output(inst[:instance_id]).inspect
80
+ end
81
+
82
+ desc "terminate [<instance_id or hostname>]", "terminate specified instance or first available"
83
+ def terminate(id=nil)
84
+ inst = sumo.find(id) || (sumo.running | sumo.pending).first || abort("No running or pending instances")
85
+
86
+ sumo.terminate(inst[:instance_id])
87
+ puts "#{inst[:hostname] || inst[:instance_id]} scheduled for termination"
88
+ end
89
+
90
+ desc "terminate_all", "terminate all instances"
91
+ def terminate_all
92
+ instances = (sumo.running | sumo.pending)
93
+ abort("No running or pending instances") if instances.empty?
94
+ instances.each do |inst|
95
+ sumo.terminate(inst[:instance_id])
96
+ puts "#{inst[:hostname] || inst[:instance_id]} scheduled for termination"
97
+ end
98
+ end
99
+
100
+ desc "volumes", "list all volumes"
101
+ def volumes
102
+ sumo.volumes.each do |v|
103
+ printf "%-10s %4sMB %10s %15s %15s\n", v[:volume_id], v[:size], v[:status], v[:instance], v[:device]
104
+ end
105
+ end
106
+
107
+ desc "create_volume [<megabytes>]", "create a volume"
108
+ def create_volume(size=5)
109
+ task("Create #{size}MB volume") { sumo.create_volume(size) }
110
+ end
111
+
112
+ desc "destroy_volume [<volume_id>]", "destroy a volume"
113
+ def destroy_volume(volume=nil)
114
+ vol_id = (sumo.find_volume(volume) || sumo.nondestroyed_volumes.first || abort("No volumes"))[:volume_id]
115
+ task("Destroy volume") { sumo.destroy_volume(vol_id) }
116
+ end
117
+
118
+ desc "attach [<volume_id>] [<instance_id or hostname>] [<device>]", "attach volume to running instance"
119
+ def attach(volume=nil, inst_id=nil, device=nil)
120
+ vol_id = (sumo.find_volume(volume) || sumo.available_volumes.first || abort("No available volumes"))[:volume_id]
121
+ inst_id = (sumo.find(inst_id) || sumo.running.first || abort("No running instances"))[:instance_id]
122
+ device ||= '/dev/sdc1'
123
+ task("Attach #{vol_id} to #{inst_id} as #{device}") do
124
+ sumo.attach(vol_id, inst_id, device)
125
+ end
126
+ end
127
+
128
+ desc "detach [<volume_id>]", "detach volume from instance"
129
+ def detach(volume=nil)
130
+ vol_id = (sumo.find_volume(volume) || sumo.attached_volumes.first || abort("No attached volumes"))[:volume_id]
131
+ task("Detach #{vol_id}") { sumo.detach(vol_id) }
132
+ end
133
+
134
+ no_tasks do
135
+ def sumo
136
+ @sumo ||= Sumo.new
137
+ end
138
+
139
+ def config
140
+ sumo.config
141
+ end
142
+
143
+ def task(msg, &block)
144
+ printf "---> %-24s ", "#{msg}..."
145
+ start = Time.now
146
+ result = block.call || 'done'
147
+ finish = Time.now
148
+ time = sprintf("%0.1f", finish - start)
149
+ puts "#{result} (#{time}s)"
150
+ result
151
+ end
152
+
153
+ def connect_ssh(hostname)
154
+ sumo.wait_for_ssh(hostname)
155
+ system "ssh -i #{sumo.keypair_file} #{config['user']}@#{hostname}"
156
+ if $?.success?
157
+ puts "\nType 'sumo terminate' if you're done with this instance."
158
+ end
159
+ end
160
+
161
+ def display_resources(host)
162
+ resources = sumo.resources(host)
163
+ unless resources.empty?
164
+ puts "Your instance is exporting the following resources:"
165
+ resources.each do |resource|
166
+ puts " #{resource}"
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
172
+
173
+ CLI.start
data/lib/sumo.rb ADDED
@@ -0,0 +1,268 @@
1
+ require 'AWS'
2
+ require 'yaml'
3
+ require 'socket'
4
+
5
+ class Sumo
6
+ def launch
7
+ ami = config['ami']
8
+ raise "No AMI selected" unless ami
9
+
10
+ create_keypair unless File.exists? keypair_file
11
+
12
+ create_security_group
13
+ open_firewall(22)
14
+
15
+ result = ec2.run_instances(
16
+ :image_id => ami,
17
+ :instance_type => config['instance_size'] || 'm1.small',
18
+ :key_name => 'sumo',
19
+ :group_id => [ 'sumo' ],
20
+ :availability_zone => config['availability_zone'],
21
+ )
22
+ result.instancesSet.item[0].instanceId
23
+ end
24
+
25
+ def list
26
+ @list ||= fetch_list
27
+ end
28
+
29
+ def volumes
30
+ result = ec2.describe_volumes
31
+ return [] unless result.volumeSet
32
+
33
+ result.volumeSet.item.map do |row|
34
+ {
35
+ :volume_id => row["volumeId"],
36
+ :size => row["size"],
37
+ :status => row["status"],
38
+ :device => (row["attachmentSet"]["item"].first["device"] rescue ""),
39
+ :instance_id => (row["attachmentSet"]["item"].first["instanceId"] rescue ""),
40
+ }
41
+ end
42
+ end
43
+
44
+ def available_volumes
45
+ volumes.select { |vol| vol[:status] == 'available' }
46
+ end
47
+
48
+ def attached_volumes
49
+ volumes.select { |vol| vol[:status] == 'in-use' }
50
+ end
51
+
52
+ def nondestroyed_volumes
53
+ volumes.select { |vol| vol[:status] != 'deleting' }
54
+ end
55
+
56
+ def attach(volume, instance, device)
57
+ result = ec2.attach_volume(
58
+ :volume_id => volume,
59
+ :instance_id => instance,
60
+ :device => device
61
+ )
62
+ "done"
63
+ end
64
+
65
+ def detach(volume)
66
+ result = ec2.detach_volume(:volume_id => volume, :force => "true")
67
+ "done"
68
+ end
69
+
70
+ def create_volume(size)
71
+ result = ec2.create_volume(
72
+ :availability_zone => config['availability_zone'],
73
+ :size => size.to_s
74
+ )
75
+ result["volumeId"]
76
+ end
77
+
78
+ def destroy_volume(volume)
79
+ ec2.delete_volume(:volume_id => volume)
80
+ "done"
81
+ end
82
+
83
+ def fetch_list
84
+ result = ec2.describe_instances
85
+ return [] unless result.reservationSet
86
+
87
+ instances = []
88
+ result.reservationSet.item.each do |r|
89
+ r.instancesSet.item.each do |item|
90
+ instances << {
91
+ :instance_id => item.instanceId,
92
+ :status => item.instanceState.name,
93
+ :hostname => item.dnsName
94
+ }
95
+ end
96
+ end
97
+ instances
98
+ end
99
+
100
+ def find(id_or_hostname)
101
+ return unless id_or_hostname
102
+ id_or_hostname = id_or_hostname.strip.downcase
103
+ list.detect do |inst|
104
+ inst[:hostname] == id_or_hostname or
105
+ inst[:instance_id] == id_or_hostname or
106
+ inst[:instance_id].gsub(/^i-/, '') == id_or_hostname
107
+ end
108
+ end
109
+
110
+ def find_volume(volume_id)
111
+ return unless volume_id
112
+ volume_id = volume_id.strip.downcase
113
+ volumes.detect do |volume|
114
+ inst[:volume_id] == volume_id or
115
+ inst[:volume_id].gsub(/^vol-/, '') == volume_id
116
+ end
117
+ end
118
+
119
+ def running
120
+ list_by_status('running')
121
+ end
122
+
123
+ def pending
124
+ list_by_status('pending')
125
+ end
126
+
127
+ def list_by_status(status)
128
+ list.select { |i| i[:status] == status }
129
+ end
130
+
131
+ def instance_info(instance_id)
132
+ fetch_list.detect do |inst|
133
+ inst[:instance_id] == instance_id
134
+ end
135
+ end
136
+
137
+ def wait_for_hostname(instance_id)
138
+ raise ArgumentError unless instance_id and instance_id.match(/^i-/)
139
+ loop do
140
+ if inst = instance_info(instance_id)
141
+ if hostname = inst[:hostname]
142
+ return hostname
143
+ end
144
+ end
145
+ sleep 1
146
+ end
147
+ end
148
+
149
+ def wait_for_ssh(hostname)
150
+ raise ArgumentError unless hostname
151
+ loop do
152
+ begin
153
+ Timeout::timeout(4) do
154
+ TCPSocket.new(hostname, 22)
155
+ return
156
+ end
157
+ rescue SocketError, Timeout::Error, Errno::ECONNREFUSED, Errno::EHOSTUNREACH
158
+ end
159
+ end
160
+ end
161
+
162
+ def bootstrap_chef(hostname)
163
+ commands = [
164
+ 'apt-get update',
165
+ 'apt-get autoremove -y',
166
+ 'apt-get install -y ruby ruby-dev rubygems git-core',
167
+ 'gem sources -a http://gems.opscode.com',
168
+ 'gem install chef ohai --no-rdoc --no-ri',
169
+ "git clone #{config['cookbooks_url']}",
170
+ ]
171
+ ssh(hostname, commands)
172
+ end
173
+
174
+ def setup_role(hostname, role)
175
+ commands = [
176
+ "cd chef-cookbooks",
177
+ "/var/lib/gems/1.8/bin/chef-solo -c config.json -j roles/#{role}.json"
178
+ ]
179
+ ssh(hostname, commands)
180
+ end
181
+
182
+ def ssh(hostname, cmds)
183
+ IO.popen("ssh -i #{keypair_file} #{config['user']}@#{hostname} > ~/.sumo/ssh.log 2>&1", "w") do |pipe|
184
+ pipe.puts cmds.join(' && ')
185
+ end
186
+ unless $?.success?
187
+ abort "failed\nCheck ~/.sumo/ssh.log for the output"
188
+ end
189
+ end
190
+
191
+ def resources(hostname)
192
+ @resources ||= {}
193
+ @resources[hostname] ||= fetch_resources(hostname)
194
+ end
195
+
196
+ def fetch_resources(hostname)
197
+ cmd = "ssh -i #{keypair_file} #{config['user']}@#{hostname} 'cat /root/resources' 2>&1"
198
+ out = IO.popen(cmd, 'r') { |pipe| pipe.read }
199
+ abort "failed to read resources, output:\n#{out}" unless $?.success?
200
+ parse_resources(out, hostname)
201
+ end
202
+
203
+ def parse_resources(raw, hostname)
204
+ raw.split("\n").map do |line|
205
+ line.gsub(/localhost/, hostname)
206
+ end
207
+ end
208
+
209
+ def terminate(instance_id)
210
+ ec2.terminate_instances(:instance_id => [ instance_id ])
211
+ end
212
+
213
+ def console_output(instance_id)
214
+ ec2.get_console_output(:instance_id => instance_id)["output"]
215
+ end
216
+
217
+ def config
218
+ @config ||= default_config.merge read_config
219
+ end
220
+
221
+ def default_config
222
+ {
223
+ 'user' => 'root',
224
+ 'ami' => 'ami-ed46a784',
225
+ 'availability_zone' => 'us-east-1b',
226
+ }
227
+ end
228
+
229
+ def sumo_dir
230
+ "#{ENV['HOME']}/.sumo"
231
+ end
232
+
233
+ def read_config
234
+ YAML.load File.read("#{sumo_dir}/config.yml")
235
+ rescue Errno::ENOENT
236
+ raise "Sumo is not configured, please fill in ~/.sumo/config.yml"
237
+ end
238
+
239
+ def keypair_file
240
+ "#{sumo_dir}/keypair.pem"
241
+ end
242
+
243
+ def create_keypair
244
+ keypair = ec2.create_keypair(:key_name => "sumo").keyMaterial
245
+ File.open(keypair_file, 'w') { |f| f.write keypair }
246
+ File.chmod 0600, keypair_file
247
+ end
248
+
249
+ def create_security_group
250
+ ec2.create_security_group(:group_name => 'sumo', :group_description => 'Sumo')
251
+ rescue AWS::InvalidGroupDuplicate
252
+ end
253
+
254
+ def open_firewall(port)
255
+ ec2.authorize_security_group_ingress(
256
+ :group_name => 'sumo',
257
+ :ip_protocol => 'tcp',
258
+ :from_port => port,
259
+ :to_port => port,
260
+ :cidr_ip => '0.0.0.0/0'
261
+ )
262
+ rescue AWS::InvalidPermissionDuplicate
263
+ end
264
+
265
+ def ec2
266
+ @ec2 ||= AWS::EC2::Base.new(:access_key_id => config['access_id'], :secret_access_key => config['access_secret'])
267
+ end
268
+ end
data/spec/base.rb ADDED
@@ -0,0 +1,21 @@
1
+ require File.dirname(__FILE__) + '/../lib/sumo'
2
+
3
+ require 'bacon'
4
+ require 'mocha/standalone'
5
+ require 'mocha/object'
6
+
7
+ class Bacon::Context
8
+ include Mocha::API
9
+
10
+ def initialize(name, &block)
11
+ @name = name
12
+ @before, @after = [
13
+ [lambda { mocha_setup }],
14
+ [lambda { mocha_verify ; mocha_teardown }]
15
+ ]
16
+ @block = block
17
+ end
18
+
19
+ def xit(desc, &bk)
20
+ end
21
+ end
data/spec/sumo_spec.rb ADDED
@@ -0,0 +1,31 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+
3
+ require 'fileutils'
4
+
5
+ describe Sumo do
6
+ before do
7
+ @work_path = "/tmp/spec_#{Process.pid}/"
8
+ FileUtils.mkdir_p(@work_path)
9
+ File.open("#{@work_path}/config.yml", "w") do |f|
10
+ f.write YAML.dump({})
11
+ end
12
+
13
+ @sumo = Sumo.new
14
+ @sumo.stubs(:sumo_dir).returns(@work_path)
15
+ end
16
+
17
+ after do
18
+ FileUtils.rm_rf(@work_path)
19
+ end
20
+
21
+ it "defaults to user root if none is specified in the config" do
22
+ @sumo.config['user'].should == 'root'
23
+ end
24
+
25
+ it "uses specified user if one is in the config" do
26
+ File.open("#{@work_path}/config.yml", "w") do |f|
27
+ f.write YAML.dump('user' => 'joe')
28
+ end
29
+ @sumo.config['user'].should == 'joe'
30
+ end
31
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: adamwiggins-sumo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Adam Wiggins
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-09-07 00:00:00 -07:00
13
+ default_executable: sumo
14
+ dependencies: []
15
+
16
+ description: A no-hassle way to launch one-off EC2 instances from the command line
17
+ email: adam@heroku.com
18
+ executables:
19
+ - sumo
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.rdoc
24
+ files:
25
+ - README.rdoc
26
+ - Rakefile
27
+ - VERSION
28
+ - bin/sumo
29
+ - lib/sumo.rb
30
+ - spec/base.rb
31
+ - spec/sumo_spec.rb
32
+ has_rdoc: true
33
+ homepage: http://github.com/adamwiggins/sumo
34
+ post_install_message:
35
+ rdoc_options:
36
+ - --charset=UTF-8
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0"
44
+ version:
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: "0"
50
+ version:
51
+ requirements: []
52
+
53
+ rubyforge_project: sumo
54
+ rubygems_version: 1.2.0
55
+ signing_key:
56
+ specification_version: 2
57
+ summary: A no-hassle way to launch one-off EC2 instances from the command line
58
+ test_files:
59
+ - spec/base.rb
60
+ - spec/sumo_spec.rb