cardmagic-sumo 0.3.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 +123 -0
  2. data/Rakefile +25 -0
  3. data/VERSION +1 -0
  4. data/bin/sumo +174 -0
  5. data/lib/sumo.rb +326 -0
  6. data/spec/base.rb +21 -0
  7. data/spec/sumo_spec.rb +31 -0
  8. metadata +101 -0
data/README.rdoc ADDED
@@ -0,0 +1,123 @@
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, 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
+
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ require 'jeweler'
2
+
3
+ Jeweler::Tasks.new do |s|
4
+ s.name = "cardmagic-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/cardmagic/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
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.3.0
data/bin/sumo ADDED
@@ -0,0 +1,174 @@
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 [<role>]", "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 [<instance_id or hostname>]", "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 [<instance_id or hostname>]", "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 [<instance_id or hostname>]", "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 [<instance_id or hostname>]", "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 [<megabytes>]", "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 [<volume_id>]", "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 [<volume_id>] [<instance_id or hostname>] [<device>]", "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 [<volume_id>]", "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
data/lib/sumo.rb ADDED
@@ -0,0 +1,326 @@
1
+ require 'AWS'
2
+ require 'yaml'
3
+ require 'socket'
4
+ require 'net/ssh'
5
+
6
+ class Sumo
7
+ def launch
8
+ ami = config['ami']
9
+ raise "No AMI selected" unless ami
10
+
11
+ create_keypair unless File.exists? keypair_file
12
+
13
+ create_security_group
14
+ open_firewall(22)
15
+
16
+ result = ec2.run_instances(
17
+ :image_id => ami,
18
+ :instance_type => config['instance_size'] || 'm1.small',
19
+ :key_name => 'sumo',
20
+ :security_group => [ 'sumo' ],
21
+ :availability_zone => config['availability_zone']
22
+ )
23
+ result.instancesSet.item[0].instanceId
24
+ end
25
+
26
+ def list
27
+ @list ||= fetch_list
28
+ end
29
+
30
+ def volumes
31
+ result = ec2.describe_volumes
32
+ return [] unless result.volumeSet
33
+
34
+ result.volumeSet.item.map do |row|
35
+ {
36
+ :volume_id => row["volumeId"],
37
+ :size => row["size"],
38
+ :status => row["status"],
39
+ :device => (row["attachmentSet"]["item"].first["device"] rescue ""),
40
+ :instance_id => (row["attachmentSet"]["item"].first["instanceId"] rescue ""),
41
+ }
42
+ end
43
+ end
44
+
45
+ def available_volumes
46
+ volumes.select { |vol| vol[:status] == 'available' }
47
+ end
48
+
49
+ def attached_volumes
50
+ volumes.select { |vol| vol[:status] == 'in-use' }
51
+ end
52
+
53
+ def nondestroyed_volumes
54
+ volumes.select { |vol| vol[:status] != 'deleting' }
55
+ end
56
+
57
+ def attach(volume, instance, device)
58
+ result = ec2.attach_volume(
59
+ :volume_id => volume,
60
+ :instance_id => instance,
61
+ :device => device
62
+ )
63
+ "done"
64
+ end
65
+
66
+ def detach(volume)
67
+ result = ec2.detach_volume(:volume_id => volume, :force => "true")
68
+ "done"
69
+ end
70
+
71
+ def create_volume(size)
72
+ result = ec2.create_volume(
73
+ :availability_zone => config['availability_zone'],
74
+ :size => size.to_s
75
+ )
76
+ result["volumeId"]
77
+ end
78
+
79
+ def destroy_volume(volume)
80
+ ec2.delete_volume(:volume_id => volume)
81
+ "done"
82
+ end
83
+
84
+ def fetch_list
85
+ result = ec2.describe_instances
86
+ return [] unless result.reservationSet
87
+
88
+ instances = []
89
+ result.reservationSet.item.each do |r|
90
+ r.instancesSet.item.each do |item|
91
+ instances << {
92
+ :instance_id => item.instanceId,
93
+ :status => item.instanceState.name,
94
+ :hostname => item.dnsName
95
+ }
96
+ end
97
+ end
98
+ instances
99
+ end
100
+
101
+ def find(id_or_hostname)
102
+ return unless id_or_hostname
103
+ id_or_hostname = id_or_hostname.strip.downcase
104
+ list.detect do |inst|
105
+ inst[:hostname] == id_or_hostname or
106
+ inst[:instance_id] == id_or_hostname or
107
+ inst[:instance_id].gsub(/^i-/, '') == id_or_hostname
108
+ end
109
+ end
110
+
111
+ def find_volume(volume_id)
112
+ return unless volume_id
113
+ volume_id = volume_id.strip.downcase
114
+ volumes.detect do |volume|
115
+ volume[:volume_id] == volume_id or
116
+ volume[:volume_id].gsub(/^vol-/, '') == volume_id
117
+ end
118
+ end
119
+
120
+ def running
121
+ list_by_status('running')
122
+ end
123
+
124
+ def pending
125
+ list_by_status('pending')
126
+ end
127
+
128
+ def list_by_status(status)
129
+ list.select { |i| i[:status] == status }
130
+ end
131
+
132
+ def instance_info(instance_id)
133
+ fetch_list.detect do |inst|
134
+ inst[:instance_id] == instance_id
135
+ end
136
+ end
137
+
138
+ def wait_for_hostname(instance_id)
139
+ raise ArgumentError unless instance_id and instance_id.match(/^i-/)
140
+ loop do
141
+ if inst = instance_info(instance_id)
142
+ if hostname = inst[:hostname]
143
+ return hostname
144
+ end
145
+ end
146
+ sleep 1
147
+ end
148
+ end
149
+
150
+ def wait_for_ssh(hostname)
151
+ raise ArgumentError unless hostname
152
+ loop do
153
+ begin
154
+ Timeout::timeout(4) do
155
+ TCPSocket.new(hostname, 22)
156
+ return
157
+ end
158
+ rescue SocketError, Timeout::Error, Errno::ECONNREFUSED, Errno::EHOSTUNREACH
159
+ end
160
+ end
161
+ end
162
+
163
+ def bootstrap_chef(hostname)
164
+ commands = [
165
+ 'apt-get update',
166
+ 'apt-get autoremove -y',
167
+ 'apt-get install -y ruby ruby-dev rubygems git-core',
168
+ 'gem sources -a http://gems.opscode.com',
169
+ 'gem install chef ohai --no-rdoc --no-ri',
170
+ config['cookbooks_url'] ? "git clone #{config['cookbooks_url']} chef-cookbooks" : "echo done",
171
+ ]
172
+ ssh(hostname, commands)
173
+ if config['cookbooks_dir']
174
+ scp(hostname, config['cookbooks_dir'], "chef-cookbooks")
175
+ end
176
+ end
177
+
178
+ def setup_role(hostname, role)
179
+ commands = [
180
+ "cd chef-cookbooks",
181
+ "/var/lib/gems/1.8/bin/chef-solo -c config.json -j dna/#{role}.json"
182
+ ]
183
+ ssh(hostname, commands)
184
+ end
185
+
186
+ def ssh(hostname, cmds)
187
+ unless IO.read(File.expand_path("~/.ssh/known_hosts")).include?(hostname)
188
+ `ssh-keyscan -t rsa #{hostname} >> $HOME/.ssh/known_hosts`
189
+ end
190
+ IO.popen("ssh -t -t -i #{keypair_file} #{config['user']}@#{hostname} > ~/.sumo/ssh.log 2>&1", "w") do |pipe|
191
+ pipe.puts cmds.join(' && ')
192
+ end
193
+ unless $?.success?
194
+ abort "failed\nCheck ~/.sumo/ssh.log for the output"
195
+ end
196
+ end
197
+
198
+ def scp(hostname, directory, endpoint=".")
199
+ `scp -i #{keypair_file} -r #{directory} #{config['user']}@#{hostname}:#{endpoint}`
200
+ unless $?.success?
201
+ abort "failed to transfer #{directory}"
202
+ end
203
+ end
204
+
205
+ def new_ssh(hostname, cmds)
206
+ Net::SSH.start(hostname, config['user'], :keys => [keypair_file], :compression => "none") do |ssh|
207
+ # capture all stderr and stdout output from a remote process
208
+
209
+ File.open(File.expand_path("~/.sumo/ssh.log"), "w") do |log|
210
+ ssh.open_channel do |channel|
211
+ cmds.each do |cmd|
212
+ channel.exec cmd do |ch, success|
213
+ abort "failed on #{cmd}\nCheck ~/.sumo/ssh.log for the output" unless success
214
+
215
+ channel.on_data do |ch, data|
216
+ puts "Got data #{data.inspect}"
217
+ log << data
218
+ log.flush
219
+ end
220
+
221
+ channel.on_extended_data do |ch, type, data|
222
+ puts "Got data #{data.inspect}"
223
+ log << data
224
+ log.flush
225
+ end
226
+
227
+ channel.on_close do |ch|
228
+ puts "channel is closing!"
229
+ end
230
+ end
231
+ end
232
+ end
233
+
234
+ ssh.loop
235
+ end
236
+ end
237
+ end
238
+
239
+ def resources(hostname)
240
+ @resources ||= {}
241
+ @resources[hostname] ||= fetch_resources(hostname)
242
+ end
243
+
244
+ def fetch_resources(hostname)
245
+ cmd = "ssh -i #{keypair_file} #{config['user']}@#{hostname} 'cat /root/resources' 2>&1"
246
+ out = IO.popen(cmd, 'r') { |pipe| pipe.read }
247
+ abort "failed to read resources, output:\n#{out}" unless $?.success?
248
+ parse_resources(out, hostname)
249
+ end
250
+
251
+ def parse_resources(raw, hostname)
252
+ raw.split("\n").map do |line|
253
+ line.gsub(/localhost/, hostname)
254
+ end
255
+ end
256
+
257
+ def terminate(instance_id)
258
+ ec2.terminate_instances(:instance_id => [ instance_id ])
259
+ end
260
+
261
+ def console_output(instance_id)
262
+ ec2.get_console_output(:instance_id => instance_id)["output"]
263
+ end
264
+
265
+ def config
266
+ @config ||= default_config.merge read_config
267
+ end
268
+
269
+ def default_config
270
+ {
271
+ 'user' => 'root',
272
+ 'ami' => 'ami-ed46a784',
273
+ 'availability_zone' => 'us-east-1b'
274
+ }
275
+ end
276
+
277
+ def sumo_dir
278
+ "#{ENV['HOME']}/.sumo"
279
+ end
280
+
281
+ def read_config
282
+ YAML.load File.read("#{sumo_dir}/config.yml")
283
+ rescue Errno::ENOENT
284
+ raise "Sumo is not configured, please fill in ~/.sumo/config.yml"
285
+ end
286
+
287
+ def keypair_file
288
+ "#{sumo_dir}/keypair.pem"
289
+ end
290
+
291
+ def create_keypair
292
+ keypair = ec2.create_keypair(:key_name => "sumo").keyMaterial
293
+ File.open(keypair_file, 'w') { |f| f.write keypair }
294
+ File.chmod 0600, keypair_file
295
+ end
296
+
297
+ def create_security_group
298
+ ec2.create_security_group(:group_name => 'sumo', :group_description => 'Sumo')
299
+ rescue AWS::InvalidGroupDuplicate
300
+ end
301
+
302
+ def open_firewall(port)
303
+ ec2.authorize_security_group_ingress(
304
+ :group_name => 'sumo',
305
+ :ip_protocol => 'tcp',
306
+ :from_port => port,
307
+ :to_port => port,
308
+ :cidr_ip => '0.0.0.0/0'
309
+ )
310
+ rescue AWS::InvalidPermissionDuplicate
311
+ end
312
+
313
+ def ec2
314
+ @ec2 ||= AWS::EC2::Base.new(
315
+ :access_key_id => config['access_id'],
316
+ :secret_access_key => config['access_secret'],
317
+ :server => server
318
+ )
319
+ end
320
+
321
+ def server
322
+ zone = config['availability_zone']
323
+ host = zone.slice(0, zone.length - 1)
324
+ "#{host}.ec2.amazonaws.com"
325
+ end
326
+ 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,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cardmagic-sumo
3
+ version: !ruby/object:Gem::Version
4
+ hash: 19
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 3
9
+ - 0
10
+ version: 0.3.0
11
+ platform: ruby
12
+ authors:
13
+ - Adam Wiggins
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-06-14 00:00:00 -07:00
19
+ default_executable: sumo
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: amazon-ec2
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: thor
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :runtime
48
+ version_requirements: *id002
49
+ description: A no-hassle way to launch one-off EC2 instances from the command line
50
+ email: adam@heroku.com
51
+ executables:
52
+ - sumo
53
+ extensions: []
54
+
55
+ extra_rdoc_files:
56
+ - README.rdoc
57
+ files:
58
+ - README.rdoc
59
+ - Rakefile
60
+ - VERSION
61
+ - bin/sumo
62
+ - lib/sumo.rb
63
+ - spec/base.rb
64
+ - spec/sumo_spec.rb
65
+ has_rdoc: true
66
+ homepage: http://github.com/cardmagic/sumo
67
+ licenses: []
68
+
69
+ post_install_message:
70
+ rdoc_options:
71
+ - --charset=UTF-8
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ hash: 3
80
+ segments:
81
+ - 0
82
+ version: "0"
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ hash: 3
89
+ segments:
90
+ - 0
91
+ version: "0"
92
+ requirements: []
93
+
94
+ rubyforge_project: sumo
95
+ rubygems_version: 1.3.7
96
+ signing_key:
97
+ specification_version: 3
98
+ summary: A no-hassle way to launch one-off EC2 instances from the command line
99
+ test_files:
100
+ - spec/base.rb
101
+ - spec/sumo_spec.rb