stem 0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +22 -0
- data/README.md +44 -0
- data/bin/stem +13 -0
- data/lib/stem/cli.rb +91 -0
- data/lib/stem/image.rb +25 -0
- data/lib/stem/instance.rb +111 -0
- data/lib/stem/ip.rb +19 -0
- data/lib/stem/util.rb +11 -0
- data/lib/stem.rb +11 -0
- metadata +82 -0
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2010 Peter van Hardenberg
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
#Stem
|
2
|
+
##EC2 made easy.
|
3
|
+
|
4
|
+
##Introduction
|
5
|
+
|
6
|
+
Stem is a thin, light-weight EC2 instance management library which abstracts the Amazon EC2 API and provides an intuitive interface for designing, launching, and managing running instances.
|
7
|
+
|
8
|
+
Stem is named after the model it encourages -- simple AMIs created on demand with many running copies derived from that.
|
9
|
+
|
10
|
+
##Usage
|
11
|
+
|
12
|
+
You can use Stem to manage your instances either from the commandline or directly via the library. You should create an instance which will serve as your "stem" and be converted into an AMI. Once you have tested this instance, create a snapshot of the instance, then use it by name to launch new instances with their own individual configuration.
|
13
|
+
|
14
|
+
Here's a simple example from the command line. Begin by launching the example prototype instance.
|
15
|
+
|
16
|
+
$ bin/stem launch chrysanthemum/postgres-prototype/config.json chrysanthemum/postgres-prototype/userdata.sh
|
17
|
+
|
18
|
+
The config.json file specifies which AMI to start from, and what kind of EBS drive configuration to use. It is important that the drives are specified in the configuration file as any drives attached to the instance after launch will not become part of the eventual AMI you are creating
|
19
|
+
|
20
|
+
You can monitor the instance's fabrication process via
|
21
|
+
|
22
|
+
$ stem list
|
23
|
+
|
24
|
+
The instance you created will boot, install some packages on top of a stock Ubuntu 10.4 AMI, then (if everything goes according to plan) shut itself down and go into a "stopped" state that indicates success. If any part of the stem fabrication fails, the instance will remain running. Once the instance reaches stopped, type
|
25
|
+
|
26
|
+
$ stem create postgres-server <instance-id>
|
27
|
+
|
28
|
+
The AMI may take as long as half an hour to build, depending on how the gremlins in EC2 are behaving on any given day. You can check on their progress with
|
29
|
+
|
30
|
+
$ bin/amy
|
31
|
+
|
32
|
+
If the AMI fabrication reaches the state "failed" you will have to manually reissue the `create` command and hope that the gremlins are more forgiving the second time around.
|
33
|
+
|
34
|
+
Now that you have a simple postgres-server, you'll want to boot it up and create a database on it with some unique credentials! One of the simplest ways to solve this problem is to provide the instance with a templated userdata script which will perform per-instance configuration. I like mustache for this purpose.
|
35
|
+
|
36
|
+
$ mustache userdata.sh.yaml postgres-server/userdata.sh.mustache > postgres-server/userdata.sh
|
37
|
+
$ stem launch postgres-server/config.json postgres-server/userdata.sh
|
38
|
+
|
39
|
+
You can, of course, delete the produced userdata.sh once the instance is launched.
|
40
|
+
|
41
|
+
##Inspiration and Thanks
|
42
|
+
|
43
|
+
Stem is almost entirely based on Orion Henry's Judo gem, and Blake Mizerany's work on variously patton, carealot, napkin, and several other experiments. Thanks also for feedback, testing and patches from Adam Wiggins, Mark McGranahan, Noah Zoschke, and Jason Dusek.
|
44
|
+
|
data/bin/stem
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$:.unshift File.dirname(__FILE__) + "/../lib"
|
3
|
+
require 'stem'
|
4
|
+
|
5
|
+
# Help is the default.
|
6
|
+
ARGV << '-h' if ARGV.empty? && $stdin.tty?
|
7
|
+
|
8
|
+
# Process options
|
9
|
+
Stem::CLI.parse_options(ARGV) if $stdin.tty?
|
10
|
+
|
11
|
+
# Still here - run the command
|
12
|
+
Stem::CLI.dispatch_command(ARGV.shift, ARGV)
|
13
|
+
|
data/lib/stem/cli.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Stem
|
4
|
+
Version = 0.2
|
5
|
+
|
6
|
+
module CLI
|
7
|
+
extend self
|
8
|
+
|
9
|
+
# Return a structure describing the options.
|
10
|
+
def parse_options(args)
|
11
|
+
opts = OptionParser.new do |opts|
|
12
|
+
opts.banner = "Usage: stem COMMAND ..."
|
13
|
+
|
14
|
+
opts.separator " "
|
15
|
+
|
16
|
+
opts.separator "Examples:"
|
17
|
+
opts.separator " $ stem launch prototype.config prototype-userdata.sh"
|
18
|
+
opts.separator " $ stem list"
|
19
|
+
opts.separator " $ stem create ami-name instance-id"
|
20
|
+
|
21
|
+
opts.separator " "
|
22
|
+
opts.separator "Options:"
|
23
|
+
|
24
|
+
opts.on("-v", "--version", "Print the version") do |v|
|
25
|
+
puts "Stem v#{Stem::Version}"
|
26
|
+
exit
|
27
|
+
end
|
28
|
+
|
29
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
30
|
+
puts opts
|
31
|
+
exit
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
opts.separator ""
|
36
|
+
|
37
|
+
opts.parse!(args)
|
38
|
+
end
|
39
|
+
|
40
|
+
def dispatch_command command, arguments
|
41
|
+
case command
|
42
|
+
when "launch"
|
43
|
+
launch(*arguments)
|
44
|
+
when "create"
|
45
|
+
create(*arguments)
|
46
|
+
when "list"
|
47
|
+
list(*arguments)
|
48
|
+
when "describe"
|
49
|
+
describe(*arguments)
|
50
|
+
when "destroy"
|
51
|
+
destroy(*arguments)
|
52
|
+
when nil
|
53
|
+
puts "Please provide a command."
|
54
|
+
else
|
55
|
+
puts "Command \"#{command}\" not recognized."
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def launch config_file = nil, userdata_file = nil
|
60
|
+
abort "No config file" unless config_file
|
61
|
+
userdata = File.new(userdata_file).read() if userdata_file
|
62
|
+
instance = Stem::Instance::launch(JSON.parse(File.new(config_file).read()), userdata)
|
63
|
+
puts "New instance ID: #{instance}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def create name = nil, instance = nil
|
67
|
+
abort "Usage: create ami-name instance-to-capture" unless name && instance
|
68
|
+
image_id = Stem::Image::create(name, instance)
|
69
|
+
puts "New image ID: #{image_id}"
|
70
|
+
end
|
71
|
+
|
72
|
+
def describe what
|
73
|
+
require 'pp'
|
74
|
+
if (what[0..2] == "ami")
|
75
|
+
pp Stem::Image::describe(what)
|
76
|
+
elsif
|
77
|
+
pp Stem::Instance::describe(what)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def destroy instance = nil
|
82
|
+
abort "Usage: destroy instance-id" unless instance
|
83
|
+
Stem::Instance::destroy(instance)
|
84
|
+
end
|
85
|
+
|
86
|
+
def list *arguments
|
87
|
+
Stem::Instance::list(*arguments)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
data/lib/stem/image.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module Stem
|
2
|
+
module Image
|
3
|
+
include Util
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def create name, instance
|
7
|
+
description = {}
|
8
|
+
swirl.call("CreateImage", "InstanceId" => instance, "Name" => name, "Description" => "%%" + description.to_json)["imageId"]
|
9
|
+
end
|
10
|
+
|
11
|
+
def deregister image
|
12
|
+
swirl.call("DeregisterImage", "ImageId" => image)["return"]
|
13
|
+
end
|
14
|
+
|
15
|
+
def named name
|
16
|
+
i = swirl.call "DescribeImages", "Owner" => "self"
|
17
|
+
ami = i["imagesSet"].select {|m| m["name"] == name }.map { |m| m["imageId"] }.first
|
18
|
+
end
|
19
|
+
|
20
|
+
def describe image
|
21
|
+
swirl.call("DescribeImages", "ImageId" => image)["imagesSet"][0]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module Stem
|
2
|
+
module Instance
|
3
|
+
include Util
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def launch config, userdata = nil
|
7
|
+
throw "No config provided" unless config
|
8
|
+
|
9
|
+
ami = nil
|
10
|
+
if config["ami"]
|
11
|
+
ami = config["ami"]
|
12
|
+
elsif config["ami-name"]
|
13
|
+
ami = Image::named(config["ami-name"])
|
14
|
+
throw "AMI named #{config["ami-name"]} was not found. (Does it need creating?)" unless ami
|
15
|
+
end
|
16
|
+
throw "No AMI specified." unless ami
|
17
|
+
|
18
|
+
opt = {
|
19
|
+
"MinCount" => "1",
|
20
|
+
"MaxCount" => "1",
|
21
|
+
"KeyName" => config["key_name"] || "default",
|
22
|
+
"InstanceType" => config["instance_type"] || "m1.small",
|
23
|
+
"ImageId" => ami
|
24
|
+
}
|
25
|
+
if config["availability_zone"]
|
26
|
+
opt.merge! "Placement.AvailabilityZone" => config["availability_zone"]
|
27
|
+
end
|
28
|
+
|
29
|
+
if config["volumes"]
|
30
|
+
devices = []
|
31
|
+
sizes = []
|
32
|
+
config["volumes"].each do |v|
|
33
|
+
puts "Adding a volume of #{v["size"]} to be mounted at #{v["device"]}."
|
34
|
+
devices << v["device"]
|
35
|
+
sizes << v["size"].to_s
|
36
|
+
end
|
37
|
+
|
38
|
+
opt.merge! "BlockDeviceMapping.#.Ebs.VolumeSize" => sizes,
|
39
|
+
"BlockDeviceMapping.#.DeviceName" => devices
|
40
|
+
end
|
41
|
+
|
42
|
+
if userdata
|
43
|
+
puts "Userdata provided, encoded and sent to the instance."
|
44
|
+
opt.merge!({ "UserData" => Base64.encode64(userdata)})
|
45
|
+
end
|
46
|
+
|
47
|
+
response = swirl.call "RunInstances", opt
|
48
|
+
|
49
|
+
puts "Success!"
|
50
|
+
response["instancesSet"].each do |i|
|
51
|
+
return i["instanceId"]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
def restart instance_id
|
55
|
+
swirl.call "RebootInstances", "InstanceId" => instance_id
|
56
|
+
end
|
57
|
+
|
58
|
+
def destroy instance_id
|
59
|
+
swirl.call "TerminateInstances", "InstanceId" => instance_id
|
60
|
+
end
|
61
|
+
|
62
|
+
def stop instance_id
|
63
|
+
swirl.call "StopInstances", "InstanceId" => instance_id
|
64
|
+
end
|
65
|
+
|
66
|
+
def describe instance
|
67
|
+
throw "You must provide an instance ID to describe" unless instance
|
68
|
+
swirl.call("DescribeInstances", "InstanceId" => instance)["reservationSet"][0]["instancesSet"][0]
|
69
|
+
end
|
70
|
+
|
71
|
+
def list
|
72
|
+
instances = swirl.call("DescribeInstances")
|
73
|
+
|
74
|
+
lookup = {}
|
75
|
+
instances["reservationSet"].each do |r|
|
76
|
+
r["instancesSet"].each do |i|
|
77
|
+
lookup[i["imageId"]] = nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
amis = swirl.call("DescribeImages", "ImageId" => lookup.keys)["imagesSet"]
|
81
|
+
|
82
|
+
amis.each do |ami|
|
83
|
+
name = ami["name"] || ami["imageId"]
|
84
|
+
if !ami["description"] || ami["description"][0..1] != "%%"
|
85
|
+
# only truncate ugly names from other people (never truncate ours)
|
86
|
+
name.gsub!(/^(.{8}).+(.{8})/) { $1 + "..." + $2 }
|
87
|
+
name = "(foreign) " + name
|
88
|
+
end
|
89
|
+
lookup[ami["imageId"]] = name
|
90
|
+
end
|
91
|
+
|
92
|
+
puts "------------------------------------------"
|
93
|
+
puts "Instances"
|
94
|
+
puts "------------------------------------------"
|
95
|
+
instances["reservationSet"].each do |r|
|
96
|
+
r["instancesSet"].each do |i|
|
97
|
+
name = lookup[i["imageId"]]
|
98
|
+
puts "%-15s %-15s %-15s %s" % [ i["instanceId"], i["ipAddress"] || "no ip", i["instanceState"]["name"], name
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
puts "------------------------------------------"
|
103
|
+
puts "AMIs"
|
104
|
+
puts "------------------------------------------"
|
105
|
+
images = c.call "DescribeImages", "Owner" => "self"
|
106
|
+
images["imagesSet"].each do
|
107
|
+
puts "%-15s %s" % [ img["name"], img["imageId"] ]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
data/lib/stem/ip.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module Stem
|
2
|
+
module Ip
|
3
|
+
include Util
|
4
|
+
|
5
|
+
extend self
|
6
|
+
def allocate
|
7
|
+
swirl.call("AllocateAddress")["publicIp"]
|
8
|
+
end
|
9
|
+
|
10
|
+
def associate ip, instance
|
11
|
+
result = swirl.call("AssociateAddress", "InstanceId" => instance, "PublicIp" => ip)["return"]
|
12
|
+
result == "true"
|
13
|
+
end
|
14
|
+
|
15
|
+
def release ip
|
16
|
+
result = swirl.call("ReleaseAddress", "PublicIp" => ip)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/stem/util.rb
ADDED
data/lib/stem.rb
ADDED
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: stem
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 2
|
8
|
+
version: "0.2"
|
9
|
+
platform: ruby
|
10
|
+
authors:
|
11
|
+
- Peter van Hardenberg
|
12
|
+
autorequire:
|
13
|
+
bindir: bin
|
14
|
+
cert_chain: []
|
15
|
+
|
16
|
+
date: 2010-08-12 00:00:00 -07:00
|
17
|
+
default_executable:
|
18
|
+
dependencies:
|
19
|
+
- !ruby/object:Gem::Dependency
|
20
|
+
name: swirl
|
21
|
+
prerelease: false
|
22
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
segments:
|
27
|
+
- 1
|
28
|
+
- 5
|
29
|
+
- 2
|
30
|
+
version: 1.5.2
|
31
|
+
type: :runtime
|
32
|
+
version_requirements: *id001
|
33
|
+
description: minimalist EC2 instance management
|
34
|
+
email:
|
35
|
+
- pvh@heroku.com
|
36
|
+
executables:
|
37
|
+
- stem
|
38
|
+
extensions: []
|
39
|
+
|
40
|
+
extra_rdoc_files: []
|
41
|
+
|
42
|
+
files:
|
43
|
+
- LICENSE
|
44
|
+
- README.md
|
45
|
+
- lib/stem/cli.rb
|
46
|
+
- lib/stem/image.rb
|
47
|
+
- lib/stem/instance.rb
|
48
|
+
- lib/stem/ip.rb
|
49
|
+
- lib/stem/util.rb
|
50
|
+
- lib/stem.rb
|
51
|
+
has_rdoc: true
|
52
|
+
homepage: http://github.com/pvh/stem
|
53
|
+
licenses: []
|
54
|
+
|
55
|
+
post_install_message:
|
56
|
+
rdoc_options: []
|
57
|
+
|
58
|
+
require_paths:
|
59
|
+
- lib
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
segments:
|
65
|
+
- 0
|
66
|
+
version: "0"
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
segments:
|
72
|
+
- 0
|
73
|
+
version: "0"
|
74
|
+
requirements: []
|
75
|
+
|
76
|
+
rubyforge_project:
|
77
|
+
rubygems_version: 1.3.6
|
78
|
+
signing_key:
|
79
|
+
specification_version: 2
|
80
|
+
summary: an EC2 instance management library designed to get out of your way and give you instances
|
81
|
+
test_files: []
|
82
|
+
|