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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7750e1149348fc952ef8d2dd124282861961bc4f
4
- data.tar.gz: 1c183faa0793ee65741b64366e1463b3c4593e51
3
+ metadata.gz: 5a27d83ab1700ae7818a55ebace8c6c9bb29a99b
4
+ data.tar.gz: dfa29eebe123325201c9747dc6386e8f07072672
5
5
  SHA512:
6
- metadata.gz: db6263d45a815839d43aed07c08a22cfc5dc578cbcb9c3438e3ab74ba619ba78dd3d8780beaf4648d27336284192cd4112b9e2ce6edd7d3029b5089d6db441fa
7
- data.tar.gz: 694c6a0e63d9358f6831f21f16bfe9f2caffcf51c27080689e85545e24a9ce1f4b0c0ec3a44fcaa5610586e92545e1b7ad11c52e4fb696405fdde281fc0c6b1f
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
@@ -22,6 +22,7 @@ lib/syncwrap/component.rb
22
22
  lib/syncwrap/context.rb
23
23
  lib/syncwrap/distro.rb
24
24
  lib/syncwrap/formatter.rb
25
+ lib/syncwrap/git_help.rb
25
26
  lib/syncwrap/host.rb
26
27
  lib/syncwrap/main.rb
27
28
  lib/syncwrap/path_util.rb
@@ -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
- aws_configure( File.expand_path( @aws_config, sync_file_path ) )
62
- else
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 key profile. The
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 import_hosts( regions, output_file )
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, output_file )
118
+ append_host_definitions( hlist, cmt, sync_file )
105
119
  end
106
120
  end
107
121
 
108
- def create_hosts( count, profile_key, name, output_file )
109
- profile = @profiles[ profile_key ].dup or
110
- raise "Profile #{profile_key} not registered"
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 profiles overrides.
113
- # Also add some targeted CLI overrides (like for :availability_zones)
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.host_names.include?( name )
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, output_file )
134
- host[ :just_created ] = true #after so this isn't written
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
- 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
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 ! host_names.include?( name )
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, output_file )
169
- File.open( output_file, "a" ) do |out|
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|
@@ -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
- wait_until( "termination of #{inst.id}", 2.0 ) { inst.status == :terminated }
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
@@ -15,7 +15,7 @@
15
15
  #++
16
16
 
17
17
  module SyncWrap
18
- VERSION='2.0.0'
18
+ VERSION='2.1.0'
19
19
 
20
20
  GEM_ROOT = File.dirname(File.dirname(File.dirname(__FILE__))) # :nodoc:
21
21
  end
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 = "Usage: syncwrap {options} [Component[.method]] ..."
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 definitions",
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 pattern (may use multiple)" ) do |p|
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 role (may use multiple)" ) do |r|
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
- "Specify the number of hosts to process concurrently",
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/method" ) do
86
+ "Flush shell commands after each component" ) do
79
87
  @options[ :flush_component ] = true
80
88
  end
81
89
 
82
- opts.on( "-s", "--no-coalesce",
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 local/remote command execution" ) do
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 verbose output",
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 where P has format: [N*]profile[:name]",
152
+ "Create hosts. P has form: [N*]profile[:name]",
132
153
  " N: number to create (default: 1)",
133
- " profile: the profile name as setup in the sync file",
134
- " name: Host name, or prefix in the case on N>1",
135
- "Hosts are appended to the sync file and space" ) do |h|
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( "-I", "--import-hosts REGIONS",
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 data via provider",
156
- "WARNING: potential for data loss!" ) do |name|
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 hosts, also delete any attached storage",
162
- "volumes which wouldn't otherwise be deleted.",
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
@@ -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-p353)
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-p353"
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 || dryrun?
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
- "#{local_root}/lib/hashdot/profiles/",
52
- excludes: :dev, user: :root )
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
- unless ver == iyyov_version
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 #as root
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 = rput( 'var/iyyov/jobs.rb', iyyov_run_dir, user: run_user )
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
- @apt_update_state_lock.synchronize do
74
- if @apt_update_state[ host ]
75
- false
76
- else
77
- @apt_update_state[ host ] = true
78
- true
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: Users pem #{@ssh_user_pem} not found. " +
78
- "Will not use #{@ssh_user}.\n" +
79
- "Components may fail without sudo access" )
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 host[ :just_created ] && ssh_access_timeout > 0
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
@@ -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 && ( failed || opts[ :verbose ] )
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
@@ -32,8 +32,8 @@ module SyncWrap
32
32
  def initialize( space, props = {} )
33
33
  @space = space
34
34
  @props = {}
35
- merge_props( props )
36
35
  @contents = [ :all ]
36
+ merge_props( props )
37
37
  end
38
38
 
39
39
  # Return the :name property.
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' directory.
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.0.0
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-02-26 00:00:00.000000000 Z
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