cluster 0.5.33
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +69 -0
- data/VERSION +1 -0
- data/bin/cluster +14 -0
- data/bin/ec2-consistent-snapshot +676 -0
- data/bin/periodic.sh +19 -0
- data/examples/cacerts.pem +19 -0
- data/examples/credentials.yml +24 -0
- data/examples/monitor.god +88 -0
- data/examples/users.sh +42 -0
- data/lib/cluster.rb +267 -0
- data/lib/cluster/cli.rb +206 -0
- data/lib/cluster/configuration.rb +52 -0
- data/lib/cluster/infrastructure.rb +160 -0
- data/lib/cluster/infrastructures/amazon.rb +568 -0
- data/lib/cluster/infrastructures/amazon_instance.rb +270 -0
- data/lib/cluster/infrastructures/amazon_release.rb +63 -0
- data/lib/cluster/instance.rb +97 -0
- data/lib/cluster/release.rb +30 -0
- data/lib/cluster/version.rb +25 -0
- data/lib/ext/array.rb +13 -0
- data/lib/ext/cluster_extensions.rb +13 -0
- metadata +206 -0
data/lib/cluster/cli.rb
ADDED
@@ -0,0 +1,206 @@
|
|
1
|
+
require 'cluster'
|
2
|
+
|
3
|
+
class Cluster
|
4
|
+
class Cli
|
5
|
+
def initialize(arguments = nil)
|
6
|
+
arguments ||= ARGV
|
7
|
+
|
8
|
+
unless arguments.length >= 1
|
9
|
+
puts "#{Cluster::NAME} usage: #{Cluster::NAME} [infa args] command [command args]"
|
10
|
+
exit 1
|
11
|
+
end
|
12
|
+
|
13
|
+
if arguments.include?('--version')
|
14
|
+
puts "#{Cluster::NAME} version #{Cluster::Version::STRING}"
|
15
|
+
exit 0
|
16
|
+
end
|
17
|
+
|
18
|
+
infra = []
|
19
|
+
command = nil
|
20
|
+
params = []
|
21
|
+
for arg in arguments
|
22
|
+
if command
|
23
|
+
params << arg
|
24
|
+
elsif arg =~ /^-/
|
25
|
+
infra << arg
|
26
|
+
else
|
27
|
+
command = arg
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
if arg = infra.detect {|a| a =~ /^(-c|--credentials=)(.+)/ }
|
32
|
+
file = $2
|
33
|
+
file = (file[0, 1] == File::SEPARATOR) ? file : File.join(ENV['PWD'], file)
|
34
|
+
|
35
|
+
Cluster::Configuration[:credentials_file] = file
|
36
|
+
infra.delete arg
|
37
|
+
else
|
38
|
+
Cluster.set_credentials_file
|
39
|
+
end
|
40
|
+
|
41
|
+
logger_file = if arg = infra.detect {|a| a =~ /^--logger=(.+)$/ }
|
42
|
+
infra.delete arg
|
43
|
+
$1
|
44
|
+
elsif Cluster::Configuration[:credentials_file]
|
45
|
+
File.join(File.dirname(Cluster::Configuration[:credentials_file]), 'cluster.log')
|
46
|
+
else
|
47
|
+
'/tmp/cluster.log'
|
48
|
+
end
|
49
|
+
|
50
|
+
unless command
|
51
|
+
puts "#{Cluster::NAME} usage: #{Cluster::NAME} [infa args] command [command args]"
|
52
|
+
exit 1
|
53
|
+
end
|
54
|
+
|
55
|
+
if command.downcase == 'gemurl'
|
56
|
+
# We don't need to do any infrastructure for this one...and
|
57
|
+
# we may not have it yet...
|
58
|
+
puts Cluster::LOCATION
|
59
|
+
exit 0
|
60
|
+
elsif command.downcase == 'imageurl'
|
61
|
+
# We don't need to do any infrastructure for this one...and
|
62
|
+
# we may not have it yet...
|
63
|
+
puts Cluster::IMAGES
|
64
|
+
exit 0
|
65
|
+
end
|
66
|
+
|
67
|
+
file = File.open(logger_file, File::WRONLY | File::APPEND | File::CREAT)
|
68
|
+
|
69
|
+
$stderr = file
|
70
|
+
Cluster::Configuration['logger'] = Logger.new(file, 5, 512000)
|
71
|
+
|
72
|
+
@sub = Infrastructure.connect(infra)
|
73
|
+
|
74
|
+
@named_output = (!params.empty? and params[0] == '-d' and params.shift)
|
75
|
+
@ip_output = (!params.empty? and params[0] == '-i' and params.shift)
|
76
|
+
@id_output = (!params.empty? and params[0] == '-I' and params.shift)
|
77
|
+
|
78
|
+
@cluster = Cluster.new @sub
|
79
|
+
if respond_to? command
|
80
|
+
self.send command, *params
|
81
|
+
elsif @cluster.respond_to? command
|
82
|
+
puts @cluster.send(command, *params)
|
83
|
+
else
|
84
|
+
STDERR.puts "#{Cluster::NAME} does not understand #{command}."
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def machines(*params)
|
89
|
+
res = @cluster.machines(*params)
|
90
|
+
print_instances(res)
|
91
|
+
end
|
92
|
+
|
93
|
+
def services(*params)
|
94
|
+
res = @cluster.services(*params)
|
95
|
+
print_instances(res)
|
96
|
+
end
|
97
|
+
alias :service :services
|
98
|
+
|
99
|
+
def labeled(*params)
|
100
|
+
print_instances @cluster.labeled(params.first)
|
101
|
+
end
|
102
|
+
|
103
|
+
def list(*params)
|
104
|
+
print_instances(@sub.instances) if params.empty?
|
105
|
+
|
106
|
+
print_instances @sub.instances.select {|i| params.any? {|p| i.identified_by? p } }
|
107
|
+
end
|
108
|
+
|
109
|
+
def print_instances(instances)
|
110
|
+
return if instances.empty?
|
111
|
+
|
112
|
+
for instance in instances
|
113
|
+
if @named_output
|
114
|
+
puts instance.dns
|
115
|
+
elsif @ip_output
|
116
|
+
puts instance.ip
|
117
|
+
elsif @id_output
|
118
|
+
puts instance.id
|
119
|
+
else
|
120
|
+
puts instance.to_s(:long)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def alter_instances(ids)
|
126
|
+
altered = @sub.instances.map {|ins|
|
127
|
+
if ids.any? {|p| ins.identified_by? p}
|
128
|
+
yield ins
|
129
|
+
ins
|
130
|
+
else
|
131
|
+
nil
|
132
|
+
end
|
133
|
+
}.compact
|
134
|
+
|
135
|
+
@sub.alter_instances! altered
|
136
|
+
puts "Instances altered :"
|
137
|
+
for ins in altered
|
138
|
+
puts ins.to_s(:long)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def enable(*params)
|
143
|
+
service_list = params.shift
|
144
|
+
|
145
|
+
if !service_list or params.empty?
|
146
|
+
STDERR.puts "Usage: #{Cluster::NAME} [infra] enable service[,services] instance [instances]"
|
147
|
+
exit 2
|
148
|
+
end
|
149
|
+
|
150
|
+
servs = service_list.split(',')
|
151
|
+
alter_instances(params) {|ins| ins.enable servs }
|
152
|
+
end
|
153
|
+
|
154
|
+
def disable(*params)
|
155
|
+
service_list = params.shift
|
156
|
+
|
157
|
+
if !service_list or params.empty?
|
158
|
+
STDERR.puts "Usage: #{Cluster::NAME} [infra] disable service[,services] instance [instances]"
|
159
|
+
exit 2
|
160
|
+
end
|
161
|
+
|
162
|
+
servs = service_list.split(',')
|
163
|
+
alter_instances(params) {|ins| ins.disable servs }
|
164
|
+
end
|
165
|
+
|
166
|
+
def label(name, id)
|
167
|
+
alter_instances([id]) {|i| i.friendly_name = name.downcase}
|
168
|
+
end
|
169
|
+
|
170
|
+
def unlabel(id)
|
171
|
+
alter_instances([id]) {|i| i.friendly_name = ''}
|
172
|
+
end
|
173
|
+
|
174
|
+
def set_state(state, *ids)
|
175
|
+
alter_instances(ids) {|i| i.set_state state }
|
176
|
+
end
|
177
|
+
|
178
|
+
def stop(*ids)
|
179
|
+
alter_instances(ids) {|i| i.stop! }
|
180
|
+
end
|
181
|
+
|
182
|
+
def release(*params)
|
183
|
+
if params.empty?
|
184
|
+
puts "Need an environment to work on for releases."
|
185
|
+
exit 1
|
186
|
+
end
|
187
|
+
|
188
|
+
tag_output = (!params.empty? and params.delete('-t'))
|
189
|
+
rel = @cluster.release(*params)
|
190
|
+
if !rel
|
191
|
+
puts "No release found for environment #{params.first}"
|
192
|
+
exit 1
|
193
|
+
elsif tag_output
|
194
|
+
puts rel.tag
|
195
|
+
else
|
196
|
+
puts "#{rel.environment} was released at #{rel.created_at} with '#{rel.tag}'"
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def security(*args)
|
201
|
+
@cluster.security(*args).each {|k, v|
|
202
|
+
puts "#{k}: #{v.join(' ')}"
|
203
|
+
}
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
class Cluster
|
4
|
+
module Logging
|
5
|
+
# Logging should be set up by configuration early, but if it isnt
|
6
|
+
# then we will just write to STDERR
|
7
|
+
@@logger = nil
|
8
|
+
|
9
|
+
def logger
|
10
|
+
@@logger ||= Cluster::Configuration['logger']
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Configuration
|
15
|
+
include Cluster::Logging
|
16
|
+
|
17
|
+
@@system_config = {}
|
18
|
+
@@credentials = nil
|
19
|
+
|
20
|
+
class << self
|
21
|
+
def credentials?
|
22
|
+
self[:credentials_file]
|
23
|
+
end
|
24
|
+
|
25
|
+
def credentials
|
26
|
+
@@credentials ||= credentials? && File.open(self[:credentials_file]) {|f| YAML::load(f) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def []=(key, value)
|
30
|
+
@@system_config[key.to_s] = value
|
31
|
+
end
|
32
|
+
|
33
|
+
def [](key)
|
34
|
+
@@system_config[key.to_s]
|
35
|
+
end
|
36
|
+
|
37
|
+
def method_missing(meth, *params)
|
38
|
+
field = meth.to_s
|
39
|
+
if options.respond_to? field
|
40
|
+
options.send(field)
|
41
|
+
else
|
42
|
+
super
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def options(params = nil)
|
47
|
+
@@system_config ||= self.new(params)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
%w(ostruct optparse).each {|l| require l }
|
2
|
+
|
3
|
+
class Infrastructure
|
4
|
+
include Cluster::Logging
|
5
|
+
|
6
|
+
@@subsystem = nil
|
7
|
+
@@cluster_name = 'cluster'
|
8
|
+
@@in_cluster = false
|
9
|
+
@@machine_sizes = %w(minimum basic average power super)
|
10
|
+
|
11
|
+
def initialize(arguments = nil)
|
12
|
+
@arguments = arguments
|
13
|
+
end
|
14
|
+
|
15
|
+
def configure
|
16
|
+
@options = OpenStruct.new
|
17
|
+
@options.current_instance_id = nil
|
18
|
+
@options.original_services = []
|
19
|
+
|
20
|
+
if credentials?
|
21
|
+
cluster = credentials['cluster']
|
22
|
+
if cluster
|
23
|
+
@options.current_instance_id = cluster['id']
|
24
|
+
@options.original_services = cluster['services'].split(',')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
@@subsystem ||= self
|
29
|
+
end
|
30
|
+
|
31
|
+
def instances
|
32
|
+
raise NotImplementedError
|
33
|
+
end
|
34
|
+
|
35
|
+
def current_instance
|
36
|
+
return nil unless @options.current_instance_id
|
37
|
+
|
38
|
+
instances.detect {|ins| ins.id.eql?(@options.current_instance_id) }
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def credentials?
|
43
|
+
return true if @credentials
|
44
|
+
|
45
|
+
if Cluster::Configuration.credentials?
|
46
|
+
@credentials = Cluster::Configuration.credentials
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def credentials
|
51
|
+
@credentials
|
52
|
+
end
|
53
|
+
|
54
|
+
def in_cluster?
|
55
|
+
raise NotImplementedError
|
56
|
+
end
|
57
|
+
|
58
|
+
def names(*roles)
|
59
|
+
raise NotImplementedError
|
60
|
+
end
|
61
|
+
|
62
|
+
def release_class
|
63
|
+
raise NotImplementedError
|
64
|
+
end
|
65
|
+
|
66
|
+
def machines(filter_groups)
|
67
|
+
if filter_groups.empty?
|
68
|
+
self.instances
|
69
|
+
else
|
70
|
+
self.instances.select {|i| i.groups.any? {|g| filter_groups.include? g }}
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def services(filter_services)
|
75
|
+
filter_services = Array(filter_services) unless filter_services.is_a? Array
|
76
|
+
|
77
|
+
if filter_services.empty?
|
78
|
+
puts "No services provided to return a list for #{filter_services.join(' ')}"
|
79
|
+
exit 1
|
80
|
+
else
|
81
|
+
self.instances.select {|i|
|
82
|
+
i.services.any? {|s| filter_services.include?(s) and !i.disabled_services.include?(s)}
|
83
|
+
}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def to_credentials
|
88
|
+
opts = @options.marshal_dump.keys.inject({}) {|m, k|
|
89
|
+
if k.eql? :role
|
90
|
+
m
|
91
|
+
else
|
92
|
+
m.merge k.to_s => @options.send(k)
|
93
|
+
end
|
94
|
+
}
|
95
|
+
{'cluster_name' => self.class.cluster_name,
|
96
|
+
self.class.to_s.downcase => opts }
|
97
|
+
end
|
98
|
+
|
99
|
+
@@dns = nil
|
100
|
+
class << self
|
101
|
+
def sizes
|
102
|
+
@@machine_sizes
|
103
|
+
end
|
104
|
+
alias :machine_sizes :sizes
|
105
|
+
|
106
|
+
def current
|
107
|
+
@@subsystem
|
108
|
+
end
|
109
|
+
|
110
|
+
def cluster_name
|
111
|
+
@@cluster_name
|
112
|
+
end
|
113
|
+
|
114
|
+
def connect(arguments = nil)
|
115
|
+
name = nil
|
116
|
+
# removes the infrastructure argument, if given, and processes it
|
117
|
+
new_args = arguments.map {|arg|
|
118
|
+
case arg
|
119
|
+
when /^-i([^=]+)$/, /^--infr[^=]+=(.+)$/
|
120
|
+
name = $1
|
121
|
+
nil
|
122
|
+
when /^-n([^=]+)$/, /^--name[^=]+=(.+)$/
|
123
|
+
@@cluster_name = $1
|
124
|
+
else
|
125
|
+
arg
|
126
|
+
end
|
127
|
+
}.compact
|
128
|
+
|
129
|
+
name ||= 'amazon'
|
130
|
+
|
131
|
+
begin
|
132
|
+
require File.join('cluster', 'infrastructures', name)
|
133
|
+
rescue LoadError => err
|
134
|
+
STDERR.puts "Subsystem of #{name} either is not included or not available.\n\t#{err.message}\n#{err.backtrace.join("\n\t")}"
|
135
|
+
exit 1
|
136
|
+
end
|
137
|
+
|
138
|
+
begin
|
139
|
+
sub = Object.const_get name.capitalize
|
140
|
+
rescue NameError
|
141
|
+
STDERR.puts "Subsystem of #{name.capitalize} cannot be initialized."
|
142
|
+
exit 1
|
143
|
+
end
|
144
|
+
|
145
|
+
sub.new new_args
|
146
|
+
end
|
147
|
+
|
148
|
+
def dns
|
149
|
+
return @@dns unless @@dns.nil?
|
150
|
+
ns = ''
|
151
|
+
File.open('/etc/resolv.conf').each do |line| ns = $1 if line =~ /nameserver\s+([\d\.]+)/; end
|
152
|
+
|
153
|
+
@@dns = Resolv::DNS.new(:nameserver => ns)
|
154
|
+
end
|
155
|
+
|
156
|
+
def in_cluster?
|
157
|
+
@@in_cluster
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,568 @@
|
|
1
|
+
%w(uuidtools yaml right_aws resolv sdb/active_sdb cluster/infrastructures/amazon_release cluster/infrastructures/amazon_instance).each {|l| require l}
|
2
|
+
|
3
|
+
class Amazon < Infrastructure
|
4
|
+
|
5
|
+
def configure
|
6
|
+
super
|
7
|
+
@@in_cluster = in_cluster?
|
8
|
+
@@instances = nil
|
9
|
+
|
10
|
+
if credentials? and credentials.include? 'amazon'
|
11
|
+
creds = credentials['amazon']
|
12
|
+
@options.key = creds['key']
|
13
|
+
@options.secret = creds['secret']
|
14
|
+
@options.owner = creds['owner']
|
15
|
+
@options.cluster_bucket = creds['cluster_bucket']
|
16
|
+
@options.bucket_key = creds['bucket_key']
|
17
|
+
@options.cluster_domain = creds['cluster_domain']
|
18
|
+
@options.zone = creds['zone']
|
19
|
+
@options.volumes = creds['volumes']
|
20
|
+
end
|
21
|
+
|
22
|
+
@options.key ||= ENV['AMAZON_ACCESS_KEY_ID']
|
23
|
+
@options.secret ||= ENV['AMAZON_SECRET_ACCESS_KEY']
|
24
|
+
@options.owner ||= ENV['AMAZON_OWNER_ID']
|
25
|
+
@options.cluster_bucket ||= ENV['CLUSTER_BUCKET']
|
26
|
+
@options.bucket_key ||= 'cluster_credentials.yml'
|
27
|
+
@options.zone ||= ENV['AMAZON_ZONE']
|
28
|
+
@options.cluster_domain ||= ENV['CLUSTER_DOMAIN'] || self.class.cluster_name
|
29
|
+
@options.volumes ||= {}
|
30
|
+
|
31
|
+
@options.role = (ENV['CLUSTER_ROLE'] or ENV['RAILS_ENV'] or 'production')
|
32
|
+
|
33
|
+
@options.cluster_image_key = 'cluster_images.yml'
|
34
|
+
@options.spot_instances = false
|
35
|
+
@options.price = false
|
36
|
+
|
37
|
+
OptionParser.new {|o|
|
38
|
+
o.banner = "Amazon Infrastructure Options"
|
39
|
+
|
40
|
+
o.on('-k', '--key VAL', "Amazon Access Key ID") do |v|
|
41
|
+
@options.key = v
|
42
|
+
end
|
43
|
+
|
44
|
+
o.on('-s', '--secret VAL', 'Amazon Access Secret') do |v|
|
45
|
+
@options.secret = v
|
46
|
+
end
|
47
|
+
|
48
|
+
o.on('-o', '--owenr VAL', 'Amazon User Code') do |v|
|
49
|
+
@options.owner = v
|
50
|
+
end
|
51
|
+
|
52
|
+
o.on('-d', '--domain VAL', 'Amazon Domain to use') do |v|
|
53
|
+
@options.cluster_domain = v
|
54
|
+
end
|
55
|
+
|
56
|
+
o.on('-b', '--bucket VAL', 'Cluster Bucket') do |v|
|
57
|
+
@options.cluster_bucket = v
|
58
|
+
end
|
59
|
+
|
60
|
+
o.on('-f', '--bucket-credentials-file VAL', 'Cluster credentials file location on the bucket.') do |v|
|
61
|
+
@options.bucket_key = v
|
62
|
+
end
|
63
|
+
|
64
|
+
o.on('--source-bucket VAL', 'Bucket that has the host data.') do |v|
|
65
|
+
end
|
66
|
+
|
67
|
+
o.on('-r', '--role VAL', 'Role in which to operate.') do |v|
|
68
|
+
@options.role = v
|
69
|
+
end
|
70
|
+
|
71
|
+
o.on('-z', '--zone VAL', "Availability Zone") do |v|
|
72
|
+
@options.zone = v
|
73
|
+
end
|
74
|
+
|
75
|
+
o.on('--spot', "Use Spot Instances") do |v|
|
76
|
+
@options.spot_instances = true
|
77
|
+
end
|
78
|
+
|
79
|
+
o.on('--price=VAL', 'Maximum price for the spot instance') do |v|
|
80
|
+
@options.price = v
|
81
|
+
end
|
82
|
+
|
83
|
+
}.parse(@arguments)
|
84
|
+
|
85
|
+
unless @options.key and @options.secret
|
86
|
+
$stderr.puts "Amazon Infrastructure cannot communicate without secret and key."
|
87
|
+
exit 2
|
88
|
+
end
|
89
|
+
|
90
|
+
unless @options.cluster_domain
|
91
|
+
$stderr.puts "Amazon Infrastructure needs to know what domain to connect to."
|
92
|
+
exit 2
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def instances
|
97
|
+
@@instances ||= load_instances
|
98
|
+
end
|
99
|
+
|
100
|
+
def cost(sizes)
|
101
|
+
unless @options.spot_instances
|
102
|
+
puts "Cost only works for spot instances currently (ie. supply infrastructure argument of --spot"
|
103
|
+
exit 3
|
104
|
+
end
|
105
|
+
args = {:start_time => (Time.now - (7 * 24 * 60 * 60)),
|
106
|
+
:end_time => Time.now,
|
107
|
+
:product_description => "Linux/UNIX"}
|
108
|
+
sizes.map {|s|
|
109
|
+
t = [size_to_instance_type(s)]
|
110
|
+
prices = ecc.describe_spot_price_history(args.merge(:instance_types => t))
|
111
|
+
p = prices.reduce(0) {|c,p| c + p[:spot_price] } / prices.length
|
112
|
+
[s, "%0.3f" % p]
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
def create_file_store(name)
|
117
|
+
sss.bucket(name, true)
|
118
|
+
end
|
119
|
+
|
120
|
+
def create_data_store(name)
|
121
|
+
sdb.create_domain(name)
|
122
|
+
end
|
123
|
+
|
124
|
+
def load_instances
|
125
|
+
terminated = []
|
126
|
+
iss = ecc.describe_instances.map {|ins|
|
127
|
+
if %w(terminated shutting-down).include? ins[:aws_state]
|
128
|
+
terminated << ins[:aws_instance_id]
|
129
|
+
nil
|
130
|
+
else
|
131
|
+
AmazonInstance.new(ins)
|
132
|
+
end
|
133
|
+
}.compact
|
134
|
+
|
135
|
+
begin
|
136
|
+
res = sdb.select "select * from #{domain} where entry='machine'"
|
137
|
+
rescue RightAws::AwsError
|
138
|
+
unless domains.include? domain
|
139
|
+
sdb.create_domain domain
|
140
|
+
retry
|
141
|
+
end
|
142
|
+
end
|
143
|
+
sdbs = self.class.from_sdb_results res
|
144
|
+
|
145
|
+
sdbs.each do |sd|
|
146
|
+
aid = sd[:aws_id]
|
147
|
+
iid = sd['ec2_id']
|
148
|
+
if !iid
|
149
|
+
started = sd['start_time_sorted'] && Time.parse(sd['start_time_sorted'])
|
150
|
+
diff = started && (Time.now - started)
|
151
|
+
if diff and diff < (12 * 3600)
|
152
|
+
ins = AmazonInstance.new
|
153
|
+
ins.set_sdb_attributes sd
|
154
|
+
iss.push ins
|
155
|
+
else
|
156
|
+
$stderr.puts "Cannot find machine #{aid} -- old entry being removed."
|
157
|
+
sdb.delete_attributes domain, aid
|
158
|
+
end
|
159
|
+
else
|
160
|
+
if ins = iss.detect {|i| i.id.eql? iid }
|
161
|
+
ins.set_sdb_attributes sd
|
162
|
+
elsif terminated.include? iid
|
163
|
+
$stderr.puts "Removing terminated entry #{iid}"
|
164
|
+
sdb.delete_attributes domain, aid
|
165
|
+
else
|
166
|
+
$stderr.puts "Orphaned cluster record of #{aid}. (Just started?) [#{sd.inspect}]"
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
iss.each do |ins|
|
172
|
+
if ins.no_sdb?
|
173
|
+
puts "Cannot find cluster registration for #{ins.ec2_id} -- creating."
|
174
|
+
sdb.put_attributes domain, ins.aws_id, ins.attributes, :replace
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
iss
|
179
|
+
end
|
180
|
+
|
181
|
+
def alter_instances!(*iss)
|
182
|
+
list = iss.empty? ? instances : iss
|
183
|
+
|
184
|
+
for ins in list.flatten
|
185
|
+
yield ins if block_given?
|
186
|
+
attrs = ins.attributes
|
187
|
+
remove = attrs.keys.select {|k| attrs[k].empty? and attrs.delete(k) }
|
188
|
+
unless remove.empty?
|
189
|
+
sdb.delete_attributes domain, ins.aws_id, remove
|
190
|
+
end
|
191
|
+
sdb.put_attributes domain, ins.aws_id, ins.attributes, :replace
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def in_cluster?
|
196
|
+
return @cluster_check if @cluster_check
|
197
|
+
|
198
|
+
check = false
|
199
|
+
begin
|
200
|
+
Timeout::timeout(1) do
|
201
|
+
begin
|
202
|
+
s = TCPSocket.new('169.254.169.254', 80)
|
203
|
+
s.close
|
204
|
+
check = true
|
205
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
|
206
|
+
# NOP
|
207
|
+
end
|
208
|
+
end
|
209
|
+
rescue Timeout::Error
|
210
|
+
end
|
211
|
+
|
212
|
+
@@in_cluster = check
|
213
|
+
end
|
214
|
+
|
215
|
+
def size_to_instance_type(size)
|
216
|
+
unless self.class.sizes.include? size.downcase
|
217
|
+
puts "#{Cluster::NAME} does not have a machine size of #{size}\n\tAvailable Sizes: (#{self.class.sizes.join(', ')})"
|
218
|
+
exit 2
|
219
|
+
end
|
220
|
+
AmazonInstance.size_to_type(size)
|
221
|
+
end
|
222
|
+
|
223
|
+
def new_instance(size, services)
|
224
|
+
args = { :services => services, :size => size }
|
225
|
+
if @options.spot_instances
|
226
|
+
unless @options.price
|
227
|
+
puts "Amazon Spot instances need a '--price=??' argument"
|
228
|
+
exit 3
|
229
|
+
end
|
230
|
+
args.merge! :spot_price => @options.price
|
231
|
+
end
|
232
|
+
=begin
|
233
|
+
type = size_to_type size
|
234
|
+
groups = services_to_groups services
|
235
|
+
image = type_to_image type
|
236
|
+
puts "type #{type} Groups #{groups.inspect} K #{key} I #{image} UD #{user}"
|
237
|
+
=end
|
238
|
+
ins = AmazonInstance.create args
|
239
|
+
puts ins.inspect
|
240
|
+
ins
|
241
|
+
end
|
242
|
+
|
243
|
+
def save_monitor(io, key)
|
244
|
+
if bucket.put key, io
|
245
|
+
old = sdb.select "select * from #{domain} where entry = 'monitor'"
|
246
|
+
monitor = unless old[:items].empty?
|
247
|
+
self.class.from_sdb_results(old).first
|
248
|
+
else
|
249
|
+
{ 'aws_id' => UUIDTools::UUID.timestamp_create.to_s,
|
250
|
+
'entry' => 'monitor' }
|
251
|
+
end
|
252
|
+
monitor.merge! 'updated_at' => Time.now.xmlschema(3),
|
253
|
+
'key' => key,
|
254
|
+
'bucket' => @cluster_bucket
|
255
|
+
sdb.put_attributes domain, monitor['aws_id'], self.class.to_sdb_attributes(monitor), :replace
|
256
|
+
monitor
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def fetch_monitor
|
261
|
+
res = sdb.select "select * from #{domain} where entry = 'monitor'"
|
262
|
+
return nil if res[:items].empty?
|
263
|
+
monitor = self.class.from_sdb_results(res).first
|
264
|
+
bucket.get(monitor['key'])
|
265
|
+
end
|
266
|
+
|
267
|
+
def current_instance
|
268
|
+
unless in_cluster?
|
269
|
+
puts "Are we in the cluster?"
|
270
|
+
exit 3
|
271
|
+
end
|
272
|
+
|
273
|
+
require 'open-uri'
|
274
|
+
ec2_id = open('http://169.254.169.254/latest/meta-data/instance-id') {|f| f.read }
|
275
|
+
ins = self.instances.detect {|i| i.ec2_id.eql? ec2_id }
|
276
|
+
|
277
|
+
unless ins
|
278
|
+
puts "#{Cluster::NAME} cannot determine the current instance."
|
279
|
+
exit 2
|
280
|
+
end
|
281
|
+
|
282
|
+
if @options.current_instance_id
|
283
|
+
aws = self.instances.detect {|i| i.aws_id.eql? @options.current_instance_id }
|
284
|
+
if aws and aws.aws_id != ins.aws_id
|
285
|
+
ins.services = (ins.services + aws.services).uniq
|
286
|
+
ins.disabled_services = (ins.disabled_services + aws.disabled_services).uniq
|
287
|
+
ins.spot_price = aws.spot_price unless ins.spot_price
|
288
|
+
ins.friendly_name = aws.friendly_name unless ins.friendly_name
|
289
|
+
sdb.delete_attributes domain, aws.aws_id
|
290
|
+
sdb.put_attributes domain, ins.aws_id, ins.attributes, :replace
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
ins
|
295
|
+
end
|
296
|
+
|
297
|
+
def release_class
|
298
|
+
AmazonRelease
|
299
|
+
end
|
300
|
+
|
301
|
+
# The methods below here are typically only used internally, but
|
302
|
+
# could also be called by anything that would like to access
|
303
|
+
# the Amazon tools directly.
|
304
|
+
|
305
|
+
def key
|
306
|
+
@options.key
|
307
|
+
end
|
308
|
+
|
309
|
+
def secret
|
310
|
+
@options.secret
|
311
|
+
end
|
312
|
+
|
313
|
+
def owner
|
314
|
+
@options.owner
|
315
|
+
end
|
316
|
+
|
317
|
+
def domain
|
318
|
+
@options.cluster_domain
|
319
|
+
end
|
320
|
+
|
321
|
+
def sdb(params = {})
|
322
|
+
return @sdb if @sdb
|
323
|
+
@sdb = RightAws::SdbInterface.new(key, secret, connection_params(params))
|
324
|
+
unless @sdb
|
325
|
+
puts "Amazon cannot connect to SimpleDB"
|
326
|
+
exit 3
|
327
|
+
end
|
328
|
+
@sdb
|
329
|
+
end
|
330
|
+
|
331
|
+
def connect_to_active_sdb(params = {})
|
332
|
+
RightAws::ActiveSdb.establish_connection key, secret, connection_params(params)
|
333
|
+
end
|
334
|
+
|
335
|
+
def sss(params = {})
|
336
|
+
return @s3 if @s3
|
337
|
+
@s3 = RightAws::S3.new(key, secret, connection_params(params))
|
338
|
+
unless @s3
|
339
|
+
puts "Amazon cannot connect to S3"
|
340
|
+
exit 3
|
341
|
+
end
|
342
|
+
@s3
|
343
|
+
end
|
344
|
+
|
345
|
+
def sqs(params = {})
|
346
|
+
return @sqs if @sqs
|
347
|
+
@sqs = RightAws::SqsGen2.new(key, secret, connection_params(params))
|
348
|
+
unless @sqs
|
349
|
+
puts "Amazon cannot connect to S3"
|
350
|
+
exit 3
|
351
|
+
end
|
352
|
+
@sqs
|
353
|
+
end
|
354
|
+
|
355
|
+
def elb(params = {})
|
356
|
+
return @elb if @elb
|
357
|
+
@elb = RightAws::ElbInterface.new(key, secret, connection_params(params))
|
358
|
+
unless @elb
|
359
|
+
puts "Amazon cannot connect to elb"
|
360
|
+
exit 3
|
361
|
+
end
|
362
|
+
@elb
|
363
|
+
end
|
364
|
+
|
365
|
+
def ecc(params = {})
|
366
|
+
return @ec2 if @ec2
|
367
|
+
@ec2 = RightAws::Ec2.new(key, secret, connection_params(params))
|
368
|
+
unless @ec2
|
369
|
+
puts "Amazon cannot connect to EC2"
|
370
|
+
exit 3
|
371
|
+
end
|
372
|
+
@ec2
|
373
|
+
end
|
374
|
+
|
375
|
+
def domains(reload = false)
|
376
|
+
@sdb_domains = if !@sdb_domains or reload
|
377
|
+
sdb.list_domains[:domains]
|
378
|
+
else
|
379
|
+
@sdb_domains
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
def in_cluster?
|
384
|
+
return @check unless @check.nil?
|
385
|
+
check = false
|
386
|
+
begin
|
387
|
+
Timeout::timeout(2) do
|
388
|
+
begin
|
389
|
+
s = TCPSocket.new('169.254.169.254', 80)
|
390
|
+
s.close
|
391
|
+
check = true
|
392
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
|
393
|
+
# NOP
|
394
|
+
end
|
395
|
+
end
|
396
|
+
rescue Timeout::Error
|
397
|
+
end
|
398
|
+
|
399
|
+
@check = check
|
400
|
+
end
|
401
|
+
|
402
|
+
def connection_params(params = {})
|
403
|
+
params = {:multi_thread => true} if params.empty?
|
404
|
+
unless params.include?(:logger)
|
405
|
+
params.merge(:logger => logger)
|
406
|
+
else
|
407
|
+
params
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
def store(key, io)
|
412
|
+
bucket.put(key, io)
|
413
|
+
end
|
414
|
+
|
415
|
+
def retrieve(key)
|
416
|
+
bucket.get key
|
417
|
+
end
|
418
|
+
|
419
|
+
def save_credentials(creds, key = nil)
|
420
|
+
key ||= @options.bucket_key
|
421
|
+
bucket.put(key, creds)
|
422
|
+
end
|
423
|
+
|
424
|
+
def save_images(input)
|
425
|
+
key ||= @options.cluster_image_key
|
426
|
+
bucket.put key, input, {}, 'public-read'
|
427
|
+
end
|
428
|
+
|
429
|
+
def credentials_url(seconds = 1200)
|
430
|
+
keygen = RightAws::S3Generator::Key.new(bucket, @options.bucket_key)
|
431
|
+
keygen.get(seconds)
|
432
|
+
end
|
433
|
+
|
434
|
+
def period(args)
|
435
|
+
shots = {}
|
436
|
+
ecc.describe_snapshots.each do |shot|
|
437
|
+
next unless shot[:aws_owner].to_s == @options.owner.to_s
|
438
|
+
next unless shot[:aws_status] == 'completed'
|
439
|
+
shot[:started_at] = Time.parse shot[:aws_started_at]
|
440
|
+
aid = shot[:aws_volume_id]
|
441
|
+
if shots.include? aid
|
442
|
+
shots[aid].push shot
|
443
|
+
else
|
444
|
+
shots.merge! aid => [shot]
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
stores = @options.volumes['stores'] || []
|
449
|
+
defaults = if @options.volumes.include?('defaults')
|
450
|
+
@options.volumes['defaults']
|
451
|
+
else
|
452
|
+
{}
|
453
|
+
end
|
454
|
+
|
455
|
+
shots.keys.each do |k|
|
456
|
+
opts = if v = stores.detect {|v| v['aws_id'] == k}
|
457
|
+
defaults.merge v
|
458
|
+
else
|
459
|
+
defaults
|
460
|
+
end
|
461
|
+
snap_count = opts['snaps'] || 2
|
462
|
+
snap_count = 2 unless snap_count > 1
|
463
|
+
|
464
|
+
ordered = shots[k].sort {|a,b| a[:started_at] <=> b[:started_at] }.reverse
|
465
|
+
ordered.slice(snap_count, ordered.length).map {|shot|
|
466
|
+
ecc.delete_snapshot shot[:aws_id]
|
467
|
+
shot[:aws_id]
|
468
|
+
}
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
def bucket
|
473
|
+
unless @options.cluster_bucket
|
474
|
+
puts "#{Cluster::NAME} has not been configured with a bucket for client materials."
|
475
|
+
exit 2
|
476
|
+
end
|
477
|
+
|
478
|
+
@bucket ||= sss.bucket(@options.cluster_bucket, true)
|
479
|
+
|
480
|
+
unless @bucket
|
481
|
+
puts "#{Cluster::NAME} bucket named #{@options.cluster_bucket} cannot be created or accessed."
|
482
|
+
exit 2
|
483
|
+
else
|
484
|
+
@bucket
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
def get_image_file
|
489
|
+
require 'open-uri'
|
490
|
+
open(Cluster::IMAGES) {|f| YAML::load(f) }
|
491
|
+
end
|
492
|
+
|
493
|
+
def get_image(bits)
|
494
|
+
@image_file ||= get_image_file
|
495
|
+
case bits
|
496
|
+
when 32
|
497
|
+
@image_file['thirtytwo']
|
498
|
+
when 64
|
499
|
+
@image_file['sixtyfour']
|
500
|
+
end
|
501
|
+
end
|
502
|
+
|
503
|
+
def options
|
504
|
+
@options
|
505
|
+
end
|
506
|
+
|
507
|
+
def security(groups)
|
508
|
+
ecc.describe_security_groups(groups).inject({}) {|m,g|
|
509
|
+
p = g[:aws_perms].map {|p|
|
510
|
+
next unless p[:cidr_ips]
|
511
|
+
[p[:cidr_ips], p[:from_port]..p[:to_port]]
|
512
|
+
}.compact
|
513
|
+
|
514
|
+
m.merge g[:aws_group_name] => p
|
515
|
+
}
|
516
|
+
end
|
517
|
+
|
518
|
+
def revoke(ips)
|
519
|
+
ips.map {|ip|
|
520
|
+
ecc.revoke_security_group_IP_ingress('access', 22, 22, 'tcp', ip) and ip
|
521
|
+
}
|
522
|
+
end
|
523
|
+
|
524
|
+
def authorize(ips)
|
525
|
+
ips.map {|ip|
|
526
|
+
ecc.authorize_security_group_IP_ingress('access', 22, 22, 'tcp', ip) and ip
|
527
|
+
}
|
528
|
+
end
|
529
|
+
|
530
|
+
def query(qry)
|
531
|
+
res = sdb.select qry
|
532
|
+
if res
|
533
|
+
self.class.from_sdb_results res
|
534
|
+
else
|
535
|
+
nil
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
|
540
|
+
class << self
|
541
|
+
def to_sdb_attributes(args)
|
542
|
+
attrs = {}
|
543
|
+
args.each do |k, v|
|
544
|
+
v && attrs.merge!(k => Array(v))
|
545
|
+
end
|
546
|
+
attrs
|
547
|
+
end
|
548
|
+
|
549
|
+
def from_sdb_results(res)
|
550
|
+
res[:items].inject([]) {|m, obj|
|
551
|
+
aws_id = obj.keys.first
|
552
|
+
attrs = obj[aws_id]
|
553
|
+
args = attrs.keys.inject({:aws_id => aws_id}) {|n, attr|
|
554
|
+
val = attrs[attr]
|
555
|
+
if val.empty?
|
556
|
+
n.merge attr => nil
|
557
|
+
elsif val.length == 1
|
558
|
+
n.merge attr => val.first
|
559
|
+
else
|
560
|
+
n.merge attr => val
|
561
|
+
end
|
562
|
+
}
|
563
|
+
|
564
|
+
m << args
|
565
|
+
}
|
566
|
+
end
|
567
|
+
end
|
568
|
+
end
|