├── .gitignore ├── README.rdoc ├── Rakefile ├── VERSION ├── bin └── sumo ├── lib └── sumo.rb ├── spec ├── base.rb └── sumo_spec.rb └── sumo.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 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: 51 | access_secret: 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, Jesse Newland, Gert Goet, 118 | and Tim Lossen 119 | 120 | Released under the MIT License: http://www.opensource.org/licenses/mit-license.php 121 | 122 | http://github.com/adamwiggins/sumo 123 | 124 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 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 | s.add_dependency "amazon-ec2" 14 | s.add_dependency "thor" 15 | end 16 | 17 | Jeweler::RubyforgeTasks.new 18 | 19 | desc 'Run specs' 20 | task :spec do 21 | sh 'bacon -s spec/*_spec.rb' 22 | end 23 | 24 | task :default => :spec 25 | 26 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.2.3 2 | -------------------------------------------------------------------------------- /bin/sumo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | require File.dirname(__FILE__) + '/../lib/sumo' 5 | 6 | require 'thor' 7 | 8 | class CLI < Thor 9 | desc "launch []", "launch an instance as role, or omit to ssh to vanilla instance" 10 | def launch(role=nil) 11 | id = task("Launch instance") { sumo.launch } 12 | host = task("Acquire hostname") { sumo.wait_for_hostname(id) } 13 | task("Wait for ssh") { sumo.wait_for_ssh(host) } 14 | 15 | if role 16 | task("Bootstrap chef") { sumo.bootstrap_chef(host) } 17 | role.split(',').each do |role| 18 | task("Setup #{role}") { sumo.setup_role(host, role) } 19 | end 20 | 21 | resources = sumo.resources(host) 22 | unless resources.empty? 23 | task("Open firewall") do 24 | ports = resources.map { |r| r.match(/:(\d+)\//)[1] } 25 | ports.each { |port| sumo.open_firewall(port) } 26 | "ports " + ports.join(", ") 27 | end 28 | end 29 | 30 | puts 31 | display_resources(host) 32 | else 33 | puts "\nLogging you in via ssh. Type 'exit' or Ctrl-D to return to your local system." 34 | puts '-' * 78 35 | connect_ssh(host) 36 | end 37 | end 38 | 39 | desc "ssh []", "ssh to a specified instance or first available" 40 | def ssh(id=nil) 41 | inst = sumo.find(id) || sumo.running.first || abort("No running instances") 42 | hostname = inst[:hostname] || wait_for_hostname(inst[:instance_id]) 43 | connect_ssh hostname 44 | end 45 | 46 | desc "resources []", "show resources exported by an instance" 47 | def resources(id=nil) 48 | inst = sumo.find(id) || sumo.running.first || abort("No running instances") 49 | hostname = inst[:hostname] || wait_for_hostname(inst[:instance_id]) 50 | display_resources(inst[:hostname]) 51 | end 52 | 53 | desc "bootstrap", "bootstrap chef and cookbooks" 54 | def bootstrap(id=nil) 55 | inst = sumo.find(id) || sumo.running.first || abort("No running instances") 56 | task "Bootstrap chef" do 57 | sumo.bootstrap_chef(inst[:hostname]) 58 | end 59 | end 60 | 61 | desc "role", "setup instance as a role" 62 | def role(role, id=nil) 63 | inst = sumo.find(id) || sumo.running.first || abort("No running instances") 64 | task "Setup #{role}" do 65 | sumo.setup_role(inst[:hostname], role) 66 | end 67 | end 68 | 69 | desc "list", "list running instances" 70 | def list 71 | sumo.list.each do |inst| 72 | printf "%-50s %-12s %s\n", inst[:hostname], inst[:instance_id], inst[:status] 73 | end 74 | end 75 | 76 | desc "console []", "get console output for instance or first available" 77 | def console(id=nil) 78 | inst = sumo.find(id) || (sumo.running | sumo.pending).first || abort("No running or pending instances") 79 | 80 | puts sumo.console_output(inst[:instance_id]).inspect 81 | end 82 | 83 | desc "terminate []", "terminate specified instance or first available" 84 | def terminate(id=nil) 85 | inst = sumo.find(id) || (sumo.running | sumo.pending).first || abort("No running or pending instances") 86 | 87 | sumo.terminate(inst[:instance_id]) 88 | puts "#{inst[:hostname] || inst[:instance_id]} scheduled for termination" 89 | end 90 | 91 | desc "terminate_all", "terminate all instances" 92 | def terminate_all 93 | instances = (sumo.running | sumo.pending) 94 | abort("No running or pending instances") if instances.empty? 95 | instances.each do |inst| 96 | sumo.terminate(inst[:instance_id]) 97 | puts "#{inst[:hostname] || inst[:instance_id]} scheduled for termination" 98 | end 99 | end 100 | 101 | desc "volumes", "list all volumes" 102 | def volumes 103 | sumo.volumes.each do |v| 104 | printf "%-10s %4sGB %10s %15s %15s\n", v[:volume_id], v[:size], v[:status], v[:instance], v[:device] 105 | end 106 | end 107 | 108 | desc "create_volume []", "create a volume" 109 | def create_volume(size=5) 110 | task("Create #{size}GB volume") { sumo.create_volume(size) } 111 | end 112 | 113 | desc "destroy_volume []", "destroy a volume" 114 | def destroy_volume(volume=nil) 115 | vol_id = (sumo.find_volume(volume) || sumo.nondestroyed_volumes.first || abort("No volumes"))[:volume_id] 116 | task("Destroy volume") { sumo.destroy_volume(vol_id) } 117 | end 118 | 119 | desc "attach [] [] []", "attach volume to running instance" 120 | def attach(volume=nil, inst_id=nil, device=nil) 121 | vol_id = (sumo.find_volume(volume) || sumo.available_volumes.first || abort("No available volumes"))[:volume_id] 122 | inst_id = (sumo.find(inst_id) || sumo.running.first || abort("No running instances"))[:instance_id] 123 | device ||= '/dev/sdc1' 124 | task("Attach #{vol_id} to #{inst_id} as #{device}") do 125 | sumo.attach(vol_id, inst_id, device) 126 | end 127 | end 128 | 129 | desc "detach []", "detach volume from instance" 130 | def detach(volume=nil) 131 | vol_id = (sumo.find_volume(volume) || sumo.attached_volumes.first || abort("No attached volumes"))[:volume_id] 132 | task("Detach #{vol_id}") { sumo.detach(vol_id) } 133 | end 134 | 135 | no_tasks do 136 | def sumo 137 | @sumo ||= Sumo.new 138 | end 139 | 140 | def config 141 | sumo.config 142 | end 143 | 144 | def task(msg, &block) 145 | printf "---> %-24s ", "#{msg}..." 146 | start = Time.now 147 | result = block.call || 'done' 148 | finish = Time.now 149 | time = sprintf("%0.1f", finish - start) 150 | puts "#{result} (#{time}s)" 151 | result 152 | end 153 | 154 | def connect_ssh(hostname) 155 | sumo.wait_for_ssh(hostname) 156 | system "ssh -i #{sumo.keypair_file} #{config['user']}@#{hostname}" 157 | if $?.success? 158 | puts "\nType 'sumo terminate' if you're done with this instance." 159 | end 160 | end 161 | 162 | def display_resources(host) 163 | resources = sumo.resources(host) 164 | unless resources.empty? 165 | puts "Your instance is exporting the following resources:" 166 | resources.each do |resource| 167 | puts " #{resource}" 168 | end 169 | end 170 | end 171 | end 172 | end 173 | 174 | CLI.start 175 | -------------------------------------------------------------------------------- /lib/sumo.rb: -------------------------------------------------------------------------------- 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 | volume[:volume_id] == volume_id or 115 | volume[: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( 267 | :access_key_id => config['access_id'], 268 | :secret_access_key => config['access_secret'], 269 | :server => server 270 | ) 271 | end 272 | 273 | def server 274 | zone = config['availability_zone'] 275 | host = zone.slice(0, zone.length - 1) 276 | "#{host}.ec2.amazonaws.com" 277 | end 278 | end 279 | -------------------------------------------------------------------------------- /spec/base.rb: -------------------------------------------------------------------------------- 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 22 | -------------------------------------------------------------------------------- /spec/sumo_spec.rb: -------------------------------------------------------------------------------- 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 32 | -------------------------------------------------------------------------------- /sumo.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | Gem::Specification.new do |s| 4 | s.name = %q{sumo} 5 | s.version = "0.2.3" 6 | 7 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 8 | s.authors = ["Adam Wiggins"] 9 | s.date = %q{2009-11-16} 10 | s.default_executable = %q{sumo} 11 | s.description = %q{A no-hassle way to launch one-off EC2 instances from the command line} 12 | s.email = %q{adam@heroku.com} 13 | s.executables = ["sumo"] 14 | s.extra_rdoc_files = [ 15 | "README.rdoc" 16 | ] 17 | s.files = [ 18 | "README.rdoc", 19 | "Rakefile", 20 | "VERSION", 21 | "bin/sumo", 22 | "lib/sumo.rb", 23 | "spec/base.rb", 24 | "spec/sumo_spec.rb" 25 | ] 26 | s.homepage = %q{http://github.com/adamwiggins/sumo} 27 | s.rdoc_options = ["--charset=UTF-8"] 28 | s.require_paths = ["lib"] 29 | s.rubyforge_project = %q{sumo} 30 | s.rubygems_version = %q{1.3.5} 31 | s.summary = %q{A no-hassle way to launch one-off EC2 instances from the command line} 32 | s.test_files = [ 33 | "spec/base.rb", 34 | "spec/sumo_spec.rb" 35 | ] 36 | 37 | if s.respond_to? :specification_version then 38 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 39 | s.specification_version = 3 40 | 41 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then 42 | s.add_runtime_dependency(%q, [">= 0"]) 43 | s.add_runtime_dependency(%q, [">= 0"]) 44 | else 45 | s.add_dependency(%q, [">= 0"]) 46 | s.add_dependency(%q, [">= 0"]) 47 | end 48 | else 49 | s.add_dependency(%q, [">= 0"]) 50 | s.add_dependency(%q, [">= 0"]) 51 | end 52 | end 53 | --------------------------------------------------------------------------------