cardmagic-sumo 0.3.0

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.
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