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 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