cluster 0.5.33
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.
- 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
|