syncwrap 2.0.0 → 2.1.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 +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
|