syncwrap 1.5.0 → 1.5.1
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/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
|