stem 0.4.9 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/stem/family.rb +116 -0
- data/lib/stem/group.rb +5 -6
- data/lib/stem/image.rb +11 -2
- data/lib/stem/instance.rb +2 -12
- data/lib/stem/util.rb +58 -24
- data/lib/stem.rb +3 -0
- metadata +8 -5
data/lib/stem/family.rb
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
module Stem
|
2
|
+
module Family
|
3
|
+
include Util
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def ami_for(family, release, architecture = "x86_64")
|
7
|
+
amis = Stem::Image::tagged(:family => family,
|
8
|
+
:release => release,
|
9
|
+
:architecture => architecture)
|
10
|
+
throw "More than one AMI matched release." if amis.length > 1
|
11
|
+
amis[0]
|
12
|
+
end
|
13
|
+
|
14
|
+
def unrelease family, release_name
|
15
|
+
prev = Stem::Image::tagged(:family => family, :release => release_name)
|
16
|
+
prev.each { |ami| Stem::Tag::destroy(ami, :release => release_name) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def member? family, ami
|
20
|
+
desc = Stem::Image::describe(ami)
|
21
|
+
throw "AMI #{ami} does not exist" if desc.nil?
|
22
|
+
tagset_to_hash(desc["tagSet"])["family"] == family
|
23
|
+
end
|
24
|
+
|
25
|
+
def members family
|
26
|
+
Stem::Image.tagged("family" => family)
|
27
|
+
end
|
28
|
+
|
29
|
+
def describe_members family
|
30
|
+
Stem::Image.describe_tagged("family" => family)
|
31
|
+
end
|
32
|
+
|
33
|
+
def release family, release_name, *amis
|
34
|
+
amis.each do |ami|
|
35
|
+
throw "#{ami} not part of #{family}" unless member?(family, ami)
|
36
|
+
end
|
37
|
+
unrelease family, release_name
|
38
|
+
amis.each { |ami| Stem::Tag::create(ami, :release => release_name) }
|
39
|
+
end
|
40
|
+
|
41
|
+
def build_image(family, config, userdata)
|
42
|
+
log = lambda { |msg|
|
43
|
+
puts "[#{family}|#{Time.now.to_s}] #{msg}"
|
44
|
+
}
|
45
|
+
|
46
|
+
aggregate_hash_options_for_ami!(config)
|
47
|
+
sha1 = image_hash(config, userdata)
|
48
|
+
|
49
|
+
log.call "Beginning to build image for #{family}"
|
50
|
+
log.call "Config:\n------\n#{ config.inspect }\n-------"
|
51
|
+
instance_id = Stem::Instance.launch(config, userdata)
|
52
|
+
|
53
|
+
log.call "Booting #{instance_id} to produce your prototype instance"
|
54
|
+
wait_for_stopped instance_id, log
|
55
|
+
|
56
|
+
timestamp = Time.now.utc.iso8601
|
57
|
+
image_id = Stem::Image.create("#{family}-#{timestamp}",
|
58
|
+
instance_id,
|
59
|
+
{
|
60
|
+
:created => timestamp,
|
61
|
+
:family => family,
|
62
|
+
:sha1 => sha1,
|
63
|
+
:source_ami => config["ami"]
|
64
|
+
})
|
65
|
+
log.call "Image ID is #{image_id}"
|
66
|
+
|
67
|
+
wait_for_available(image_id, log)
|
68
|
+
|
69
|
+
log "Terminating #{instance_id} now that the image is captured"
|
70
|
+
Stem::Instance::destroy(instance_id)
|
71
|
+
end
|
72
|
+
|
73
|
+
def image_already_built?(family, config, userdata)
|
74
|
+
aggregate_hash_options_for_ami!(config)
|
75
|
+
sha1 = image_hash(config, userdata)
|
76
|
+
!Stem::Image.tagged(:family => family, :sha1 => sha1).empty?
|
77
|
+
end
|
78
|
+
|
79
|
+
def image_hash(config, userdata)
|
80
|
+
Digest::SHA1::hexdigest([config.to_s, userdata].join(' '))
|
81
|
+
end
|
82
|
+
|
83
|
+
protected
|
84
|
+
|
85
|
+
def wait_for_stopped(instance_id, log)
|
86
|
+
log.call "waiting for instance to reach state stopped -- "
|
87
|
+
while sleep 10
|
88
|
+
state = Stem::Instance.describe(instance_id)["instanceState"]["name"]
|
89
|
+
log.call "instance #{instance_id} #{state}"
|
90
|
+
break if state == "stopped"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def wait_for_available(image_id, log)
|
95
|
+
log.call "Waiting for image to finish capturing..."
|
96
|
+
while sleep 10
|
97
|
+
begin
|
98
|
+
state = Stem::Image.describe(image_id)["imageState"]
|
99
|
+
log.call "Image #{image_id} #{state}"
|
100
|
+
case state
|
101
|
+
when "available"
|
102
|
+
log.call("Image capturing succeeded")
|
103
|
+
break
|
104
|
+
when "pending" #continue
|
105
|
+
when "terminated"
|
106
|
+
log "Image capture failed (#{image_id})"
|
107
|
+
return false
|
108
|
+
else throw "Image unexpectedly entered #{state}";
|
109
|
+
end
|
110
|
+
rescue Swirl::InvalidRequest => e
|
111
|
+
raise unless e.message =~ /does not exist/
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/lib/stem/group.rb
CHANGED
@@ -22,17 +22,16 @@ module Stem
|
|
22
22
|
nil
|
23
23
|
end
|
24
24
|
|
25
|
-
def create(name, rules = nil)
|
26
|
-
create!(name, rules)
|
25
|
+
def create(name, rules = nil, description = "")
|
26
|
+
create!(name, rules, description)
|
27
27
|
true
|
28
28
|
rescue Swirl::InvalidRequest => e
|
29
29
|
raise e unless e.message =~ /The security group '\S+' already exists/
|
30
30
|
false
|
31
31
|
end
|
32
32
|
|
33
|
-
def create!(name, rules = nil)
|
34
|
-
|
35
|
-
swirl.call "CreateSecurityGroup", "GroupName" => name, "GroupDescription" => "%%" + description.to_json
|
33
|
+
def create!(name, rules = nil, description = "")
|
34
|
+
swirl.call "CreateSecurityGroup", "GroupName" => name, "GroupDescription" => description
|
36
35
|
auth(name, rules) if rules
|
37
36
|
end
|
38
37
|
|
@@ -63,7 +62,7 @@ module Stem
|
|
63
62
|
def gen_authorize_target(index, target)
|
64
63
|
if target =~ /^\d+\.\d+\.\d+.\d+\/\d+$/
|
65
64
|
{ "IpPermissions.#{index}.IpRanges.1.CidrIp" => target }
|
66
|
-
elsif target =~ /^(
|
65
|
+
elsif target =~ /^(.+)@(\w+)$/
|
67
66
|
{ "IpPermissions.#{index}.Groups.1.GroupName" => $1,
|
68
67
|
"IpPermissions.#{index}.Groups.1.UserId" => $2 }
|
69
68
|
elsif target =~ /^@(\w+)$/
|
data/lib/stem/image.rb
CHANGED
@@ -3,8 +3,7 @@ module Stem
|
|
3
3
|
include Util
|
4
4
|
extend self
|
5
5
|
|
6
|
-
def create name, instance, tags
|
7
|
-
raise "You already have an image named '#{name}'" if named(name)
|
6
|
+
def create name, instance, tags = {}
|
8
7
|
image_id = swirl.call("CreateImage", "Name" => name, "InstanceId" => instance)["imageId"]
|
9
8
|
unless tags.empty?
|
10
9
|
# We'll retry this once if necessary due to consistency issues on the AWS side
|
@@ -38,8 +37,18 @@ module Stem
|
|
38
37
|
swirl.call("DescribeImages", opts)['imagesSet'].map {|image| image['imageId'] }
|
39
38
|
end
|
40
39
|
|
40
|
+
def describe_tagged tags
|
41
|
+
opts = tags_to_filter(tags).merge("Owner" => "self")
|
42
|
+
images = swirl.call("DescribeImages", opts)["imagesSet"]
|
43
|
+
images.each {|image| image["tags"] = tagset_to_hash(image["tagSet"]) }
|
44
|
+
images
|
45
|
+
end
|
46
|
+
|
41
47
|
def describe image
|
42
48
|
swirl.call("DescribeImages", "ImageId" => image)["imagesSet"][0]
|
49
|
+
rescue Swirl::InvalidRequest => e
|
50
|
+
raise e unless e.message =~ /does not exist/
|
51
|
+
nil
|
43
52
|
end
|
44
53
|
end
|
45
54
|
end
|
data/lib/stem/instance.rb
CHANGED
@@ -5,18 +5,8 @@ module Stem
|
|
5
5
|
|
6
6
|
def launch config, userdata = nil
|
7
7
|
throw "No config provided" unless config
|
8
|
-
|
9
|
-
ami =
|
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
|
-
elsif config["ami-tags"]
|
16
|
-
ami = Image::tagged(config['ami-tags'])[0]
|
17
|
-
throw "AMI tagged with #{config['ami-tags'].inspect} was not found. (Does it need creating?)" unless ami
|
18
|
-
end
|
19
|
-
throw "No AMI specified." unless ami
|
8
|
+
config = aggregate_hash_options_for_ami!(config)
|
9
|
+
ami = config["ami"]
|
20
10
|
|
21
11
|
opt = {
|
22
12
|
"SecurityGroup.#" => config["groups"] || [],
|
data/lib/stem/util.rb
CHANGED
@@ -1,32 +1,19 @@
|
|
1
1
|
module Stem
|
2
2
|
module Util
|
3
3
|
def swirl
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
:aws_secret_access_key => ENV["AWS_SECRET_ACCESS_KEY"],
|
11
|
-
:version => "2010-08-31"
|
12
|
-
}
|
4
|
+
@swirl ||= Swirl::EC2.new load_config
|
5
|
+
end
|
6
|
+
|
7
|
+
def tagset_to_hash(tagset)
|
8
|
+
if tagset["item"].is_a?(Hash)
|
9
|
+
{tagset["item"]["key"] => tagset["item"]["value"]}
|
13
10
|
else
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
else
|
19
|
-
abort("I was expecting to find a .swirl file in your home directory.")
|
20
|
-
end
|
21
|
-
|
22
|
-
if data.key?(account)
|
23
|
-
data[account]
|
24
|
-
else
|
25
|
-
abort("I don't see the account you're looking for")
|
11
|
+
tagset["item"].inject({}) do |h,item|
|
12
|
+
k, v = item["key"], item["value"]
|
13
|
+
h[k] = v
|
14
|
+
h
|
26
15
|
end
|
27
16
|
end
|
28
|
-
|
29
|
-
@swirl = Swirl::EC2.new config
|
30
17
|
end
|
31
18
|
|
32
19
|
def tags_to_filter(tags)
|
@@ -47,7 +34,8 @@ module Stem
|
|
47
34
|
|
48
35
|
def get_filter_opts(filters)
|
49
36
|
opts = {}
|
50
|
-
filters.each_with_index do |
|
37
|
+
filters.keys.sort.each_with_index do |k, n|
|
38
|
+
v = filters[k]
|
51
39
|
opts["Filter.#{n}.Name"] = k.to_s
|
52
40
|
v = [ v ] unless v.is_a? Array
|
53
41
|
v.each_with_index do |v, i|
|
@@ -56,6 +44,52 @@ module Stem
|
|
56
44
|
end
|
57
45
|
opts
|
58
46
|
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def load_config
|
51
|
+
account = "default"
|
52
|
+
etc = "#{ENV["HOME"]}/.swirl"
|
53
|
+
if ENV["AWS_ACCESS_KEY_ID"] && ENV["AWS_SECRET_ACCESS_KEY"]
|
54
|
+
{
|
55
|
+
:aws_access_key_id => ENV["AWS_ACCESS_KEY_ID"],
|
56
|
+
:aws_secret_access_key => ENV["AWS_SECRET_ACCESS_KEY"],
|
57
|
+
:version => "2010-08-31"
|
58
|
+
}
|
59
|
+
else
|
60
|
+
account = account.to_sym
|
61
|
+
|
62
|
+
if File.exists?(etc)
|
63
|
+
data = YAML.load_file(etc)
|
64
|
+
else
|
65
|
+
abort("I was expecting to find a .swirl file in your home directory.")
|
66
|
+
end
|
67
|
+
|
68
|
+
if data.key?(account)
|
69
|
+
data[account]
|
70
|
+
else
|
71
|
+
abort("I don't see the account you're looking for")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def aggregate_hash_options_for_ami!(config)
|
77
|
+
if config["ami"]
|
78
|
+
return config
|
79
|
+
elsif config["ami-name"]
|
80
|
+
name = config.delete("ami-name")
|
81
|
+
config["ami"] = Image::named(name)
|
82
|
+
throw "AMI named #{name} was not found. (Does it need creating?)" unless config["ami"]
|
83
|
+
elsif config["ami-tags"]
|
84
|
+
tags = config.delete('ami-tags')
|
85
|
+
config["ami"] = Image::tagged(tags)[0]
|
86
|
+
throw "AMI tagged with #{tags.inspect} was not found. (Does it need creating?)" unless config["ami"]
|
87
|
+
else
|
88
|
+
throw "No AMI specified."
|
89
|
+
end
|
90
|
+
config
|
91
|
+
end
|
92
|
+
|
59
93
|
end
|
60
94
|
end
|
61
95
|
|
data/lib/stem.rb
CHANGED
metadata
CHANGED
@@ -1,22 +1,23 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stem
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 11
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
8
|
+
- 5
|
9
|
+
- 0
|
10
|
+
version: 0.5.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Peter van Hardenberg
|
14
14
|
- Orion Henry
|
15
|
+
- Blake Gentry
|
15
16
|
autorequire:
|
16
17
|
bindir: bin
|
17
18
|
cert_chain: []
|
18
19
|
|
19
|
-
date:
|
20
|
+
date: 2011-02-25 00:00:00 -08:00
|
20
21
|
default_executable:
|
21
22
|
dependencies:
|
22
23
|
- !ruby/object:Gem::Dependency
|
@@ -39,6 +40,7 @@ description: minimalist EC2 instance management
|
|
39
40
|
email:
|
40
41
|
- pvh@heroku.com
|
41
42
|
- orion@heroku.com
|
43
|
+
- b@heroku.com
|
42
44
|
executables:
|
43
45
|
- stem
|
44
46
|
extensions: []
|
@@ -49,6 +51,7 @@ files:
|
|
49
51
|
- LICENSE
|
50
52
|
- README.md
|
51
53
|
- lib/stem/cli.rb
|
54
|
+
- lib/stem/family.rb
|
52
55
|
- lib/stem/group.rb
|
53
56
|
- lib/stem/image.rb
|
54
57
|
- lib/stem/instance.rb
|