sumo 0.1.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 (6) hide show
  1. data/README.rdoc +59 -0
  2. data/Rakefile +16 -0
  3. data/VERSION +1 -0
  4. data/bin/sumo +129 -0
  5. data/lib/sumo.rb +192 -0
  6. metadata +57 -0
data/README.rdoc ADDED
@@ -0,0 +1,59 @@
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 also manage multiple instances via "sumo list" and specifying hostname or instance id as arguments to the launch, ssh, and terminate commands.
21
+
22
+ == Setup
23
+
24
+ Dependencies:
25
+
26
+ $ sudo gem install amazon-ec2 thor
27
+
28
+ Then create ~/.sumo/config.yml containing:
29
+
30
+ ---
31
+ access_id: <your amazon access key id>
32
+ access_secret: <your amazon secret access key>
33
+ ami: ami-ed46a784
34
+
35
+ 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.
36
+
37
+ == Features
38
+
39
+ Launch, ssh to, and terminate instances.
40
+
41
+ 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.
42
+
43
+ == Anti-features
44
+
45
+ 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.
46
+
47
+ * Pool Party
48
+ * RightScale
49
+ * Engine Yard Cloud
50
+ * Cloudkick
51
+
52
+ == Meta
53
+
54
+ Created by Adam Wiggins
55
+
56
+ Released under the MIT License: http://www.opensource.org/licenses/mit-license.php
57
+
58
+ http://github.com/adamwiggins/sumo
59
+
data/Rakefile ADDED
@@ -0,0 +1,16 @@
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
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/bin/sumo ADDED
@@ -0,0 +1,129 @@
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("Opening 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 "terminate [<instance_id or hostname>]", "terminate specified instance or first available"
76
+ def terminate(id=nil)
77
+ inst = sumo.find(id) || (sumo.running | sumo.pending).first || abort("No running or pending instances")
78
+
79
+ sumo.terminate(inst[:instance_id])
80
+ puts "#{inst[:hostname] || inst[:instance_id]} scheduled for termination"
81
+ end
82
+
83
+ desc "terminate_all", "terminate all instances"
84
+ def terminate_all
85
+ instances = (sumo.running | sumo.pending)
86
+ abort("No running or pending instances") if instances.empty?
87
+ instances.each do |inst|
88
+ sumo.terminate(inst[:instance_id])
89
+ puts "#{inst[:hostname] || inst[:instance_id]} scheduled for termination"
90
+ end
91
+ end
92
+
93
+ no_tasks do
94
+ def sumo
95
+ @sumo ||= Sumo.new
96
+ end
97
+
98
+ def task(msg, &block)
99
+ printf "---> %-24s ", "#{msg}..."
100
+ start = Time.now
101
+ result = block.call || 'done'
102
+ finish = Time.now
103
+ time = sprintf("%0.1f", finish - start)
104
+ puts "#{result} (#{time}s)"
105
+ result
106
+ end
107
+
108
+ def connect_ssh(hostname)
109
+ sumo.wait_for_ssh(hostname)
110
+
111
+ system "ssh -i #{sumo.keypair_file} root@#{hostname}"
112
+ if $?.success?
113
+ puts "\nType 'sumo terminate' if you're done with this instance."
114
+ end
115
+ end
116
+
117
+ def display_resources(host)
118
+ resources = sumo.resources(host)
119
+ unless resources.empty?
120
+ puts "Your instance is exporting the following resources:"
121
+ resources.each do |resource|
122
+ puts " #{resource}"
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ CLI.start
data/lib/sumo.rb ADDED
@@ -0,0 +1,192 @@
1
+ require 'ec2'
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
+ )
21
+ result.instancesSet.item[0].instanceId
22
+ end
23
+
24
+ def list
25
+ @list ||= fetch_list
26
+ end
27
+
28
+ def fetch_list
29
+ result = ec2.describe_instances
30
+ return [] unless result.reservationSet
31
+
32
+ instances = []
33
+ result.reservationSet.item.each do |r|
34
+ r.instancesSet.item.each do |item|
35
+ instances << {
36
+ :instance_id => item.instanceId,
37
+ :status => item.instanceState.name,
38
+ :hostname => item.dnsName
39
+ }
40
+ end
41
+ end
42
+ instances
43
+ end
44
+
45
+ def find(id_or_hostname)
46
+ return unless id_or_hostname
47
+ id_or_hostname = id_or_hostname.strip.downcase
48
+ list.detect do |inst|
49
+ inst[:hostname] == id_or_hostname or
50
+ inst[:instance_id] == id_or_hostname or
51
+ inst[:instance_id].gsub(/^i-/, '') == id_or_hostname
52
+ end
53
+ end
54
+
55
+ def running
56
+ list_by_status('running')
57
+ end
58
+
59
+ def pending
60
+ list_by_status('pending')
61
+ end
62
+
63
+ def list_by_status(status)
64
+ list.select { |i| i[:status] == status }
65
+ end
66
+
67
+ def instance_info(instance_id)
68
+ fetch_list.detect do |inst|
69
+ inst[:instance_id] == instance_id
70
+ end
71
+ end
72
+
73
+ def wait_for_hostname(instance_id)
74
+ raise ArgumentError unless instance_id and instance_id.match(/^i-/)
75
+ loop do
76
+ if inst = instance_info(instance_id)
77
+ if hostname = inst[:hostname]
78
+ return hostname
79
+ end
80
+ end
81
+ sleep 1
82
+ end
83
+ end
84
+
85
+ def wait_for_ssh(hostname)
86
+ raise ArgumentError unless hostname
87
+ loop do
88
+ begin
89
+ Timeout::timeout(4) do
90
+ TCPSocket.new(hostname, 22)
91
+ return
92
+ end
93
+ rescue SocketError, Timeout::Error, Errno::ECONNREFUSED, Errno::EHOSTUNREACH
94
+ end
95
+ end
96
+ end
97
+
98
+ def bootstrap_chef(hostname)
99
+ commands = [
100
+ 'apt-get update',
101
+ 'apt-get autoremove -y',
102
+ 'apt-get install -y ruby ruby-dev rubygems git-core',
103
+ 'gem sources -a http://gems.opscode.com',
104
+ 'gem install chef ohai --no-rdoc --no-ri',
105
+ "git clone #{config['cookbooks_url']}",
106
+ ]
107
+ ssh(hostname, commands)
108
+ end
109
+
110
+ def setup_role(hostname, role)
111
+ commands = [
112
+ "cd chef-cookbooks",
113
+ "/var/lib/gems/1.8/bin/chef-solo -c config.json -j roles/#{role}.json"
114
+ ]
115
+ ssh(hostname, commands)
116
+ end
117
+
118
+ def ssh(hostname, cmds)
119
+ IO.popen("ssh -i #{keypair_file} root@#{hostname} > ~/.sumo/ssh.log 2>&1", "w") do |pipe|
120
+ pipe.puts cmds.join(' && ')
121
+ end
122
+ unless $?.success?
123
+ abort "failed\nCheck ~/.sumo/ssh.log for the output"
124
+ end
125
+ end
126
+
127
+ def resources(hostname)
128
+ @resources ||= {}
129
+ @resources[hostname] ||= fetch_resources(hostname)
130
+ end
131
+
132
+ def fetch_resources(hostname)
133
+ cmd = "ssh -i #{keypair_file} root@#{hostname} 'cat /root/resources' 2>&1"
134
+ out = IO.popen(cmd, 'r') { |pipe| pipe.read }
135
+ abort "failed to read resources, output:\n#{out}" unless $?.success?
136
+ parse_resources(out, hostname)
137
+ end
138
+
139
+ def parse_resources(raw, hostname)
140
+ raw.split("\n").map do |line|
141
+ line.gsub(/localhost/, hostname)
142
+ end
143
+ end
144
+
145
+ def terminate(instance_id)
146
+ ec2.terminate_instances(:instance_id => [ instance_id ])
147
+ end
148
+
149
+ def config
150
+ @config ||= read_config
151
+ end
152
+
153
+ def sumo_dir
154
+ "#{ENV['HOME']}/.sumo"
155
+ end
156
+
157
+ def read_config
158
+ YAML.load File.read("#{sumo_dir}/config.yml")
159
+ rescue Errno::ENOENT
160
+ raise "Sumo is not configured, please fill in ~/.sumo/config.yml"
161
+ end
162
+
163
+ def keypair_file
164
+ "#{sumo_dir}/keypair.pem"
165
+ end
166
+
167
+ def create_keypair
168
+ keypair = ec2.create_keypair(:key_name => "sumo").keyMaterial
169
+ File.open(keypair_file, 'w') { |f| f.write keypair }
170
+ File.chmod 0600, keypair_file
171
+ end
172
+
173
+ def create_security_group
174
+ ec2.create_security_group(:group_name => 'sumo', :group_description => 'Sumo')
175
+ rescue EC2::InvalidGroupDuplicate
176
+ end
177
+
178
+ def open_firewall(port)
179
+ ec2.authorize_security_group_ingress(
180
+ :group_name => 'sumo',
181
+ :ip_protocol => 'tcp',
182
+ :from_port => port,
183
+ :to_port => port,
184
+ :cidr_ip => '0.0.0.0/0'
185
+ )
186
+ rescue EC2::InvalidPermissionDuplicate
187
+ end
188
+
189
+ def ec2
190
+ @ec2 ||= EC2::Base.new(:access_key_id => config['access_id'], :secret_access_key => config['access_secret'])
191
+ end
192
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sumo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Adam Wiggins
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-08-19 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
+ has_rdoc: true
31
+ homepage: http://github.com/adamwiggins/sumo
32
+ post_install_message:
33
+ rdoc_options:
34
+ - --charset=UTF-8
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: "0"
42
+ version:
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: "0"
48
+ version:
49
+ requirements: []
50
+
51
+ rubyforge_project: sumo
52
+ rubygems_version: 1.3.1
53
+ signing_key:
54
+ specification_version: 2
55
+ summary: A no-hassle way to launch one-off EC2 instances from the command line
56
+ test_files: []
57
+