sumo 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+