syncwrap 2.0.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/History.rdoc +23 -0
- data/Manifest.txt +1 -0
- data/lib/syncwrap/amazon_ec2.rb +87 -29
- data/lib/syncwrap/amazon_ws.rb +62 -3
- data/lib/syncwrap/base.rb +1 -1
- data/lib/syncwrap/cli.rb +87 -26
- data/lib/syncwrap/component.rb +11 -0
- data/lib/syncwrap/components/cruby_vm.rb +2 -2
- data/lib/syncwrap/components/hashdot.rb +9 -4
- data/lib/syncwrap/components/iyyov.rb +20 -3
- data/lib/syncwrap/components/iyyov_daemon.rb +22 -2
- data/lib/syncwrap/components/ubuntu.rb +6 -11
- data/lib/syncwrap/components/users.rb +4 -4
- data/lib/syncwrap/context.rb +25 -1
- data/lib/syncwrap/git_help.rb +58 -0
- data/lib/syncwrap/host.rb +1 -1
- data/lib/syncwrap.rb +7 -5
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5a27d83ab1700ae7818a55ebace8c6c9bb29a99b
|
4
|
+
data.tar.gz: dfa29eebe123325201c9747dc6386e8f07072672
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b0fe60aaf32e21729609ed41dd05815e1ec09e2718ecaff59e159138dc2e2929f61ebc26f455b52632d64c06bf8f26b9299590a9e0ba257cc87fc86415797b5e
|
7
|
+
data.tar.gz: a5644f8fd7109d47b9e260236035a612db074383c2c36d6bca1374c30bcb641f054196d4cad9e8821ef7d52126e14b29aed4b79e2874e9f7231546de88f61e97
|
data/History.rdoc
CHANGED
@@ -1,3 +1,26 @@
|
|
1
|
+
=== 2.1.0 (2014-3-5)
|
2
|
+
* Simplify existing state handling (i.e. Ubuntu.first_apt?, Users
|
3
|
+
:just_created) by introducing SyncWrap::Component#state
|
4
|
+
* Add hashdot_updated state (i.e. jruby update) and use in Iyyov and
|
5
|
+
IyyovDaemon components to signal required restarts.
|
6
|
+
* Add :iyyov_root_jobs_installed state keeping to avoid redundant
|
7
|
+
rput's to Iyyov root jobs.rb
|
8
|
+
* Add CLI --create-image and AmazonEC2 support
|
9
|
+
* Add SyncWrap::GitHelp module for using a git hash as a host/image
|
10
|
+
tag from the profile
|
11
|
+
* Add :imaging state awareness to Iyyov and IyyovDaemon: stop (or
|
12
|
+
don't launch) these when imaging
|
13
|
+
* Add availability_zone to imported/saved Amazon host properties
|
14
|
+
* Fix failure on (EC2) import from Host.initialize order
|
15
|
+
* EC2 create_instance maps profile description and tag to tags
|
16
|
+
* Add --ssh-session (interactive shell) support to CLI
|
17
|
+
* Add --verbose-changes (verbose only for changing rputs) CLI option
|
18
|
+
* Drop CLI -I short option for infrequently used --import-hosts
|
19
|
+
* Disable EC2 operations, don't fail, if AWS credentials not found
|
20
|
+
* Improve Users pem-not-found warning
|
21
|
+
* Improve CLI --help summary
|
22
|
+
* Upgrade default CRubyVM to 2.0.0-p451
|
23
|
+
|
1
24
|
=== 2.0.0 (2014-2-26)
|
2
25
|
* Major rewrite with only very limited conceptual compatibility with
|
3
26
|
1.x. See README, LAYOUT, and examples.
|
data/Manifest.txt
CHANGED
data/lib/syncwrap/amazon_ec2.rb
CHANGED
@@ -15,8 +15,10 @@
|
|
15
15
|
#++
|
16
16
|
|
17
17
|
require 'time'
|
18
|
+
require 'securerandom'
|
18
19
|
|
19
20
|
require 'syncwrap/amazon_ws'
|
21
|
+
require 'syncwrap/path_util'
|
20
22
|
require 'syncwrap/host'
|
21
23
|
|
22
24
|
module SyncWrap
|
@@ -34,6 +36,7 @@ module SyncWrap
|
|
34
36
|
#
|
35
37
|
class AmazonEC2
|
36
38
|
include AmazonWS
|
39
|
+
include PathUtil
|
37
40
|
|
38
41
|
# FIXME: Interim strategy: use AmazonWS and defer deciding final
|
39
42
|
# organization.
|
@@ -58,9 +61,15 @@ module SyncWrap
|
|
58
61
|
# the sync file path.
|
59
62
|
if @aws_config
|
60
63
|
if sync_file_path
|
61
|
-
|
62
|
-
|
64
|
+
@aws_config = File.expand_path( @aws_config, sync_file_path )
|
65
|
+
end
|
66
|
+
|
67
|
+
if File.exist?( @aws_config )
|
63
68
|
aws_configure( @aws_config )
|
69
|
+
else
|
70
|
+
@aws_config = relativize( @aws_config )
|
71
|
+
warn "WARNING: #{aws_config} not found. EC2 provider operations not available."
|
72
|
+
@aws_config = nil
|
64
73
|
end
|
65
74
|
end
|
66
75
|
end
|
@@ -68,7 +77,7 @@ module SyncWrap
|
|
68
77
|
# Define a host profile by Symbol key and Hash value.
|
69
78
|
#
|
70
79
|
# Profiles may inherit properties from a :base_profile, either
|
71
|
-
# specified by that key, or the :default
|
80
|
+
# specified by that key, or the :default profile. The
|
72
81
|
# base_profile must be defined in advance (above in the sync
|
73
82
|
# file). When merging profile to any base_profile, the :roles
|
74
83
|
# property is concatenated via set union. All other properties are
|
@@ -90,7 +99,12 @@ module SyncWrap
|
|
90
99
|
@profiles[ key ] = profile
|
91
100
|
end
|
92
101
|
|
93
|
-
def
|
102
|
+
def get_profile( key )
|
103
|
+
@profiles[ key ] or raise "Profile #{key} not registered"
|
104
|
+
end
|
105
|
+
|
106
|
+
def import_hosts( regions, sync_file )
|
107
|
+
require_configured!
|
94
108
|
hlist = import_host_props( regions )
|
95
109
|
unless hlist.empty?
|
96
110
|
|
@@ -101,16 +115,17 @@ module SyncWrap
|
|
101
115
|
|
102
116
|
time = Time.now.utc
|
103
117
|
cmt = "\n# Import of AWS #{regions.join ','} on #{time.iso8601}"
|
104
|
-
append_host_definitions( hlist, cmt,
|
118
|
+
append_host_definitions( hlist, cmt, sync_file )
|
105
119
|
end
|
106
120
|
end
|
107
121
|
|
108
|
-
def create_hosts( count,
|
109
|
-
|
110
|
-
|
122
|
+
def create_hosts( count, profile, name, sync_file )
|
123
|
+
require_configured!
|
124
|
+
profile = get_profile( profile ) if profile.is_a?( Symbol )
|
125
|
+
profile = profile.dup
|
111
126
|
|
112
|
-
# FIXME: Support
|
113
|
-
#
|
127
|
+
# FIXME: Support profile overrides? Also add some targeted CLI
|
128
|
+
# overrides (like for :availability_zone)?
|
114
129
|
|
115
130
|
if profile[ :user_data ] == :ec2_user_sudo
|
116
131
|
profile[ :user_data ] = ec2_user_data
|
@@ -121,31 +136,68 @@ module SyncWrap
|
|
121
136
|
|
122
137
|
count.times do
|
123
138
|
hname = if count == 1
|
124
|
-
if space.
|
125
|
-
raise "Host #{name} already exists!"
|
126
|
-
end
|
139
|
+
raise "Host #{name} already exists!" if space.get_host( name )
|
127
140
|
name
|
128
141
|
else
|
129
142
|
find_name( name )
|
130
143
|
end
|
131
144
|
props = aws_create_instance( hname, profile )
|
132
145
|
host = space.host( props )
|
133
|
-
append_host_definitions( [ host ], nil,
|
134
|
-
host[ :just_created ] = true
|
146
|
+
append_host_definitions( [ host ], nil, sync_file )
|
147
|
+
host[ :just_created ] = true
|
148
|
+
# Need to use a host prop for this since context(s) do not
|
149
|
+
# exist yet. Note it is set after append_host_definitions, to
|
150
|
+
# avoid permanently writing this property to the sync_file.
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Create a temporary host using the specified profile, yield to
|
155
|
+
# block for provisioning, then create a machine image and
|
156
|
+
# terminate the host. If block returns false, then the image will
|
157
|
+
# not be created nor will the host be terminated.
|
158
|
+
# On success, returns image_id (ami-*) and name.
|
159
|
+
def create_image_from_profile( profile_key, sync_file )
|
160
|
+
require_configured!
|
161
|
+
profile = get_profile( profile_key ).dup
|
162
|
+
tag = profile[ :tag ]
|
163
|
+
profile[ :tag ] = tag = tag.call if tag.is_a?( Proc )
|
164
|
+
|
165
|
+
opts = {}
|
166
|
+
opts[ :name ] = profile_key.to_s
|
167
|
+
opts[ :name ] += ( '-' + tag ) if tag
|
168
|
+
opts[ :description ] = profile[ :description ]
|
169
|
+
|
170
|
+
if image_name_exist?( profile[ :region ], opts[ :name ] )
|
171
|
+
raise "Image name #{opts[:name]} (profile-tag) already exists."
|
135
172
|
end
|
173
|
+
|
174
|
+
hname = nil
|
175
|
+
loop do
|
176
|
+
hname = SecureRandom::hex(4)
|
177
|
+
break unless space.get_host( hname )
|
178
|
+
end
|
179
|
+
create_hosts( 1, profile, hname, sync_file )
|
180
|
+
host = space.host( hname, imaging: true )
|
181
|
+
|
182
|
+
success = yield( host )
|
183
|
+
|
184
|
+
if success
|
185
|
+
image_id = create_image( host, opts )
|
186
|
+
terminate_hosts( [ hname ], false, sync_file, false )
|
187
|
+
[ image_id, opts[ :name ] ]
|
188
|
+
end
|
189
|
+
|
136
190
|
end
|
137
191
|
|
138
|
-
def terminate_hosts( names, delete_attached_storage, sync_file )
|
192
|
+
def terminate_hosts( names, delete_attached_storage, sync_file, do_wait = true )
|
193
|
+
require_configured!
|
139
194
|
names.each do |name|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
else
|
147
|
-
raise "Host #{name} not found in Space, sync file."
|
148
|
-
end
|
195
|
+
host = space.get_host( name )
|
196
|
+
raise "Host #{name} not found in Space, sync file." unless host
|
197
|
+
raise "Host #{name} missing :id" unless host[:id]
|
198
|
+
raise "Host #{name} missing :region" unless host[:region]
|
199
|
+
aws_terminate_instance( host, delete_attached_storage, do_wait )
|
200
|
+
delete_host_definition( host, sync_file )
|
149
201
|
end
|
150
202
|
end
|
151
203
|
|
@@ -153,20 +205,26 @@ module SyncWrap
|
|
153
205
|
|
154
206
|
attr_reader :space
|
155
207
|
|
208
|
+
def require_configured!
|
209
|
+
unless @aws_config
|
210
|
+
raise( ":aws_config file not found, " +
|
211
|
+
"operation not supported without AWS credentials" )
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
156
215
|
def find_name( prefix )
|
157
|
-
host_names = space.host_names
|
158
216
|
i = 1
|
159
217
|
name = nil
|
160
218
|
loop do
|
161
219
|
name = "%s-%2d" % [ prefix, i ]
|
162
|
-
break if !
|
220
|
+
break if !space.get_host( name )
|
163
221
|
i += 1
|
164
222
|
end
|
165
223
|
name
|
166
224
|
end
|
167
225
|
|
168
|
-
def append_host_definitions( hosts, comment,
|
169
|
-
File.open(
|
226
|
+
def append_host_definitions( hosts, comment, sync_file )
|
227
|
+
File.open( sync_file, "a" ) do |out|
|
170
228
|
out.puts comment if comment
|
171
229
|
|
172
230
|
hosts.each do |host|
|
data/lib/syncwrap/amazon_ws.rb
CHANGED
@@ -103,7 +103,9 @@ module SyncWrap
|
|
103
103
|
iopts = opts.dup
|
104
104
|
iopts.delete( :ebs_volumes )
|
105
105
|
iopts.delete( :ebs_volume_options )
|
106
|
-
iopts.delete( :roles )
|
106
|
+
iopts.delete( :roles ) #-> tags
|
107
|
+
iopts.delete( :description ) #-> tags
|
108
|
+
iopts.delete( :tag ) #-> tags
|
107
109
|
|
108
110
|
if iopts[ :count ] && iopts[ :count ] != 1
|
109
111
|
raise ":count #{iopts[ :count ]} != 1 is not supported"
|
@@ -125,6 +127,16 @@ module SyncWrap
|
|
125
127
|
inst.add_tag( 'Roles', value: opts[ :roles ].join(' ') )
|
126
128
|
end
|
127
129
|
|
130
|
+
if opts[ :description ]
|
131
|
+
inst.add_tag( 'Description', value: opts[ :description ] )
|
132
|
+
end
|
133
|
+
|
134
|
+
tag = opts[ :tag ]
|
135
|
+
if tag
|
136
|
+
tag = tag.call if tag.is_a?( Proc )
|
137
|
+
inst.add_tag( 'Tag', value: tag )
|
138
|
+
end
|
139
|
+
|
128
140
|
wait_for_running( inst )
|
129
141
|
|
130
142
|
# FIXME: Split method
|
@@ -150,6 +162,50 @@ module SyncWrap
|
|
150
162
|
instance_to_props( region, inst )
|
151
163
|
end
|
152
164
|
|
165
|
+
# Return true if the authenticated AWS user in the sepecified
|
166
|
+
# region already owns an image of the specified name
|
167
|
+
def image_name_exist?( region, name )
|
168
|
+
ec2 = AWS::EC2.new.regions[ region ]
|
169
|
+
images = ec2.images.with_owner( :self )
|
170
|
+
images.any? { |img| img.name == name }
|
171
|
+
end
|
172
|
+
|
173
|
+
# Create an image for the specified host which will be stopped
|
174
|
+
# before imaging and not restarted
|
175
|
+
#
|
176
|
+
# === Options
|
177
|
+
#
|
178
|
+
# :name:: Required, image compatable (i.e. no spaces, identifier) name
|
179
|
+
#
|
180
|
+
# :description:: Image description
|
181
|
+
#
|
182
|
+
def create_image( host, opts = {} )
|
183
|
+
opts = opts.dup
|
184
|
+
name = opts.delete( :name ) or raise "Missing required name for image"
|
185
|
+
region = host[ :region ]
|
186
|
+
ec2 = AWS::EC2.new.regions[ region ]
|
187
|
+
inst = ec2.instances[ host[ :id ] ]
|
188
|
+
raise "Host ID #{host[:id]} does not exist?" unless inst.exists?
|
189
|
+
|
190
|
+
inst.stop
|
191
|
+
stat = wait_until( "instance #{inst.id} to stop", 2.0 ) do
|
192
|
+
s = inst.status
|
193
|
+
s if s == :stopped || s == :terminated
|
194
|
+
end
|
195
|
+
raise "Instance #{inst.id} has status #{stat}" unless stat == :stopped
|
196
|
+
|
197
|
+
image = inst.create_image( name, { no_reboot: true }.merge( opts ) )
|
198
|
+
stat = wait_until( "image #{image.id} to be available", 2.0 ) do
|
199
|
+
s = image.state
|
200
|
+
s if s != :pending
|
201
|
+
end
|
202
|
+
unless stat == :available
|
203
|
+
raise "Image #{image.id} failed: #{image.state_reason}"
|
204
|
+
end
|
205
|
+
|
206
|
+
image.image_id
|
207
|
+
end
|
208
|
+
|
153
209
|
# Create a Route53 DNS CNAME from iprops :name to :internet_name.
|
154
210
|
# Options are per {AWS::Route53::ResourceRecordSetCollection.create}[http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/Route53/ResourceRecordSetCollection.html#create-instance_method]
|
155
211
|
# (currently undocumented) with the following additions:
|
@@ -197,7 +253,7 @@ module SyncWrap
|
|
197
253
|
# :region and :id.
|
198
254
|
#
|
199
255
|
# _WARNING_: data _will_ be lost!
|
200
|
-
def aws_terminate_instance( iprops, delete_attached_storage = false )
|
256
|
+
def aws_terminate_instance( iprops, delete_attached_storage = false, do_wait = true )
|
201
257
|
ec2 = AWS::EC2.new.regions[ iprops[ :region ] ]
|
202
258
|
inst = ec2.instances[ iprops[ :id ] ]
|
203
259
|
unless inst.exists?
|
@@ -215,7 +271,9 @@ module SyncWrap
|
|
215
271
|
end
|
216
272
|
|
217
273
|
inst.terminate
|
218
|
-
|
274
|
+
if do_wait || !ebs_volumes.empty?
|
275
|
+
wait_until( "termination of #{inst.id}", 2.0 ) { inst.status == :terminated }
|
276
|
+
end
|
219
277
|
|
220
278
|
ebs_volumes = ebs_volumes.map do |vid|
|
221
279
|
volume = ec2.volumes[ vid ]
|
@@ -264,6 +322,7 @@ module SyncWrap
|
|
264
322
|
|
265
323
|
{ id: inst.id,
|
266
324
|
region: region,
|
325
|
+
availability_zone: inst.availability_zone,
|
267
326
|
ami: inst.image_id,
|
268
327
|
name: tags[ 'Name' ],
|
269
328
|
internet_name: inst.dns_name,
|
data/lib/syncwrap/base.rb
CHANGED
data/lib/syncwrap/cli.rb
CHANGED
@@ -33,9 +33,11 @@ module SyncWrap
|
|
33
33
|
@roles = []
|
34
34
|
@host_patterns = []
|
35
35
|
@create_plan = []
|
36
|
+
@image_plan = []
|
36
37
|
@import_regions = []
|
37
38
|
@terminate_hosts = []
|
38
39
|
@delete_attached_storage = false
|
40
|
+
@ssh_session = nil
|
39
41
|
@space = Space.new
|
40
42
|
end
|
41
43
|
|
@@ -43,43 +45,49 @@ module SyncWrap
|
|
43
45
|
|
44
46
|
def parse_cmd( args )
|
45
47
|
opts = OptionParser.new do |opts|
|
46
|
-
opts.banner =
|
48
|
+
opts.banner = <<-TEXT
|
49
|
+
Usage: syncwrap {options} [Component[.method]] ..."
|
50
|
+
General options:
|
51
|
+
TEXT
|
52
|
+
opts.summary_width = 30
|
53
|
+
opts.summary_indent = " "
|
47
54
|
|
48
55
|
opts.on( "-f", "--file FILE",
|
49
|
-
"Load FILE for role/host/component
|
50
|
-
"(default: './sync.rb')" ) do |f|
|
56
|
+
"Load FILE for role/host/component/profile",
|
57
|
+
"definitions. (default: './sync.rb')" ) do |f|
|
51
58
|
@sw_file = f
|
52
59
|
end
|
53
60
|
|
54
61
|
opts.on( "-h", "--hosts PATTERN",
|
55
|
-
"Constrain hosts by
|
62
|
+
"Constrain hosts by name PATTERN",
|
63
|
+
"(may use multiple)" ) do |p|
|
56
64
|
@host_patterns << Regexp.new( p )
|
57
65
|
end
|
58
66
|
|
59
67
|
opts.on( "-r", "--hosts-with-role ROLE",
|
60
|
-
"Constrain hosts by
|
68
|
+
"Constrain hosts by ROLE (may use multiple)" ) do |r|
|
61
69
|
@roles << r.sub(/^:/,'').to_sym
|
62
70
|
end
|
63
71
|
|
64
|
-
opts.on( "-n", "--dryrun",
|
65
|
-
"Run in \"dry run\", or no changes/test mode",
|
66
|
-
"(typically combined with -v)" ) do
|
67
|
-
@options[ :dryrun ] = true
|
68
|
-
end
|
69
|
-
|
70
72
|
opts.on( "-t", "--threads N",
|
71
|
-
"
|
73
|
+
"The number of hosts to process concurrently",
|
72
74
|
"(default: all hosts)",
|
73
75
|
Integer ) do |n|
|
74
76
|
@options[ :threads ] = n
|
75
77
|
end
|
76
78
|
|
79
|
+
opts.on( "-n", "--dryrun",
|
80
|
+
"Run in \"dry run\", or no changes/test mode",
|
81
|
+
"(typically combined with -v)" ) do
|
82
|
+
@options[ :dryrun ] = true
|
83
|
+
end
|
84
|
+
|
77
85
|
opts.on( "-e", "--each-component",
|
78
|
-
"Flush shell commands after each component
|
86
|
+
"Flush shell commands after each component" ) do
|
79
87
|
@options[ :flush_component ] = true
|
80
88
|
end
|
81
89
|
|
82
|
-
opts.on( "
|
90
|
+
opts.on( "--no-coalesce",
|
83
91
|
"Do not coalesce streams (as is the default)" ) do
|
84
92
|
@options[ :coalesce ] = false
|
85
93
|
end
|
@@ -90,12 +98,17 @@ module SyncWrap
|
|
90
98
|
end
|
91
99
|
|
92
100
|
opts.on( "-v", "--verbose",
|
93
|
-
"Show details of
|
101
|
+
"Show details of rput and remote commands" ) do
|
94
102
|
@options[ :verbose ] = true
|
95
103
|
end
|
96
104
|
|
105
|
+
opts.on( "-c", "--verbose-changes",
|
106
|
+
"Be verbose only about actual rput changes" ) do
|
107
|
+
@options[ :verbose_changes ] = true
|
108
|
+
end
|
109
|
+
|
97
110
|
opts.on( "-x", "--expand-shell",
|
98
|
-
"Use -x (expand) instead of -v shell
|
111
|
+
"Use -x (expand) instead of -v shell output",
|
99
112
|
"(sh_verbose: :x, typically combined with -v)" ) do
|
100
113
|
@options[ :sh_verbose ] = :x
|
101
114
|
end
|
@@ -127,12 +140,21 @@ module SyncWrap
|
|
127
140
|
@list_hosts = true
|
128
141
|
end
|
129
142
|
|
143
|
+
opts.on( "-S", "--ssh-session HOST",
|
144
|
+
"Only exec an ssh login session on HOST name",
|
145
|
+
"(ssh args can be passed after an '--')" ) do |h|
|
146
|
+
@ssh_session = h
|
147
|
+
end
|
148
|
+
|
149
|
+
opts.separator( "Provider specific operations and options:" )
|
150
|
+
|
130
151
|
opts.on( "-C", "--create-host P",
|
131
|
-
"Create hosts
|
152
|
+
"Create hosts. P has form: [N*]profile[:name]",
|
132
153
|
" N: number to create (default: 1)",
|
133
|
-
" profile:
|
134
|
-
" name: Host name, or prefix
|
135
|
-
"
|
154
|
+
" profile: profile name as in sync file",
|
155
|
+
" name: Host name, or prefix when N>1",
|
156
|
+
"Appends hosts to the sync file and space for",
|
157
|
+
"immediate provisioning." ) do |h|
|
136
158
|
first,rest = h.split('*')
|
137
159
|
if rest
|
138
160
|
count = first.to_i
|
@@ -145,28 +167,44 @@ module SyncWrap
|
|
145
167
|
@create_plan << [ count, profile, name ]
|
146
168
|
end
|
147
169
|
|
148
|
-
opts.on( "
|
170
|
+
opts.on( "--create-image PROFILE",
|
171
|
+
"Create a machine image using a temp. host",
|
172
|
+
"of PROFILE, provisioning only that host,",
|
173
|
+
"imaging, and terminating." ) do |profile|
|
174
|
+
@image_plan << profile.to_sym
|
175
|
+
end
|
176
|
+
|
177
|
+
opts.on( "--import-hosts REGIONS",
|
149
178
|
"Import hosts form provider 'region' names, ",
|
150
179
|
"append to sync file and exit." ) do |rs|
|
151
180
|
@import_regions = rs.split( /[\s,]+/ )
|
152
181
|
end
|
153
182
|
|
154
183
|
opts.on( "--terminate-host NAME",
|
155
|
-
"Terminate the specified instance and
|
156
|
-
"WARNING: potential
|
184
|
+
"Terminate the specified instance and remove ",
|
185
|
+
"from sync file. WARNING: potential data loss" ) do |name|
|
157
186
|
@terminate_hosts << name
|
158
187
|
end
|
159
188
|
|
160
189
|
opts.on( "--delete-attached-storage",
|
161
|
-
"When terminating
|
162
|
-
"volumes which
|
163
|
-
"WARNING: Data WILL be lost!" ) do
|
190
|
+
"When terminating, also delete attached",
|
191
|
+
"volumes which would not otherwise be",
|
192
|
+
"deleted. WARNING: Data WILL be lost!" ) do
|
164
193
|
@delete_attached_storage = true
|
165
194
|
end
|
166
195
|
|
196
|
+
opts.separator <<-TEXT
|
197
|
+
|
198
|
+
By default, runs #install on all Components of all hosts, including
|
199
|
+
any just created. This can be limited by specifying --host
|
200
|
+
PATTERN(s), --hosts-with-role ROLE(s), specific Component[.method](s)
|
201
|
+
or options above which exit early or have constraints noted.
|
202
|
+
TEXT
|
203
|
+
|
167
204
|
end
|
168
205
|
|
169
206
|
@component_plan = opts.parse!( args )
|
207
|
+
# Usually; but treat these as ssh args if --ssh-session
|
170
208
|
|
171
209
|
rescue OptionParser::ParseError => e
|
172
210
|
$stderr.puts e.message
|
@@ -194,10 +232,33 @@ module SyncWrap
|
|
194
232
|
exit 0
|
195
233
|
end
|
196
234
|
|
235
|
+
if !@image_plan.empty?
|
236
|
+
success = nil
|
237
|
+
p = space.provider
|
238
|
+
@image_plan.each do |profile_key|
|
239
|
+
ami,name = p.create_image_from_profile(profile_key, @sw_file) do |host|
|
240
|
+
space.execute( [ host ], [], @options )
|
241
|
+
end
|
242
|
+
exit( 1 ) unless ami
|
243
|
+
puts "Image #{ami} (#{name}) created for profile #{profile_key}"
|
244
|
+
end
|
245
|
+
exit 0
|
246
|
+
end
|
247
|
+
|
197
248
|
@create_plan.each do |count, profile, name|
|
198
249
|
space.provider.create_hosts( count, profile, name, @sw_file )
|
199
250
|
end
|
200
251
|
|
252
|
+
if @ssh_session
|
253
|
+
host = space.get_host( @ssh_session )
|
254
|
+
host = space.ssh_host_name( host )
|
255
|
+
raise "Host #{@ssh_session} not found in sync file" unless host
|
256
|
+
extra_args = @component_plan
|
257
|
+
raise "SSH args? #{extra_args.inspect}" if extra_args.first =~ /^[^\-]/
|
258
|
+
@component_plan = []
|
259
|
+
Kernel.exec( 'ssh', *extra_args, host )
|
260
|
+
end
|
261
|
+
|
201
262
|
resolve_hosts
|
202
263
|
lookup_component_plan
|
203
264
|
resolve_components
|
data/lib/syncwrap/component.rb
CHANGED
@@ -58,6 +58,14 @@ module SyncWrap
|
|
58
58
|
ctx.host
|
59
59
|
end
|
60
60
|
|
61
|
+
# A Hash-like interface of Symbol keys to arbitrary values, backed
|
62
|
+
# read-only by the host properties. Use this for stashing any
|
63
|
+
# execution time state on a per context/host basis; as doing so on
|
64
|
+
# the cross-context Component instances themselves is inadvisable.
|
65
|
+
def state
|
66
|
+
ctx.state
|
67
|
+
end
|
68
|
+
|
61
69
|
# Enqueue a bash shell command or script fragment to be run on the
|
62
70
|
# host of the current Context. Newlines in command are interpreted
|
63
71
|
# as per bash. For example, it is common to use a here-document
|
@@ -374,6 +382,9 @@ module SyncWrap
|
|
374
382
|
#
|
375
383
|
# :verbose:: Output stdout/stderr from rsync (default: false)
|
376
384
|
#
|
385
|
+
# :verbose_changes:: Promote to verbose output if there are any
|
386
|
+
# changes (default: false).
|
387
|
+
#
|
377
388
|
# :erb_process:: If false, treat '.erb' suffixed files as normal
|
378
389
|
# files (default: true)
|
379
390
|
#
|
@@ -44,7 +44,7 @@ module SyncWrap
|
|
44
44
|
|
45
45
|
# The ruby version to install, like it appears in source packages
|
46
46
|
# from ruby-lang.org.
|
47
|
-
# (Default: 2.0.0-
|
47
|
+
# (Default: 2.0.0-p451)
|
48
48
|
attr_accessor :ruby_version
|
49
49
|
|
50
50
|
# If true, attempt to uninstall any pre-existing distro packaged
|
@@ -53,7 +53,7 @@ module SyncWrap
|
|
53
53
|
attr_accessor :do_uninstall_distro_ruby
|
54
54
|
|
55
55
|
def initialize( opts = {} )
|
56
|
-
@ruby_version = "2.0.0-
|
56
|
+
@ruby_version = "2.0.0-p451"
|
57
57
|
@do_uninstall_distro_ruby = true
|
58
58
|
|
59
59
|
super
|
@@ -42,15 +42,20 @@ module SyncWrap
|
|
42
42
|
# Install hashdot if the binary version doesn't match, otherwise
|
43
43
|
# just update the profile config files.
|
44
44
|
def install
|
45
|
-
if !test_hashdot_binary
|
45
|
+
if !test_hashdot_binary
|
46
46
|
install_system_deps
|
47
47
|
install_hashdot
|
48
|
+
changes = [ :installed ]
|
48
49
|
else
|
49
50
|
# Just update config as needed.
|
50
|
-
rput( 'src/hashdot/profiles/',
|
51
|
-
|
52
|
-
|
51
|
+
changes = rput( 'src/hashdot/profiles/',
|
52
|
+
"#{local_root}/lib/hashdot/profiles/",
|
53
|
+
excludes: :dev, user: :root )
|
53
54
|
end
|
55
|
+
unless changes.empty?
|
56
|
+
state[ :hashdot_updated ] = changes
|
57
|
+
end
|
58
|
+
changes
|
54
59
|
end
|
55
60
|
|
56
61
|
def install_system_deps
|
@@ -41,13 +41,24 @@ module SyncWrap
|
|
41
41
|
def install
|
42
42
|
# Shorten if the desired iyyov version is already running
|
43
43
|
pid, ver = capture_running_version( 'iyyov' )
|
44
|
-
|
44
|
+
if ver != iyyov_version
|
45
45
|
install_run_dir #as root
|
46
46
|
install_iyyov_gem #as root
|
47
47
|
install_iyyov_init #as root
|
48
|
-
iyyov_restart
|
48
|
+
iyyov_restart unless state[ :imaging ] #as root
|
49
|
+
true
|
50
|
+
elsif state[ :hashdot_updated ] && !state[ :imaging ]
|
51
|
+
iyyov_restart
|
49
52
|
true
|
50
53
|
end
|
54
|
+
|
55
|
+
# FIXME: There is a potential race condition brewing here. If
|
56
|
+
# Iyyov is restarted, then job changes (i.e. jobs.d files) are
|
57
|
+
# immediately made before Iyyov is done reloading, then those
|
58
|
+
# changes may not be detected. Thus job upgrades may not occur.
|
59
|
+
# This might be best fixed in Iyyov itself.
|
60
|
+
|
61
|
+
iyyov_stop if state[ :imaging ]
|
51
62
|
false
|
52
63
|
end
|
53
64
|
|
@@ -84,12 +95,18 @@ module SyncWrap
|
|
84
95
|
# including any forced mtime update.
|
85
96
|
def iyyov_install_jobs( force = false )
|
86
97
|
|
87
|
-
changes =
|
98
|
+
changes = []
|
99
|
+
|
100
|
+
if force || !state[ :iyyov_root_jobs_installed ]
|
101
|
+
changes += rput( 'var/iyyov/jobs.rb', iyyov_run_dir, user: run_user )
|
102
|
+
state[ :iyyov_root_jobs_installed ] = true
|
103
|
+
end
|
88
104
|
|
89
105
|
if force && changes.empty?
|
90
106
|
rudo "touch #{iyyov_run_dir}/jobs.rb"
|
91
107
|
changes << [ '.f..T......', "#{iyyov_run_dir}/jobs.rb" ]
|
92
108
|
end
|
109
|
+
|
93
110
|
changes
|
94
111
|
end
|
95
112
|
|
@@ -80,9 +80,29 @@ module SyncWrap
|
|
80
80
|
"#{iyyov_run_dir}/jobs.d/#{name_instance}.rb",
|
81
81
|
user: run_user )
|
82
82
|
changes += iyyov_install_jobs
|
83
|
-
elsif !changes.empty?
|
84
|
-
rudo( "kill #{pid} || true" ) # ..and let Iyyov restart it
|
85
83
|
end
|
84
|
+
|
85
|
+
# If we found a daemon pid then kill if either:
|
86
|
+
#
|
87
|
+
# (1) the version is the same (i.e no other signal via updates
|
88
|
+
# above for Iyyov) but there was a config change or hashdot
|
89
|
+
# was updated (i.e. new jruby version). In this case Iyyov
|
90
|
+
# should be up to restart the daemon.
|
91
|
+
#
|
92
|
+
# (2) We are :imaging, in which case we want a graceful
|
93
|
+
# shutdown. In this case Iyyov should have already been kill
|
94
|
+
# signalled itself and will not restart the daemon.
|
95
|
+
#
|
96
|
+
# In all cases there is the potential that the process has
|
97
|
+
# already stopped (i.e. crashed, etc) between above pid capture
|
98
|
+
# and the kill. Ignore kill failures.
|
99
|
+
if pid &&
|
100
|
+
( ( ver == version && ( !changes.empty? ||
|
101
|
+
state[ :hashdot_updated ] ) ) ||
|
102
|
+
state[ :imaging ] )
|
103
|
+
rudo( "kill #{pid} || true" )
|
104
|
+
end
|
105
|
+
|
86
106
|
changes
|
87
107
|
end
|
88
108
|
|
@@ -16,7 +16,6 @@
|
|
16
16
|
|
17
17
|
require 'syncwrap/component'
|
18
18
|
require 'syncwrap/distro'
|
19
|
-
require 'thread'
|
20
19
|
|
21
20
|
module SyncWrap
|
22
21
|
|
@@ -26,9 +25,6 @@ module SyncWrap
|
|
26
25
|
include SyncWrap::Distro
|
27
26
|
|
28
27
|
def initialize( opts = {} )
|
29
|
-
@apt_update_state_lock = Mutex.new
|
30
|
-
@apt_update_state = {}
|
31
|
-
|
32
28
|
super
|
33
29
|
|
34
30
|
packages_map.merge!( 'apr' => 'libapr1',
|
@@ -70,13 +66,12 @@ module SyncWrap
|
|
70
66
|
protected
|
71
67
|
|
72
68
|
def first_apt?
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
end
|
69
|
+
s = state
|
70
|
+
if s[ :ubuntu_apt_updated ]
|
71
|
+
false
|
72
|
+
else
|
73
|
+
s[ :ubuntu_apt_updated ] = true
|
74
|
+
true
|
80
75
|
end
|
81
76
|
end
|
82
77
|
|
@@ -74,9 +74,9 @@ module SyncWrap
|
|
74
74
|
@ssh_user_pem =
|
75
75
|
relativize( path_relative_to_caller( @ssh_user_pem, caller ) )
|
76
76
|
unless File.exist?( @ssh_user_pem )
|
77
|
-
warn( "WARNING:
|
78
|
-
"
|
79
|
-
"
|
77
|
+
warn( "WARNING: #{@ssh_user_pem} not found, " +
|
78
|
+
"Users will not use #{@ssh_user}.\n" +
|
79
|
+
" Expect failures if user #{ENV['USER']} isn't already a sudoer." )
|
80
80
|
@ssh_user = nil
|
81
81
|
@ssh_user_pem = nil
|
82
82
|
end
|
@@ -84,7 +84,7 @@ module SyncWrap
|
|
84
84
|
end
|
85
85
|
|
86
86
|
def install
|
87
|
-
ensure_ssh_access if
|
87
|
+
ensure_ssh_access if state[ :just_created ] && ssh_access_timeout > 0
|
88
88
|
|
89
89
|
rdir = find_source( local_home_root )
|
90
90
|
users = home_users
|
data/lib/syncwrap/context.rb
CHANGED
@@ -45,10 +45,15 @@ module SyncWrap
|
|
45
45
|
# The current Host of this context
|
46
46
|
attr_reader :host
|
47
47
|
|
48
|
+
# A Hash-like interface of keys/values backed read-only by the
|
49
|
+
# host properties.
|
50
|
+
attr_reader :state
|
51
|
+
|
48
52
|
# Construct given host and default_options to use for all #sh and
|
49
53
|
# #rput calls.
|
50
54
|
def initialize( host, opts = {} )
|
51
55
|
@host = host
|
56
|
+
@state = StateHash.new( host )
|
52
57
|
reset_queue
|
53
58
|
@queue_locked = false
|
54
59
|
@default_options = opts
|
@@ -240,7 +245,9 @@ module SyncWrap
|
|
240
245
|
fmt.lock.unlock if stream_output
|
241
246
|
end
|
242
247
|
|
243
|
-
if !stream_output &&
|
248
|
+
if !stream_output &&
|
249
|
+
( failed || opts[ :verbose ] ||
|
250
|
+
( opts[ :verbose_changes ] && !outputs.empty? && mode == :rsync ) )
|
244
251
|
fmt.sync do
|
245
252
|
fmt.write_header( host, mode, opts )
|
246
253
|
if mode == :rsync
|
@@ -257,4 +264,21 @@ module SyncWrap
|
|
257
264
|
|
258
265
|
end
|
259
266
|
|
267
|
+
# The Context#state Hash-like implementation, backed read-only by
|
268
|
+
# the associated host properties.
|
269
|
+
class StateHash
|
270
|
+
def initialize( host )
|
271
|
+
@host = host
|
272
|
+
@props = {}
|
273
|
+
end
|
274
|
+
|
275
|
+
def []( key )
|
276
|
+
@props[ key ] || @host[ key ]
|
277
|
+
end
|
278
|
+
|
279
|
+
def []=( key, val )
|
280
|
+
@props[ key.to_sym ] = val
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
260
284
|
end
|
@@ -0,0 +1,58 @@
|
|
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 'syncwrap/path_util'
|
18
|
+
|
19
|
+
module SyncWrap
|
20
|
+
|
21
|
+
# Utility methods for sync file/sources kept in a git repository.
|
22
|
+
module GitHelp
|
23
|
+
extend PathUtil
|
24
|
+
|
25
|
+
# Raises RuntimeError if the git tree at path (default to caller's
|
26
|
+
# path) is not clean
|
27
|
+
def self.require_clean!( path = nil )
|
28
|
+
path ||= caller_path( caller )
|
29
|
+
delta = `cd #{path} && git status --porcelain -- . 2>&1`
|
30
|
+
if delta.split( /^/ ).length > 0
|
31
|
+
warn( "Commit or move these first:\n" + delta )
|
32
|
+
raise "Git repo at #{path} not clean"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Return the abbreviated SHA-1 hash for the last git commit at
|
37
|
+
# path
|
38
|
+
def self.hash( path = nil )
|
39
|
+
path ||= caller_path( caller )
|
40
|
+
`cd #{path} && git log -n 1 --format='format:%h'`
|
41
|
+
end
|
42
|
+
|
43
|
+
# Return a lambda that will #require_clean! for callers path
|
44
|
+
# before providing #hash. Use this for cases where you only want
|
45
|
+
# to use a git hash when it is an accurate reflection of the local
|
46
|
+
# file state. Since the test is deferred, it will only be required
|
47
|
+
# for actions (i.e. image creation, etc.) that actually use it.
|
48
|
+
def self.clean_hash
|
49
|
+
cpath = caller_path( caller )
|
50
|
+
lambda do
|
51
|
+
require_clean!( cpath )
|
52
|
+
hash( cpath )
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
data/lib/syncwrap/host.rb
CHANGED
data/lib/syncwrap.rb
CHANGED
@@ -81,7 +81,7 @@ module SyncWrap
|
|
81
81
|
|
82
82
|
# Load the specified file path as per a sync.rb, into this
|
83
83
|
# Space. If relative, path is assumed to be relative to the caller
|
84
|
-
# (i.e. Rakefile, etc.) as with the conventional 'sync'
|
84
|
+
# (i.e. Rakefile, etc.) as with the conventional 'sync.rb'.
|
85
85
|
def load_sync_file_relative( fpath = './sync.rb' )
|
86
86
|
load_sync_file( path_relative_to_caller( fpath, caller ) )
|
87
87
|
end
|
@@ -174,15 +174,16 @@ module SyncWrap
|
|
174
174
|
host
|
175
175
|
end
|
176
176
|
|
177
|
+
# Return host by name, or nil if not defined.
|
178
|
+
def get_host( name )
|
179
|
+
@hosts[ name ]
|
180
|
+
end
|
181
|
+
|
177
182
|
# All Host instances, in order added.
|
178
183
|
def hosts
|
179
184
|
@hosts.values
|
180
185
|
end
|
181
186
|
|
182
|
-
def host_names
|
183
|
-
@hosts.keys
|
184
|
-
end
|
185
|
-
|
186
187
|
# Return an ordered, unique set of component classes, direct or via
|
187
188
|
# roles, currently contained by the specified hosts or all hosts.
|
188
189
|
def component_classes( hs = hosts )
|
@@ -379,5 +380,6 @@ module SyncWrap
|
|
379
380
|
|
380
381
|
# Additional autoloads (optional support)
|
381
382
|
autoload :AmazonEC2, 'syncwrap/amazon_ec2'
|
383
|
+
autoload :GitHelp, 'syncwrap/git_help'
|
382
384
|
|
383
385
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: syncwrap
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Kellum
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-03-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: term-ansicolor
|
@@ -187,6 +187,7 @@ files:
|
|
187
187
|
- lib/syncwrap/context.rb
|
188
188
|
- lib/syncwrap/distro.rb
|
189
189
|
- lib/syncwrap/formatter.rb
|
190
|
+
- lib/syncwrap/git_help.rb
|
190
191
|
- lib/syncwrap/host.rb
|
191
192
|
- lib/syncwrap/main.rb
|
192
193
|
- lib/syncwrap/path_util.rb
|