adamwiggins-sumo 0.2.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.
- data/README.rdoc +122 -0
- data/Rakefile +23 -0
- data/VERSION +1 -0
- data/bin/sumo +173 -0
- data/lib/sumo.rb +268 -0
- data/spec/base.rb +21 -0
- data/spec/sumo_spec.rb +31 -0
- 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
|