syncwrap 1.5.2 → 2.0.0
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.
- checksums.yaml +7 -0
- data/History.rdoc +19 -0
- data/Manifest.txt +82 -34
- data/README.rdoc +96 -48
- data/Rakefile +0 -65
- data/bin/syncwrap +27 -0
- data/examples/LAYOUT.rdoc +70 -0
- data/examples/Rakefile +16 -0
- data/examples/ec2.rb +44 -0
- data/examples/hello.rb +14 -0
- data/examples/hello_binding.rb +27 -0
- data/examples/jruby.rb +11 -0
- data/examples/private/aws.json +4 -0
- data/examples/rput.rb +24 -0
- data/examples/sync/home/bob/.ssh/authorized_keys +1 -0
- data/examples/sync/tmp/sample.erb +3 -0
- data/lib/syncwrap/amazon_ec2.rb +236 -0
- data/lib/syncwrap/amazon_ws.rb +308 -0
- data/lib/syncwrap/base.rb +4 -2
- data/lib/syncwrap/cli.rb +328 -0
- data/lib/syncwrap/component.rb +443 -0
- data/lib/syncwrap/components/commercial_jdk.rb +76 -0
- data/lib/syncwrap/components/cruby_vm.rb +144 -0
- data/lib/syncwrap/components/etc_hosts.rb +44 -0
- data/lib/syncwrap/{geminabox.rb → components/geminabox.rb} +12 -17
- data/lib/syncwrap/components/hashdot.rb +97 -0
- data/lib/syncwrap/components/iyyov.rb +144 -0
- data/lib/syncwrap/components/iyyov_daemon.rb +125 -0
- data/lib/syncwrap/components/jruby_vm.rb +122 -0
- data/lib/syncwrap/components/mdraid.rb +204 -0
- data/lib/syncwrap/components/network.rb +99 -0
- data/lib/syncwrap/components/open_jdk.rb +70 -0
- data/lib/syncwrap/components/postgresql.rb +159 -0
- data/lib/syncwrap/components/qpid.rb +303 -0
- data/lib/syncwrap/components/rhel.rb +71 -0
- data/lib/syncwrap/components/run_user.rb +99 -0
- data/lib/syncwrap/components/ubuntu.rb +85 -0
- data/lib/syncwrap/components/users.rb +200 -0
- data/lib/syncwrap/context.rb +260 -0
- data/lib/syncwrap/distro.rb +53 -60
- data/lib/syncwrap/formatter.rb +149 -0
- data/lib/syncwrap/host.rb +134 -0
- data/lib/syncwrap/main.rb +62 -0
- data/lib/syncwrap/path_util.rb +55 -0
- data/lib/syncwrap/rsync.rb +227 -0
- data/lib/syncwrap/ruby_support.rb +110 -0
- data/lib/syncwrap/shell.rb +207 -0
- data/lib/syncwrap.rb +367 -1
- data/{etc → sync/etc}/gemrc +1 -3
- data/sync/etc/hosts.erb +8 -0
- data/{etc/init.d/iyyov → sync/etc/init.d/iyyov.erb} +35 -7
- data/sync/etc/sysconfig/pgsql/postgresql.erb +2 -0
- data/sync/src/hashdot/Makefile.erb +98 -0
- data/sync/src/hashdot/profiles/default.hdp.erb +25 -0
- data/sync/src/hashdot/profiles/jruby-common.hdp +28 -0
- data/sync/src/hashdot/profiles/jruby-shortlived.hdp +9 -0
- data/sync/src/hashdot/profiles/jruby.hdp.erb +13 -0
- data/sync/src/hashdot/profiles/shortlived.hdp +6 -0
- data/sync/var/iyyov/default/config.rb +1 -0
- data/sync/var/iyyov/default/daemon.rb.erb +15 -0
- data/sync/var/iyyov/jobs.rb.erb +4 -0
- data/test/muddled_sync.rb +13 -0
- data/test/setup.rb +39 -0
- data/test/sync/d1/bar +1 -0
- data/test/sync/d1/foo.erb +1 -0
- data/test/sync/d3/d2/bar +1 -0
- data/test/sync/d3/d2/foo.erb +1 -0
- data/test/test_components.rb +108 -0
- data/test/test_context.rb +107 -0
- data/test/test_context_rput.rb +289 -0
- data/test/test_rsync.rb +138 -0
- data/test/test_shell.rb +233 -0
- data/test/test_space.rb +218 -0
- data/test/test_space_main.rb +40 -0
- data/test/zfile +1 -0
- metadata +204 -71
- data/etc/sysconfig/pgsql/postgresql +0 -2
- data/lib/syncwrap/aws.rb +0 -448
- data/lib/syncwrap/common.rb +0 -161
- data/lib/syncwrap/ec2.rb +0 -59
- data/lib/syncwrap/hashdot.rb +0 -70
- data/lib/syncwrap/iyyov.rb +0 -139
- data/lib/syncwrap/java.rb +0 -61
- data/lib/syncwrap/jruby.rb +0 -118
- data/lib/syncwrap/postgresql.rb +0 -135
- data/lib/syncwrap/qpid.rb +0 -251
- data/lib/syncwrap/remote_task.rb +0 -199
- data/lib/syncwrap/rhel.rb +0 -67
- data/lib/syncwrap/ubuntu.rb +0 -78
- data/lib/syncwrap/user_run.rb +0 -102
- data/test/test_syncwrap.rb +0 -202
- data/var/iyyov/jobs.rb +0 -11
- /data/{etc → sync/etc}/corosync/corosync.conf +0 -0
- /data/{etc → sync/etc}/corosync/uidgid.d/qpid +0 -0
- /data/{etc → sync/etc}/init.d/qpidd +0 -0
- /data/{etc → sync/etc}/sysctl.d/61-postgresql-shm.conf +0 -0
- /data/{usr/local → sync/jruby}/bin/jgem +0 -0
- /data/{postgresql → sync/postgresql}/rhel/pg_hba.conf +0 -0
- /data/{postgresql → sync/postgresql}/rhel/pg_ident.conf +0 -0
- /data/{postgresql → sync/postgresql}/rhel/postgresql.conf +0 -0
- /data/{postgresql → sync/postgresql}/ubuntu/environment +0 -0
- /data/{postgresql → sync/postgresql}/ubuntu/pg_ctl.conf +0 -0
- /data/{postgresql → sync/postgresql}/ubuntu/pg_hba.conf +0 -0
- /data/{postgresql → sync/postgresql}/ubuntu/pg_ident.conf +0 -0
- /data/{postgresql → sync/postgresql}/ubuntu/postgresql.conf +0 -0
- /data/{postgresql → sync/postgresql}/ubuntu/start.conf +0 -0
- /data/{usr → sync/usr}/local/etc/qpidd.conf +0 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# Copyright (c) 2011-2014 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 'time'
|
|
18
|
+
|
|
19
|
+
require 'syncwrap/amazon_ws'
|
|
20
|
+
require 'syncwrap/host'
|
|
21
|
+
|
|
22
|
+
module SyncWrap
|
|
23
|
+
|
|
24
|
+
# Amazon EC2 host provider. Supports importing, creating, and
|
|
25
|
+
# terminating hosts.
|
|
26
|
+
#
|
|
27
|
+
# == Synopsis
|
|
28
|
+
#
|
|
29
|
+
# Add the following to your sync.rb
|
|
30
|
+
#
|
|
31
|
+
# ec2 = AmazonEC2.new( space )
|
|
32
|
+
#
|
|
33
|
+
# Then add profiles via #profile, as needed.
|
|
34
|
+
#
|
|
35
|
+
class AmazonEC2
|
|
36
|
+
include AmazonWS
|
|
37
|
+
|
|
38
|
+
# FIXME: Interim strategy: use AmazonWS and defer deciding final
|
|
39
|
+
# organization.
|
|
40
|
+
|
|
41
|
+
# The json configuration file path, parsed and passed to
|
|
42
|
+
# AWS::config. This file should contain a json object with the
|
|
43
|
+
# minimal required keys: access_key_id, secret_access_key. A
|
|
44
|
+
# relative path is interpreted relative to the :sync_file_path
|
|
45
|
+
# option if provided on construction. (default: private/aws.json)
|
|
46
|
+
attr_accessor :aws_config
|
|
47
|
+
|
|
48
|
+
def initialize( space, opts = {} )
|
|
49
|
+
super()
|
|
50
|
+
@profiles = {}
|
|
51
|
+
@space = space
|
|
52
|
+
@aws_config = 'private/aws.json'
|
|
53
|
+
opts = opts.dup
|
|
54
|
+
sync_file_path = opts.delete( :sync_file_path )
|
|
55
|
+
opts.each { |name, val| send( name.to_s + '=', val ) }
|
|
56
|
+
|
|
57
|
+
# Look up aws_config (i.e. default private/aws.json) relative to
|
|
58
|
+
# the sync file path.
|
|
59
|
+
if @aws_config
|
|
60
|
+
if sync_file_path
|
|
61
|
+
aws_configure( File.expand_path( @aws_config, sync_file_path ) )
|
|
62
|
+
else
|
|
63
|
+
aws_configure( @aws_config )
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Define a host profile by Symbol key and Hash value.
|
|
69
|
+
#
|
|
70
|
+
# Profiles may inherit properties from a :base_profile, either
|
|
71
|
+
# specified by that key, or the :default key profile. The
|
|
72
|
+
# base_profile must be defined in advance (above in the sync
|
|
73
|
+
# file). When merging profile to any base_profile, the :roles
|
|
74
|
+
# property is concatenated via set union. All other properties are
|
|
75
|
+
# overwritten.
|
|
76
|
+
#
|
|
77
|
+
# FIXME: All other profile properties are as currently defined by
|
|
78
|
+
# #aws_create_instance.
|
|
79
|
+
def profile( key, profile )
|
|
80
|
+
profile = profile.dup
|
|
81
|
+
base = profile.delete( :base_profile ) || :default
|
|
82
|
+
base_profile = @profiles[ base ]
|
|
83
|
+
if base_profile
|
|
84
|
+
profile = base_profile.merge( profile )
|
|
85
|
+
if base_profile[ :roles ] && profile[ :roles ]
|
|
86
|
+
profile[ :roles ] = ( base_profile[ :roles ] | profile[ :roles ] )
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
@profiles[ key ] = profile
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def import_hosts( regions, output_file )
|
|
94
|
+
hlist = import_host_props( regions )
|
|
95
|
+
unless hlist.empty?
|
|
96
|
+
|
|
97
|
+
hlist.map! do |props|
|
|
98
|
+
props[:name] ||= props[:id].to_s
|
|
99
|
+
Host.new( space, props )
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
time = Time.now.utc
|
|
103
|
+
cmt = "\n# Import of AWS #{regions.join ','} on #{time.iso8601}"
|
|
104
|
+
append_host_definitions( hlist, cmt, output_file )
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def create_hosts( count, profile_key, name, output_file )
|
|
109
|
+
profile = @profiles[ profile_key ].dup or
|
|
110
|
+
raise "Profile #{profile_key} not registered"
|
|
111
|
+
|
|
112
|
+
# FIXME: Support profiles overrides.
|
|
113
|
+
# Also add some targeted CLI overrides (like for :availability_zones)
|
|
114
|
+
|
|
115
|
+
if profile[ :user_data ] == :ec2_user_sudo
|
|
116
|
+
profile[ :user_data ] = ec2_user_data
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
dname = profile.delete( :default_name )
|
|
120
|
+
name ||= dname
|
|
121
|
+
|
|
122
|
+
count.times do
|
|
123
|
+
hname = if count == 1
|
|
124
|
+
if space.host_names.include?( name )
|
|
125
|
+
raise "Host #{name} already exists!"
|
|
126
|
+
end
|
|
127
|
+
name
|
|
128
|
+
else
|
|
129
|
+
find_name( name )
|
|
130
|
+
end
|
|
131
|
+
props = aws_create_instance( hname, profile )
|
|
132
|
+
host = space.host( props )
|
|
133
|
+
append_host_definitions( [ host ], nil, output_file )
|
|
134
|
+
host[ :just_created ] = true #after so this isn't written
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def terminate_hosts( names, delete_attached_storage, sync_file )
|
|
139
|
+
names.each do |name|
|
|
140
|
+
if space.host_names.include?( name )
|
|
141
|
+
host = space.host( name )
|
|
142
|
+
raise "Host #{name} missing :id" unless host[:id]
|
|
143
|
+
raise "Host #{name} missing :region" unless host[:region]
|
|
144
|
+
aws_terminate_instance( host, delete_attached_storage )
|
|
145
|
+
delete_host_definition( host, sync_file )
|
|
146
|
+
else
|
|
147
|
+
raise "Host #{name} not found in Space, sync file."
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
attr_reader :space
|
|
155
|
+
|
|
156
|
+
def find_name( prefix )
|
|
157
|
+
host_names = space.host_names
|
|
158
|
+
i = 1
|
|
159
|
+
name = nil
|
|
160
|
+
loop do
|
|
161
|
+
name = "%s-%2d" % [ prefix, i ]
|
|
162
|
+
break if ! host_names.include?( name )
|
|
163
|
+
i += 1
|
|
164
|
+
end
|
|
165
|
+
name
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def append_host_definitions( hosts, comment, output_file )
|
|
169
|
+
File.open( output_file, "a" ) do |out|
|
|
170
|
+
out.puts comment if comment
|
|
171
|
+
|
|
172
|
+
hosts.each do |host|
|
|
173
|
+
props = host.props.dup
|
|
174
|
+
props.delete( :name )
|
|
175
|
+
|
|
176
|
+
roles = ( host.roles - [:all] ).map { |s| ':' + s.to_s }.join ', '
|
|
177
|
+
roles << ',' unless roles.empty?
|
|
178
|
+
props = host.props.map do |key,val|
|
|
179
|
+
"#{key}: #{val.inspect}" unless key == :name
|
|
180
|
+
end.compact.join ",\n "
|
|
181
|
+
|
|
182
|
+
out.puts "host( '#{host.name}', #{roles}"
|
|
183
|
+
out.puts " #{props} ) # :auto-generated"
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def delete_host_definition( host, sync_file )
|
|
189
|
+
lines = IO.readlines( sync_file )
|
|
190
|
+
out_lines = []
|
|
191
|
+
state = :find
|
|
192
|
+
lines.each do |line|
|
|
193
|
+
if state == :find
|
|
194
|
+
if line =~ /^host\( '#{host.name}',/
|
|
195
|
+
state = :just_found
|
|
196
|
+
else
|
|
197
|
+
out_lines << line
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
if state == :just_found || state == :found
|
|
201
|
+
if state == :found && line =~ /^host\(/
|
|
202
|
+
state = :no_end
|
|
203
|
+
break
|
|
204
|
+
end
|
|
205
|
+
state = :found
|
|
206
|
+
if line =~ /^\s*[^#].*\) # :auto-generated$/
|
|
207
|
+
state = :deleted
|
|
208
|
+
end
|
|
209
|
+
elsif state == :deleted
|
|
210
|
+
out_lines << line
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
if state == :deleted
|
|
215
|
+
File.open( sync_file, "w" ) do |out|
|
|
216
|
+
out.puts out_lines
|
|
217
|
+
end
|
|
218
|
+
else
|
|
219
|
+
$stderr.puts( "WARNING: #{sync_file} entry not deleted (#{state})" )
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def ec2_user_data( user = 'ec2-user' )
|
|
224
|
+
#FIXME: Utility module for this (+ Users sudoers)?
|
|
225
|
+
script = <<-SH
|
|
226
|
+
#!/bin/sh -e
|
|
227
|
+
echo '#{user} ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/#{user}
|
|
228
|
+
echo 'Defaults:#{user} !requiretty' >> /etc/sudoers.d/#{user}
|
|
229
|
+
chmod 440 /etc/sudoers.d/#{user}
|
|
230
|
+
SH
|
|
231
|
+
script.split( "\n" ).map { |l| l.strip }.join( "\n" )
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
end
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# Copyright (c) 2011-2014 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 'aws-sdk'
|
|
18
|
+
require 'resolv'
|
|
19
|
+
require 'json'
|
|
20
|
+
|
|
21
|
+
module SyncWrap
|
|
22
|
+
|
|
23
|
+
# Supports host provisioning in EC2 via AWS APIs, creating and
|
|
24
|
+
# attaching EBS volumes, and creating Route53 record sets.
|
|
25
|
+
module AmazonWS
|
|
26
|
+
|
|
27
|
+
# Default options for Route53 record set creation
|
|
28
|
+
attr_accessor :route53_default_rs_options
|
|
29
|
+
|
|
30
|
+
# DNS Resolver options for testing Route53 (default: Use public
|
|
31
|
+
# google name servers to avoid local negative caching)
|
|
32
|
+
attr_accessor :resolver_options
|
|
33
|
+
|
|
34
|
+
def initialize
|
|
35
|
+
@default_instance_options = {
|
|
36
|
+
ebs_volumes: 0,
|
|
37
|
+
ebs_volume_options: { size: 16 }, #gb
|
|
38
|
+
lvm_volumes: [ [ 1.00, '/data' ] ],
|
|
39
|
+
security_groups: [ :default ],
|
|
40
|
+
instance_type: 'm1.medium',
|
|
41
|
+
region: 'us-east-1'
|
|
42
|
+
}
|
|
43
|
+
@route53_default_rs_options = {
|
|
44
|
+
ttl: 300,
|
|
45
|
+
wait: true
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@resolver_options = {
|
|
49
|
+
nameserver: [ '8.8.8.8', '8.8.4.4' ]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
super
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
protected
|
|
56
|
+
|
|
57
|
+
def aws_configure( json_file )
|
|
58
|
+
AWS.config( JSON.parse( IO.read( json_file ),
|
|
59
|
+
symbolize_names: true ) )
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Create a security_group given name and options. :region is the
|
|
63
|
+
# only required option, :description is good to have. Currently
|
|
64
|
+
# this is a no-op if the security group already exists.
|
|
65
|
+
def aws_create_security_group( name, opts = {} )
|
|
66
|
+
opts = opts.dup
|
|
67
|
+
region = opts.delete( :region )
|
|
68
|
+
ec2 = AWS::EC2.new.regions[ region ]
|
|
69
|
+
unless ec2.security_groups.find { |sg| sg.name == name }
|
|
70
|
+
sg = ec2.security_groups.create( name, opts )
|
|
71
|
+
|
|
72
|
+
# FIXME: Allow ssh on the "default" region named group
|
|
73
|
+
if name == region
|
|
74
|
+
sg.authorize_ingress(:tcp, 22)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Create an instance, using name as the Name tag and assumed
|
|
80
|
+
# host name. For options see
|
|
81
|
+
# {AWS::EC2::InstanceCollection.create}[http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/EC2/InstanceCollection.html#create-instance_method]
|
|
82
|
+
# with the following additions/differences:
|
|
83
|
+
#
|
|
84
|
+
# :count:: must be 1 or unspecified.
|
|
85
|
+
# :region:: Default 'us-east-1'
|
|
86
|
+
# :security_groups:: As per aws-sdk, but the special :default value
|
|
87
|
+
# is replaced with a single security group with
|
|
88
|
+
# same name as the :region.
|
|
89
|
+
# :ebs_volumes:: The number of EBS volumes to create an attach to this instance.
|
|
90
|
+
# :ebs_volume_options:: A nested Hash of options, as per
|
|
91
|
+
# {AWS::EC2::VolumeCollection.create}[http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/EC2/VolumeCollection.html#create-instance_method]
|
|
92
|
+
# with custom default :size 16 GB, and the same
|
|
93
|
+
# :availibility_zone as the instance.
|
|
94
|
+
# :lvm_volumes:: Ignored here.
|
|
95
|
+
# :roles:: Array of role Strings or Symbols (applied as Roles tag)
|
|
96
|
+
def aws_create_instance( name, opts = {} )
|
|
97
|
+
opts = deep_merge_hashes( @default_instance_options, opts )
|
|
98
|
+
region = opts.delete( :region )
|
|
99
|
+
opts.delete( :lvm_volumes ) #unused here
|
|
100
|
+
|
|
101
|
+
ec2 = AWS::EC2.new.regions[ region ]
|
|
102
|
+
|
|
103
|
+
iopts = opts.dup
|
|
104
|
+
iopts.delete( :ebs_volumes )
|
|
105
|
+
iopts.delete( :ebs_volume_options )
|
|
106
|
+
iopts.delete( :roles )
|
|
107
|
+
|
|
108
|
+
if iopts[ :count ] && iopts[ :count ] != 1
|
|
109
|
+
raise ":count #{iopts[ :count ]} != 1 is not supported"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
iopts[ :security_groups ].map! do |sg|
|
|
113
|
+
sg == :default ? region : sg
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
iopts[ :security_groups ].each do |sg|
|
|
117
|
+
aws_create_security_group( sg, region: region )
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
inst = ec2.instances.create( iopts )
|
|
121
|
+
|
|
122
|
+
inst.add_tag( 'Name', value: name )
|
|
123
|
+
|
|
124
|
+
if opts[ :roles ]
|
|
125
|
+
inst.add_tag( 'Roles', value: opts[ :roles ].join(' ') )
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
wait_for_running( inst )
|
|
129
|
+
|
|
130
|
+
# FIXME: Split method
|
|
131
|
+
# FIXME: Support alternative syntax, i.e
|
|
132
|
+
# { ebs_volumes: [ [4, size: 48], [2, size: 8] ] }
|
|
133
|
+
|
|
134
|
+
if opts[ :ebs_volumes ] > 0
|
|
135
|
+
vopts = { availability_zone: inst.availability_zone }.
|
|
136
|
+
merge( opts[ :ebs_volume_options ] )
|
|
137
|
+
|
|
138
|
+
attachments = opts[ :ebs_volumes ].times.map do |i|
|
|
139
|
+
vol = ec2.volumes.create( vopts )
|
|
140
|
+
wait_until( vol.id, 0.5 ) { vol.status == :available }
|
|
141
|
+
vol.attach_to( inst, "/dev/sdh#{i+1}" ) #=> Attachment
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
wait_until( "volumes to attach" ) do
|
|
145
|
+
!( attachments.any? { |a| a.status == :attaching } )
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
#FIXME: end
|
|
149
|
+
|
|
150
|
+
instance_to_props( region, inst )
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Create a Route53 DNS CNAME from iprops :name to :internet_name.
|
|
154
|
+
# Options are per {AWS::Route53::ResourceRecordSetCollection.create}[http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/Route53/ResourceRecordSetCollection.html#create-instance_method]
|
|
155
|
+
# (currently undocumented) with the following additions:
|
|
156
|
+
#
|
|
157
|
+
# :domain_name:: name of the hosted zone and suffix for
|
|
158
|
+
# CNAME. Should terminate in a DOT '.'
|
|
159
|
+
# :wait:: If true, wait for CNAME to resolve
|
|
160
|
+
def route53_create_host_cname( iprops, opts = {} )
|
|
161
|
+
opts = deep_merge_hashes( @route53_default_rs_options, opts )
|
|
162
|
+
dname = dot_terminate( opts.delete( :domain_name ) )
|
|
163
|
+
do_wait = opts.delete( :wait )
|
|
164
|
+
rs_opts = opts.
|
|
165
|
+
merge( resource_records: [ {value: iprops[:internet_name]} ] )
|
|
166
|
+
|
|
167
|
+
r53 = AWS::Route53.new
|
|
168
|
+
zone = r53.hosted_zones.find { |hz| hz.name == dname } or
|
|
169
|
+
raise "Route53 Hosted Zone name #{dname} not found"
|
|
170
|
+
long_name = [ iprops[:name], dname ].join('.')
|
|
171
|
+
zone.rrsets.create( long_name, 'CNAME', rs_opts )
|
|
172
|
+
wait_for_dns_resolve( long_name, dname ) if do_wait
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def wait_for_dns_resolve( long_name,
|
|
176
|
+
domain,
|
|
177
|
+
rtype = Resolv::DNS::Resource::IN::CNAME )
|
|
178
|
+
|
|
179
|
+
ns_addr = Resolv::DNS.open( @resolver_options ) do |rvr|
|
|
180
|
+
ns_n = rvr.getresource( domain, Resolv::DNS::Resource::IN::SOA ).mname
|
|
181
|
+
rvr.getaddress( ns_n ).to_s
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
sleep 3 # Initial wait
|
|
185
|
+
|
|
186
|
+
wait_until( "#{long_name} to resolve", 3.0 ) do
|
|
187
|
+
Resolv::DNS.open( nameserver: ns_addr ) do |rvr|
|
|
188
|
+
rvr.getresources( long_name, rtype ).first
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Terminate an instance and wait for it to be terminated. If
|
|
194
|
+
# requested, /dev/sdh# attached EBS volumes which are not
|
|
195
|
+
# otherwise marked for :delete_on_termination will _also_ be
|
|
196
|
+
# terminated. The minimum required properties in iprops are
|
|
197
|
+
# :region and :id.
|
|
198
|
+
#
|
|
199
|
+
# _WARNING_: data _will_ be lost!
|
|
200
|
+
def aws_terminate_instance( iprops, delete_attached_storage = false )
|
|
201
|
+
ec2 = AWS::EC2.new.regions[ iprops[ :region ] ]
|
|
202
|
+
inst = ec2.instances[ iprops[ :id ] ]
|
|
203
|
+
unless inst.exists?
|
|
204
|
+
raise "Instance #{iprops[:id]} does not exist in #{iprops[:region]}"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
ebs_volumes = []
|
|
208
|
+
if delete_attached_storage
|
|
209
|
+
ebs_volumes = inst.block_devices.map do |dev|
|
|
210
|
+
ebs = dev[ :ebs ]
|
|
211
|
+
if ebs && dev[:device_name] =~ /dh\d+$/ && !ebs[:delete_on_termination]
|
|
212
|
+
ebs[ :volume_id ]
|
|
213
|
+
end
|
|
214
|
+
end.compact
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
inst.terminate
|
|
218
|
+
wait_until( "termination of #{inst.id}", 2.0 ) { inst.status == :terminated }
|
|
219
|
+
|
|
220
|
+
ebs_volumes = ebs_volumes.map do |vid|
|
|
221
|
+
volume = ec2.volumes[ vid ]
|
|
222
|
+
if volume.exists?
|
|
223
|
+
volume
|
|
224
|
+
else
|
|
225
|
+
puts "WARN: #{volume} doesn't exist"
|
|
226
|
+
nil
|
|
227
|
+
end
|
|
228
|
+
end.compact
|
|
229
|
+
|
|
230
|
+
ebs_volumes.each do |vol|
|
|
231
|
+
wait_until( "deletion of vol #{vol.id}" ) do
|
|
232
|
+
vol.status == :available || vol.status == :deleted
|
|
233
|
+
end
|
|
234
|
+
vol.delete if vol.status == :available
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def wait_for_running( inst )
|
|
240
|
+
wait_until( "instance #{inst.id} to run", 2.0 ) { inst.status != :pending }
|
|
241
|
+
stat = inst.status
|
|
242
|
+
raise "Instance #{inst.id} has status #{stat}" unless stat == :running
|
|
243
|
+
nil
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Find running or pending instances in each region String and
|
|
247
|
+
# convert to a HostList.
|
|
248
|
+
def import_host_props( regions )
|
|
249
|
+
regions.inject([]) do |insts, region|
|
|
250
|
+
ec2 = AWS::EC2.new.regions[ region ]
|
|
251
|
+
|
|
252
|
+
found = ec2.instances.map do |inst|
|
|
253
|
+
next unless [ :running, :pending ].include?( inst.status )
|
|
254
|
+
instance_to_props( region, inst )
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
insts + found.compact
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def instance_to_props( region, inst )
|
|
263
|
+
tags = inst.tags.to_h
|
|
264
|
+
|
|
265
|
+
{ id: inst.id,
|
|
266
|
+
region: region,
|
|
267
|
+
ami: inst.image_id,
|
|
268
|
+
name: tags[ 'Name' ],
|
|
269
|
+
internet_name: inst.dns_name,
|
|
270
|
+
internet_ip: inst.ip_address,
|
|
271
|
+
internal_ip: inst.private_ip_address,
|
|
272
|
+
instance_type: inst.instance_type,
|
|
273
|
+
roles: decode_roles( tags[ 'Roles' ] ) }
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def decode_roles( roles )
|
|
277
|
+
( roles || "" ).split( /\s+/ ).map { |r| r.to_sym }
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Wait until block returns truthy, sleeping for freq seconds
|
|
281
|
+
# between attempts. Writes desc and a sequence of DOTs on a single
|
|
282
|
+
# line until complete.
|
|
283
|
+
def wait_until( desc, freq = 1.0 )
|
|
284
|
+
$stdout.write( "Waiting for " + desc )
|
|
285
|
+
until (ret = yield) do
|
|
286
|
+
$stdout.write '.'
|
|
287
|
+
sleep freq
|
|
288
|
+
end
|
|
289
|
+
ret
|
|
290
|
+
ensure
|
|
291
|
+
puts
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def dot_terminate( name )
|
|
295
|
+
( name =~ /\.$/ ) ? name : ( name + '.' )
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def deep_merge_hashes( h1, h2 )
|
|
299
|
+
h1.merge( h2 ) do |key, v1, v2|
|
|
300
|
+
if v1.is_a?( Hash ) && v2.is_a?( Hash )
|
|
301
|
+
deep_merge_hashes( v1, v2 )
|
|
302
|
+
else
|
|
303
|
+
v2
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
data/lib/syncwrap/base.rb
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#--
|
|
2
|
-
# Copyright (c) 2011-
|
|
2
|
+
# Copyright (c) 2011-2014 David Kellum
|
|
3
3
|
#
|
|
4
4
|
# Licensed under the Apache License, Version 2.0 (the "License"); you
|
|
5
5
|
# may not use this file except in compliance with the License. You may
|
|
@@ -15,5 +15,7 @@
|
|
|
15
15
|
#++
|
|
16
16
|
|
|
17
17
|
module SyncWrap
|
|
18
|
-
VERSION='
|
|
18
|
+
VERSION='2.0.0'
|
|
19
|
+
|
|
20
|
+
GEM_ROOT = File.dirname(File.dirname(File.dirname(__FILE__))) # :nodoc:
|
|
19
21
|
end
|