syncwrap 1.5.0 → 1.5.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.rdoc +3 -0
- data/Manifest.txt +1 -0
- data/lib/syncwrap/aws.rb +400 -0
- data/lib/syncwrap/base.rb +1 -1
- metadata +4 -3
data/History.rdoc
CHANGED
data/Manifest.txt
CHANGED
data/lib/syncwrap/aws.rb
ADDED
@@ -0,0 +1,400 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2011-2013 David Kellum
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License"); you
|
5
|
+
# may not use this file except in compliance with the License. You may
|
6
|
+
# obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
13
|
+
# implied. See the License for the specific language governing
|
14
|
+
# permissions and limitations under the License.
|
15
|
+
#++
|
16
|
+
|
17
|
+
require 'json'
|
18
|
+
require 'aws-sdk'
|
19
|
+
|
20
|
+
require 'syncwrap/common'
|
21
|
+
|
22
|
+
# Supports host provisioning in EC2 via AWS APIs, creating and
|
23
|
+
# attaching EBS volumes, creating Route53 record sets, and as a remote
|
24
|
+
# task: building mdraid volumes.
|
25
|
+
#
|
26
|
+
# This module also includes a disk based cache of meta-data on created
|
27
|
+
# instances which allows automated role assignment (i.e. create an
|
28
|
+
# instance and run deploy tasks on it in a single pass.)
|
29
|
+
module SyncWrap::AWS
|
30
|
+
include SyncWrap::Common
|
31
|
+
|
32
|
+
# The json configuration file, parsed and passed directly to
|
33
|
+
# AWS::config method. This file should contain a json object with
|
34
|
+
# the minimal required keys: access_key_id, secret_access_key
|
35
|
+
# (default: ./private/aws.json)
|
36
|
+
attr_accessor :aws_config_json
|
37
|
+
|
38
|
+
# The cached aws instance json file (default: ./aws_instances.json)
|
39
|
+
attr_accessor :aws_instances_json
|
40
|
+
|
41
|
+
# Array of region strings to check for aws_import_instances
|
42
|
+
attr_accessor :aws_regions
|
43
|
+
|
44
|
+
# Array of imported or read instances represented as hashes.
|
45
|
+
attr_accessor :aws_instances
|
46
|
+
|
47
|
+
# Default options (which may be overridden) in call to
|
48
|
+
# aws_create_instance.
|
49
|
+
attr_accessor :aws_default_instance_options
|
50
|
+
|
51
|
+
# Default options (which may be overridden) in call to
|
52
|
+
# aws_create_security_group.
|
53
|
+
attr_accessor :aws_default_sg_options
|
54
|
+
|
55
|
+
# Default options for Route53 record set creation
|
56
|
+
attr_accessor :route53_default_rs_options
|
57
|
+
|
58
|
+
def initialize
|
59
|
+
@aws_config_json = 'private/aws.json'
|
60
|
+
@aws_regions = %w[ us-east-1 ]
|
61
|
+
@aws_instances_json = 'aws_instances.json'
|
62
|
+
@aws_instances = []
|
63
|
+
@aws_default_instance_options = {
|
64
|
+
:ebs_volumes => 0,
|
65
|
+
:ebs_volume_options => { :size => 16 }, #gb
|
66
|
+
:lvm_volumes => [ [ 1.00, '/data' ] ],
|
67
|
+
:security_groups => :default,
|
68
|
+
:instance_type => 'm1.medium',
|
69
|
+
:region => 'us-east-1'
|
70
|
+
}
|
71
|
+
@aws_default_sg_options = {
|
72
|
+
:region => 'us-east-1'
|
73
|
+
}
|
74
|
+
|
75
|
+
@route53_default_rs_options = {}
|
76
|
+
|
77
|
+
super
|
78
|
+
|
79
|
+
aws_configure
|
80
|
+
aws_read_instances if File.exist?( @aws_instances_json )
|
81
|
+
end
|
82
|
+
|
83
|
+
def aws_configure
|
84
|
+
AWS.config( JSON.parse( IO.read( @aws_config_json ),
|
85
|
+
:symbolize_names => true ) )
|
86
|
+
end
|
87
|
+
|
88
|
+
# Create a security_group given name and options. :region is the
|
89
|
+
# only required option, :description is a good to have. Currently
|
90
|
+
# this is a no-op if the security group already exists.
|
91
|
+
def aws_create_security_group( name, opts = {} )
|
92
|
+
opts = deep_merge_hashes( @aws_default_sg_options, opts )
|
93
|
+
region = opts.delete( :region )
|
94
|
+
ec2 = AWS::EC2.new.regions[ region ]
|
95
|
+
unless ec2.security_groups.find { |sg| sg.name == name }
|
96
|
+
ec2.security_groups.create( name, opts )
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Create an instance, using name as the Name tag and assumed
|
101
|
+
# host name. For options see
|
102
|
+
# {AWS::EC2::InstanceCollection.create}[http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/EC2/InstanceCollection.html#create-instance_method]
|
103
|
+
# with the following additions/differences:
|
104
|
+
#
|
105
|
+
# :count:: must be 1 or unspecified.
|
106
|
+
# :region:: Default 'us-east-1'
|
107
|
+
# :security_groups:: As per aws-sdk, but the special :default value
|
108
|
+
# is replaced with a single security group with
|
109
|
+
# same name as the :region option.
|
110
|
+
# :ebs_volumes:: The number of EBS volumes to create an attach to this instance.
|
111
|
+
# :ebs_volume_options:: A nested Hash of options, as per
|
112
|
+
# {AWS::EC2::VolumeCollection.create}[http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/EC2/VolumeCollection.html#create-instance_method]
|
113
|
+
# with custom default :size 16 GB, and the same
|
114
|
+
# :availibility_zone as the instance.
|
115
|
+
# :lvm_volumes:: Ignored here.
|
116
|
+
# :roles:: Array of role Strings or Symbols (applied as Roles tag)
|
117
|
+
def aws_create_instance( name, opts = {} )
|
118
|
+
opts = deep_merge_hashes( @aws_default_instance_options, opts )
|
119
|
+
region = opts.delete( :region )
|
120
|
+
opts.delete( :lvm_volumes ) #unused here
|
121
|
+
|
122
|
+
ec2 = AWS::EC2.new.regions[ region ]
|
123
|
+
|
124
|
+
iopts = opts.dup
|
125
|
+
iopts.delete( :ebs_volumes )
|
126
|
+
iopts.delete( :ebs_volume_options )
|
127
|
+
iopts.delete( :roles )
|
128
|
+
|
129
|
+
if iopts[ :count ] && iopts[ :count ] != 1
|
130
|
+
raise ":count #{iopts[ :count ]} != 1 is not supported"
|
131
|
+
end
|
132
|
+
|
133
|
+
if iopts[ :security_groups ] == :default
|
134
|
+
iopts[ :security_groups ] = [ region ]
|
135
|
+
end
|
136
|
+
|
137
|
+
inst = ec2.instances.create( iopts )
|
138
|
+
|
139
|
+
inst.add_tag( 'Name', :value => name )
|
140
|
+
|
141
|
+
if opts[ :roles ]
|
142
|
+
inst.add_tag( 'Roles', :value => opts[ :roles ].join(' ') )
|
143
|
+
end
|
144
|
+
|
145
|
+
wait_for_running( inst )
|
146
|
+
|
147
|
+
if opts[ :ebs_volumes ] > 0
|
148
|
+
vopts = { :availability_zone => inst.availability_zone }.
|
149
|
+
merge( opts[ :ebs_volume_options ] )
|
150
|
+
|
151
|
+
attachments = opts[ :ebs_volumes ].times.map do |i|
|
152
|
+
vol = ec2.volumes.create( vopts )
|
153
|
+
vol.attach_to( inst, "/dev/sdh#{i+1}" ) #=> Attachment
|
154
|
+
end
|
155
|
+
|
156
|
+
sleep 1 while attachments.any? { |a| a.status == :attaching }
|
157
|
+
end
|
158
|
+
|
159
|
+
iprops = aws_instance_to_props( region, inst )
|
160
|
+
aws_instance_added( iprops )
|
161
|
+
aws_write_instances
|
162
|
+
iprops
|
163
|
+
end
|
164
|
+
|
165
|
+
# Create a Route53 DNS CNAME from iprops :name to :internet_name.
|
166
|
+
# Options are per {AWS::Route53::ResourceRecordSetCollection.create}[http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/Route53/ResourceRecordSetCollection.html#create-instance_method]
|
167
|
+
# (currently undocumented) with the following addition:
|
168
|
+
#
|
169
|
+
# :domain_name:: name of the hosted zone and suffix for
|
170
|
+
# CNAME. Should terminate in a DOT '.'
|
171
|
+
def route53_create_host_cname( iprops, opts = {} )
|
172
|
+
opts = deep_merge_hashes( @route53_default_rs_options, opts )
|
173
|
+
dname = opts.delete( :domain_name ) #with terminal '.'
|
174
|
+
rs_opts =
|
175
|
+
{ :ttl => 300 }.
|
176
|
+
merge( opts ).
|
177
|
+
merge( :resource_records => [ { :value => iprops[:internet_name] } ] )
|
178
|
+
|
179
|
+
r53 = AWS::Route53.new
|
180
|
+
zone = r53.hosted_zones.find { |hz| hz.name == dname } or
|
181
|
+
raise "Route53 Hosted Zone name #{dname} not found"
|
182
|
+
zone.rrsets.create( [ iprops[:name], dname ].join('.'), 'CNAME', rs_opts )
|
183
|
+
end
|
184
|
+
|
185
|
+
# Terminates an instance and permanently deletes any EBS volumes
|
186
|
+
# attached to /dev/sdh like via create. WARNING: potential for data
|
187
|
+
# loss! The minimum required propererties in iprops hash are :region
|
188
|
+
# and :id.
|
189
|
+
def aws_terminate_instance_and_ebs_volumes( iprops )
|
190
|
+
ec2 = AWS::EC2.new.regions[ iprops[ :region ] ]
|
191
|
+
inst = ec2.instances[ iprops[ :id ] ]
|
192
|
+
unless inst.exists?
|
193
|
+
raise "Instance #{iprops[:id]} does not exist in #{iprops[:region]}"
|
194
|
+
end
|
195
|
+
|
196
|
+
ebs_volumes = inst.block_devices.map do |dev|
|
197
|
+
ebs = dev[ :ebs ]
|
198
|
+
if ebs && dev[:device_name] =~ /dh\d+$/ && !ebs[:delete_on_termination]
|
199
|
+
ebs[ :volume_id ]
|
200
|
+
end
|
201
|
+
end.compact
|
202
|
+
|
203
|
+
puts "Terminating instance #{inst.id}"
|
204
|
+
inst.terminate
|
205
|
+
sleep 1 while inst.status != :terminated
|
206
|
+
|
207
|
+
ebs_volumes = ebs_volumes.map do |vid|
|
208
|
+
volume = ec2.volumes[ vid ]
|
209
|
+
if volume.exists?
|
210
|
+
volume
|
211
|
+
else
|
212
|
+
puts "WARN: #{volume} doesn't exist"
|
213
|
+
nil
|
214
|
+
end
|
215
|
+
end.compact
|
216
|
+
|
217
|
+
ebs_volumes.each do |vol|
|
218
|
+
puts "Deleting vol #{vol.id} (#{vol.status})"
|
219
|
+
sleep 1 until vol.status == :available || vol.status == :deleted
|
220
|
+
vol.delete if vol.status == :available
|
221
|
+
end
|
222
|
+
|
223
|
+
found = aws_find_instance( iprops )
|
224
|
+
if found
|
225
|
+
aws_instance_removed( found )
|
226
|
+
aws_write_instances
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# Return instance properties, by example via iprops, either by [
|
231
|
+
# :id, :region ], :internet_name, :internet_ip, or :name, attempted
|
232
|
+
# in that order.
|
233
|
+
def aws_find_instance( iprops )
|
234
|
+
if iprops[:id]
|
235
|
+
@aws_instances.find do |r|
|
236
|
+
r[:id] == iprops[:id] && r[:region] == iprops[:region]
|
237
|
+
end
|
238
|
+
else
|
239
|
+
[ :internet_name, :internet_ip, :name ].inject( nil ) do |found, key|
|
240
|
+
found || @aws_instances.find { |r| r[key] == iprops[key] }
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def aws_instance_added( inst )
|
246
|
+
@aws_instances << inst
|
247
|
+
@aws_instances.sort! { |p,n| p[:name] <=> n[:name] }
|
248
|
+
end
|
249
|
+
|
250
|
+
def aws_instance_removed( iprops )
|
251
|
+
@aws_instances.reject! do |row|
|
252
|
+
row[:id] == iprops[:id] && row[:region] == iprops[:region]
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def wait_for_running( inst )
|
257
|
+
while ( stat = inst.status ) == :pending
|
258
|
+
puts "Waiting 1s for instance to run"
|
259
|
+
sleep 1
|
260
|
+
end
|
261
|
+
unless stat == :running
|
262
|
+
raise "Instance #{inst.id} has status #{stat}"
|
263
|
+
end
|
264
|
+
nil
|
265
|
+
end
|
266
|
+
|
267
|
+
# Find running/pending instances in each of aws_regions, convert to
|
268
|
+
# props, and save in aws_instances list.
|
269
|
+
def aws_import_instances
|
270
|
+
|
271
|
+
instances = []
|
272
|
+
|
273
|
+
aws_regions.each do |region|
|
274
|
+
ec2 = AWS::EC2.new.regions[ region ]
|
275
|
+
|
276
|
+
found = ec2.instances.map do |inst|
|
277
|
+
next unless [ :running, :pending ].include?( inst.status )
|
278
|
+
aws_instance_to_props( region, inst )
|
279
|
+
end
|
280
|
+
|
281
|
+
instances += found.compact
|
282
|
+
end
|
283
|
+
|
284
|
+
@aws_instances = instances
|
285
|
+
end
|
286
|
+
|
287
|
+
def aws_instance_to_props( region, inst )
|
288
|
+
tags = inst.tags.to_h
|
289
|
+
name = tags[ 'Name' ]
|
290
|
+
roles = decode_roles( tags[ 'Roles' ] )
|
291
|
+
|
292
|
+
{ :id => inst.id,
|
293
|
+
:region => region,
|
294
|
+
:ami => inst.image_id,
|
295
|
+
:name => name,
|
296
|
+
:internet_name => inst.dns_name,
|
297
|
+
:internet_ip => inst.ip_address,
|
298
|
+
:internal_ip => inst.private_ip_address,
|
299
|
+
:instance_type => inst.instance_type,
|
300
|
+
:roles => roles }
|
301
|
+
end
|
302
|
+
|
303
|
+
# Read the aws_instances_json file, replacing in RAM AWS instances
|
304
|
+
def aws_read_instances
|
305
|
+
list = JSON.parse( IO.read( aws_instances_json ), :symbolize_names => true )
|
306
|
+
list.each do |inst|
|
307
|
+
inst[:roles] = ( inst[:roles] || [] ).map { |r| r.to_sym }
|
308
|
+
end
|
309
|
+
@aws_instances = list.sort { |p,n| p[:name] <=> n[:name] }
|
310
|
+
end
|
311
|
+
|
312
|
+
# Overwrite aws_instances_json file with the current AWS instances.
|
313
|
+
def aws_write_instances
|
314
|
+
File.open( aws_instances_json, 'w' ) do |fout|
|
315
|
+
aws_dump_instances( fout )
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
# Dump AWS instances as JSON but with custom layout of host per line.
|
320
|
+
def aws_dump_instances( fout = $stdout )
|
321
|
+
fout.puts '['
|
322
|
+
rows = @aws_instances.sort { |p,n| p[:name] <=> n[:name] }
|
323
|
+
rows.each_with_index do |row, i|
|
324
|
+
fout.puts( " " + JSON.generate( row, :space => ' ', :object_nl => ' ' ) +
|
325
|
+
( ( i == ( rows.length - 1 ) ) ? '' : ',' ) )
|
326
|
+
end
|
327
|
+
fout.puts ']'
|
328
|
+
end
|
329
|
+
|
330
|
+
def decode_roles( roles )
|
331
|
+
( roles || "" ).split( /\s+/ ).map { |r| r.to_sym }
|
332
|
+
end
|
333
|
+
|
334
|
+
# Runs create_lvm_volumes! if the first :lvm_volumes mount path does
|
335
|
+
# not yet exist.
|
336
|
+
def create_lvm_volumes( opts = {} )
|
337
|
+
opts = deep_merge_hashes( @aws_default_instance_options, opts )
|
338
|
+
unless exist?( opts[ :lvm_volumes ].first[1] )
|
339
|
+
create_lvm_volumes!( opts )
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
# Create an mdraid array from previously attached EBS volumes, then
|
344
|
+
# divvy up across N lvm mount points by ratio, creating filesystems and
|
345
|
+
# mounting. This mdadm and lvm recipe was originally derived from
|
346
|
+
# {Install MongoDB on Amazon EC2}[http://docs.mongodb.org/ecosystem/tutorial/install-mongodb-on-amazon-ec2/]
|
347
|
+
#
|
348
|
+
# === Options
|
349
|
+
# :ebs_volumes:: The number of EBS volumes previously created and
|
350
|
+
# attached to this instance.
|
351
|
+
# :lvm_volumes:: A table of [ slice, path (,name) ] rows where;
|
352
|
+
# slice is a floating point ratio in range (0.0,1.0],
|
353
|
+
# path is the mount point, and name is the lvm name,
|
354
|
+
# defaulting if omitted to the last path element. The
|
355
|
+
# sum of all slice values in the table should be 1.0.
|
356
|
+
def create_lvm_volumes!( opts = {} )
|
357
|
+
opts = deep_merge_hashes( @aws_default_instance_options, opts )
|
358
|
+
|
359
|
+
vol_count = opts[ :ebs_volumes ]
|
360
|
+
devices = vol_count.times.map { |i| "/dev/xvdh#{i+1}" }
|
361
|
+
|
362
|
+
cmd = <<-SH
|
363
|
+
#{dist_install_s( "mdadm", "lvm2", :minimal => true )}
|
364
|
+
mdadm --create /dev/md0 --level=10 --chunk=256 --raid-devices=#{vol_count} \
|
365
|
+
#{devices.join ' '}
|
366
|
+
echo "DEVICE #{devices.join ' '}" > /etc/mdadm.conf
|
367
|
+
mdadm --detail --scan >> /etc/mdadm.conf
|
368
|
+
SH
|
369
|
+
|
370
|
+
devices.each do |d|
|
371
|
+
cmd << <<-SH
|
372
|
+
blockdev --setra 128 #{d}
|
373
|
+
SH
|
374
|
+
end
|
375
|
+
|
376
|
+
cmd << <<-SH
|
377
|
+
blockdev --setra 128 /dev/md0
|
378
|
+
dd if=/dev/zero of=/dev/md0 bs=512 count=1
|
379
|
+
pvcreate /dev/md0
|
380
|
+
vgcreate vg0 /dev/md0
|
381
|
+
SH
|
382
|
+
|
383
|
+
opts[ :lvm_volumes ].each do | slice, path, name |
|
384
|
+
name ||= ( path =~ /\/(\w+)$/ ) && $1
|
385
|
+
cmd << <<-SH
|
386
|
+
lvcreate -l #{(slice * 100).round}%vg -n #{name} vg0
|
387
|
+
mke2fs -t ext4 -F /dev/vg0/#{name}
|
388
|
+
if [ -d #{path} ]; then
|
389
|
+
rm -rf #{path}
|
390
|
+
fi
|
391
|
+
mkdir -p #{path}
|
392
|
+
echo '/dev/vg0/#{name} #{path} ext4 defaults,auto,noatime,noexec 0 0' >> /etc/fstab
|
393
|
+
mount #{path}
|
394
|
+
SH
|
395
|
+
end
|
396
|
+
|
397
|
+
sudo cmd
|
398
|
+
end
|
399
|
+
|
400
|
+
end
|
data/lib/syncwrap/base.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: syncwrap
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.5.
|
4
|
+
version: 1.5.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -84,6 +84,7 @@ files:
|
|
84
84
|
- etc/sysctl.d/61-postgresql-shm.conf
|
85
85
|
- lib/syncwrap/base.rb
|
86
86
|
- lib/syncwrap.rb
|
87
|
+
- lib/syncwrap/aws.rb
|
87
88
|
- lib/syncwrap/common.rb
|
88
89
|
- lib/syncwrap/distro.rb
|
89
90
|
- lib/syncwrap/ec2.rb
|
@@ -127,7 +128,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
127
128
|
version: '0'
|
128
129
|
segments:
|
129
130
|
- 0
|
130
|
-
hash: -
|
131
|
+
hash: -4463767626501128078
|
131
132
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
132
133
|
none: false
|
133
134
|
requirements:
|
@@ -136,7 +137,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
136
137
|
version: '0'
|
137
138
|
segments:
|
138
139
|
- 0
|
139
|
-
hash: -
|
140
|
+
hash: -4463767626501128078
|
140
141
|
requirements: []
|
141
142
|
rubyforge_project:
|
142
143
|
rubygems_version: 1.8.25
|