etch 4.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/Rakefile +7 -8
  3. data/lib/etch.rb +959 -399
  4. data/lib/etch/client.rb +265 -335
  5. metadata +12 -16
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d0dc3526272784c37e6a08e8fcc51b2e58b75a4f
4
+ data.tar.gz: 2c781954de4ce096bbd90ed8dcb5e7925104a764
5
+ SHA512:
6
+ metadata.gz: e6f0b89da1febca23dcfcc60831bebbb0166f1a39a37882c6c837d8fbbf6f6962cc20845ff5daa9fdd282948f83326beb25acaeaf23d2a42762495721577f37d
7
+ data.tar.gz: 2ba80b2378ae28d8da759dc6ba0597d4f0f016983a33e573a5a5d48d344ebbd80ced891616407db889b0fa015ee9f3a52a9ab7747c4f75c115145944e69d2c67
data/Rakefile CHANGED
@@ -1,17 +1,16 @@
1
- require 'rake/gempackagetask'
1
+ require 'rubygems/package_task'
2
2
  spec = Gem::Specification.new do |s|
3
- s.name = 'etch'
4
- s.summary = 'Etch system configuration management client'
3
+ s.name = 'etch'
4
+ s.summary = 'Etch system configuration management client'
5
5
  s.add_dependency('facter')
6
- s.version = '4.0.0'
6
+ s.version = '5.0.0'
7
7
  s.author = 'Jason Heiss'
8
8
  s.email = 'etch-users@lists.sourceforge.net'
9
- s.homepage = 'http://etch.sourceforge.net'
9
+ s.homepage = 'http://etch.github.io'
10
10
  s.rubyforge_project = 'etchsyscm'
11
11
  s.platform = Gem::Platform::RUBY
12
12
  s.required_ruby_version = '>=1.8'
13
- s.files = Dir['**/**']
13
+ s.files = Dir['**/**']
14
14
  s.executables = [ 'etch', 'etch_to_trunk', 'etch_cron_wrapper' ]
15
15
  end
16
- Rake::GemPackageTask.new(spec).define
17
-
16
+ Gem::PackageTask.new(spec).define
@@ -10,6 +10,8 @@ Silently.silently do
10
10
  require 'fileutils' # mkdir_p
11
11
  require 'erb'
12
12
  require 'logger'
13
+ require 'yaml'
14
+ require 'set'
13
15
  end
14
16
  require 'versiontype' # Version
15
17
 
@@ -97,77 +99,66 @@ class Etch
97
99
  @sitelibbase = "#{@configdir}/sitelibs"
98
100
  @config_dtd_file = "#{@configdir}/config.dtd"
99
101
  @commands_dtd_file = "#{@configdir}/commands.dtd"
100
- @defaults_file = "#{@configdir}/defaults.xml"
101
- @nodes_file = "#{@configdir}/nodes.xml"
102
- @nodegroups_file = "#{@configdir}/nodegroups.xml"
103
102
 
104
103
  #
105
- # Load the DTD which is used to validate config.xml files
104
+ # These will be loaded on demand so that all-YAML configurations don't require them
106
105
  #
107
106
 
108
- @config_dtd = Etch.xmlloaddtd(@config_dtd_file)
109
- @commands_dtd = Etch.xmlloaddtd(@commands_dtd_file)
107
+ @config_dtd = nil
108
+ @commands_dtd = nil
110
109
 
111
110
  #
112
- # Load the defaults.xml file which sets defaults for parameters that the
113
- # users don't specify in their config.xml files.
111
+ # Load the defaults file which sets defaults for parameters that the
112
+ # users don't specify in their config files.
114
113
  #
115
114
 
116
- @defaults_xml = Etch.xmlload(@defaults_file)
115
+ @defaults = load_defaults
117
116
 
118
117
  #
119
118
  # Load the nodes file
120
119
  #
121
120
 
122
- @nodes_xml = Etch.xmlload(@nodes_file)
121
+ groups = Set.new
122
+ @nodes, nodesfile = load_nodes
123
123
  # Extract the groups for this node
124
- thisnodeelem = Etch.xmlfindfirst(@nodes_xml, "/nodes/node[@name='#{@fqdn}']")
125
- groupshash = {}
126
- if thisnodeelem
127
- Etch.xmleach(thisnodeelem, 'group') { |group| groupshash[Etch.xmltext(group)] = true }
124
+ if @nodes[@fqdn]
125
+ @nodes[@fqdn].each{|group| groups << group}
128
126
  else
129
- @logger.warn "No entry found for node #{@fqdn} in nodes.xml"
127
+ @logger.warn "No entry found for node #{@fqdn} in #{nodesfile}"
130
128
  # Some folks might want to terminate here
131
- #raise "No entry found for node #{@fqdn} in nodes.xml"
129
+ #raise "No entry found for node #{@fqdn} in #{nodesfile}"
132
130
  end
133
- @dlogger.debug "Native groups for node #{@fqdn}: #{groupshash.keys.sort.join(',')}"
131
+ @dlogger.debug "Native groups for node #{@fqdn}: #{groups.sort.join(',')}"
134
132
 
135
133
  #
136
134
  # Load the node groups file
137
135
  #
138
136
 
139
- @nodegroups_xml = Etch.xmlload(@nodegroups_file)
140
-
141
- # Extract the node group hierarchy into a hash for easy reference
142
- @group_hierarchy = {}
143
- Etch.xmleach(@nodegroups_xml, '/nodegroups/nodegroup') do |parent|
144
- Etch.xmleach(parent, 'child') do |child|
145
- @group_hierarchy[Etch.xmltext(child)] = [] if !@group_hierarchy[Etch.xmltext(child)]
146
- @group_hierarchy[Etch.xmltext(child)] << Etch.xmlattrvalue(parent, 'name')
147
- end
148
- end
137
+ @group_hierarchy = load_nodegroups
149
138
 
150
139
  # Fill out the list of groups for this node with any parent groups
151
- parentshash = {}
152
- groupshash.keys.each do |group|
153
- parents = get_parent_nodegroups(group)
154
- parents.each { |parent| parentshash[parent] = true }
140
+ parents = Set.new
141
+ groups.each do |group|
142
+ parents.merge get_parent_nodegroups(group)
155
143
  end
156
- parentshash.keys.each { |parent| groupshash[parent] = true }
157
- @dlogger.debug "Added groups for node #{@fqdn} due to node group hierarchy: #{parentshash.keys.sort.join(',')}"
144
+ parents.each{|parent| groups << parent}
145
+ @dlogger.debug "Added groups for node #{@fqdn} due to node group hierarchy: #{parents.sort.join(',')}"
158
146
 
147
+ #
159
148
  # Run the external node grouper
160
- externalhash = {}
149
+ #
150
+
151
+ externals = Set.new
161
152
  IO.popen(File.join(@configdir, 'nodegrouper') + ' ' + @fqdn) do |pipe|
162
- pipe.each { |group| externalhash[group.chomp] = true }
153
+ pipe.each{|group| externals << group.chomp}
163
154
  end
164
155
  if !$?.success?
165
- raise "External node grouper exited with error #{$?.exitstatus}"
156
+ raise "External node grouper #{File.join(@configdir, 'nodegrouper')} exited with error #{$?.exitstatus}"
166
157
  end
167
- externalhash.keys.each { |external| groupshash[external] = true }
168
- @dlogger.debug "Added groups for node #{@fqdn} due to external node grouper: #{externalhash.keys.sort.join(',')}"
158
+ groups.merge externals
159
+ @dlogger.debug "Added groups for node #{@fqdn} due to external node grouper: #{externals.sort.join(',')}"
169
160
 
170
- @groups = groupshash.keys.sort
161
+ @groups = groups.sort
171
162
  @dlogger.debug "Total groups for node #{@fqdn}: #{@groups.join(',')}"
172
163
 
173
164
  #
@@ -180,9 +171,11 @@ class Etch
180
171
  @dlogger.debug "Building complete file list for request from #{@fqdn}"
181
172
  if File.exist?(@sourcebase)
182
173
  Find.find(@sourcebase) do |path|
183
- if File.directory?(path) && File.exist?(File.join(path, 'config.xml'))
174
+ if File.directory?(path) &&
175
+ (File.exist?(File.join(path, 'config.xml')) ||
176
+ File.exist?(File.join(path, 'config.yml')))
184
177
  # Strip @sourcebase from start of path
185
- filelist << path.sub(Regexp.new('^' + Regexp.escape(@sourcebase)), '')
178
+ filelist << path.sub(Regexp.new('\A' + Regexp.escape(@sourcebase)), '')
186
179
  end
187
180
  end
188
181
  end
@@ -200,8 +193,8 @@ class Etch
200
193
  @already_generated = {}
201
194
  @generation_status = {}
202
195
  @configs = {}
203
- @need_orig = {}
204
- @allcommands = {}
196
+ @need_orig = []
197
+ @commands = {}
205
198
  @retrycommands = {}
206
199
 
207
200
  filelist.each do |file|
@@ -218,7 +211,9 @@ class Etch
218
211
  @dlogger.debug "Building complete configuration commands for request from #{@fqdn}"
219
212
  if File.exist?(@commandsbase)
220
213
  Find.find(@commandsbase) do |path|
221
- if File.directory?(path) && File.exist?(File.join(path, 'commands.xml'))
214
+ if File.directory?(path) &&
215
+ (File.exist?(File.join(path, 'commands.yml')) ||
216
+ File.exist?(File.join(path, 'commands.xml')))
222
217
  commandnames << File.basename(path)
223
218
  end
224
219
  end
@@ -239,7 +234,7 @@ class Etch
239
234
 
240
235
  {:configs => @configs,
241
236
  :need_orig => @need_orig,
242
- :allcommands => @allcommands,
237
+ :commands => @commands,
243
238
  :retrycommands => @retrycommands}
244
239
  end
245
240
 
@@ -248,17 +243,104 @@ class Etch
248
243
  #
249
244
  private
250
245
 
246
+ def load_defaults
247
+ yamldefaults = "#{@configdir}/defaults.yml"
248
+ xmldefaults = "#{@configdir}/defaults.xml"
249
+ if File.exist?(yamldefaults)
250
+ @dlogger.debug "Loading defaults from #{yamldefaults}"
251
+ defaults = symbolize_etch_keys(YAML.load(File.read(yamldefaults)))
252
+ elsif File.exist?(xmldefaults)
253
+ @dlogger.debug "Loading defaults from #{xmldefaults}"
254
+ defaults = {}
255
+ defaults_xml = Etch.xmlload(xmldefaults)
256
+ Etch.xmleach(defaults_xml, '/config/*') do |node|
257
+ section = node.name.to_sym
258
+ defaults[section] ||= {}
259
+ Etch.xmleachall(node) do |entry|
260
+ value = Etch.xmltext(entry)
261
+ # Convert things that look like numbers to match how YAML is parsed
262
+ if value.to_i.to_s == value
263
+ value = value.to_i
264
+ end
265
+ defaults[section][entry.name.to_sym] = value
266
+ end
267
+ end
268
+ else
269
+ raise "Neither defaults.yml nor defaults.xml exists"
270
+ end
271
+ # Ensure the top level sections exist
272
+ [:file, :link, :directory].each{|top| defaults[top] ||= {}}
273
+ defaults
274
+ end
275
+ def symbolize_etch_key(key)
276
+ key =~ /\Awhere (.*)/ ? key : key.to_sym
277
+ end
278
+ def symbolize_etch_keys(hash)
279
+ case hash
280
+ when Hash
281
+ Hash[hash.collect{|k,v| [symbolize_etch_key(k), symbolize_etch_keys(v)]}]
282
+ when Array
283
+ hash.collect{|e| symbolize_etch_keys(e)}
284
+ else
285
+ hash
286
+ end
287
+ end
288
+ def load_nodes
289
+ yamlnodes = "#{@configdir}/nodes.yml"
290
+ xmlnodes = "#{@configdir}/nodes.xml"
291
+ if File.exist?(yamlnodes)
292
+ @dlogger.debug "Loading native groups from #{yamlnodes}"
293
+ nodesfile = 'nodes.yml'
294
+ nodes = YAML.load(File.read(yamlnodes))
295
+ nodes ||= {}
296
+ elsif File.exist?(xmlnodes)
297
+ @dlogger.debug "Loading native groups from #{xmlnodes}"
298
+ nodesfile = 'nodes.xml'
299
+ nodes_xml = Etch.xmlload(xmlnodes)
300
+ nodes = {}
301
+ Etch.xmleach(nodes_xml, '/nodes/node') do |node|
302
+ name = Etch.xmlattrvalue(node, 'name')
303
+ nodes[name] ||= []
304
+ Etch.xmleach(node, 'group') do |group|
305
+ nodes[name] << Etch.xmltext(group)
306
+ end
307
+ end
308
+ end
309
+ nodes ||= {}
310
+ nodesfile ||= '<none>'
311
+ [nodes, nodesfile]
312
+ end
313
+ def load_nodegroups
314
+ yamlnodegroups = "#{@configdir}/nodegroups.yml"
315
+ xmlnodegroups = "#{@configdir}/nodegroups.xml"
316
+ if File.exist?(yamlnodegroups)
317
+ @dlogger.debug "Loading node group hierarchy from #{yamlnodegroups}"
318
+ group_hierarchy = YAML.load(File.read(yamlnodegroups))
319
+ elsif File.exist?(xmlnodegroups)
320
+ @dlogger.debug "Loading node group hierarchy from #{xmlnodegroups}"
321
+ group_hierarchy = {}
322
+ nodegroups_xml = Etch.xmlload(xmlnodegroups)
323
+ Etch.xmleach(nodegroups_xml, '/nodegroups/nodegroup') do |parent|
324
+ parentname = Etch.xmlattrvalue(parent, 'name')
325
+ group_hierarchy[parentname] ||= []
326
+ Etch.xmleach(parent, 'child') do |child|
327
+ group_hierarchy[parentname] << Etch.xmltext(child)
328
+ end
329
+ end
330
+ end
331
+ group_hierarchy || {}
332
+ end
333
+
251
334
  # Recursive method to get all of the parents of a node group
252
335
  def get_parent_nodegroups(group)
253
- parentshash = {}
254
- if @group_hierarchy[group]
255
- @group_hierarchy[group].each do |parent|
256
- parentshash[parent] = true
257
- grandparents = get_parent_nodegroups(parent)
258
- grandparents.each { |gp| parentshash[gp] = true }
336
+ parents = Set.new
337
+ @group_hierarchy.each do |parent, children|
338
+ if children.include?(group)
339
+ parents << parent
340
+ parents.merge get_parent_nodegroups(parent)
259
341
  end
260
342
  end
261
- parentshash.keys.sort
343
+ parents
262
344
  end
263
345
 
264
346
  # Returns the value of the generation_status variable, see comments in
@@ -279,31 +361,7 @@ class Etch
279
361
  end
280
362
  @filestack[file] = true
281
363
 
282
- config_xml_file = File.join(@sourcebase, file, 'config.xml')
283
- if !File.exist?(config_xml_file)
284
- raise "config.xml for #{file} does not exist"
285
- end
286
-
287
- # Load the config.xml file
288
- begin
289
- config_xml = Etch.xmlload(config_xml_file)
290
- rescue Exception => e
291
- raise Etch.wrap_exception(e, "Error loading config.xml for #{file}:\n" + e.message)
292
- end
293
-
294
- # Filter the config.xml file by looking for attributes
295
- begin
296
- configfilter!(Etch.xmlroot(config_xml))
297
- rescue Exception => e
298
- raise Etch.wrap_exception(e, "Error filtering config.xml for #{file}:\n" + e.message)
299
- end
300
-
301
- # Validate the filtered file against config.dtd
302
- begin
303
- Etch.xmlvalidate(config_xml, @config_dtd)
304
- rescue Exception => e
305
- raise Etch.wrap_exception(e, "Filtered config.xml for #{file} fails validation:\n" + e.message)
306
- end
364
+ config = load_config(file)
307
365
 
308
366
  generation_status = :unknown
309
367
  # As we go through the process of generating the file we'll end up with
@@ -313,7 +371,7 @@ class Etch
313
371
  # generally the original file
314
372
  # success: we successfully processed a valid configuration
315
373
  # unknown: no valid configuration nor errors encountered, probably because
316
- # filtering removed everything from the config.xml file. This
374
+ # filtering removed everything from the config file. This
317
375
  # should be considered a successful outcome, it indicates the
318
376
  # caller/client provided us with all required data and our result
319
377
  # is that no action needs to be taken.
@@ -322,18 +380,16 @@ class Etch
322
380
  # If we encounter either failure or success we set it to false or :success.
323
381
  catch :generate_done do
324
382
  # Generate any other files that this file depends on
325
- depends = []
326
383
  proceed = true
327
- Etch.xmleach(config_xml, '/config/depend') do |depend|
328
- @dlogger.debug "Generating dependency #{Etch.xmltext(depend)}"
329
- depends << Etch.xmltext(depend)
330
- r = generate_file(Etch.xmltext(depend), request)
384
+ config[:depend] && config[:depend].each do |depend|
385
+ @dlogger.debug "Generating dependency #{depend}"
386
+ r = generate_file(depend, request)
331
387
  proceed = proceed && r
332
388
  end
333
389
  # Also generate any commands that this file depends on
334
- Etch.xmleach(config_xml, '/config/dependcommand') do |dependcommand|
335
- @dlogger.debug "Generating command dependency #{Etch.xmltext(dependcommand)}"
336
- r = generate_commands(Etch.xmltext(dependcommand), request)
390
+ config[:dependcommand] && config[:dependcommand].each do |dependcommand|
391
+ @dlogger.debug "Generating command dependency #{dependcommand}"
392
+ r = generate_commands(dependcommand, request)
337
393
  proceed = proceed && r
338
394
  end
339
395
 
@@ -364,13 +420,13 @@ class Etch
364
420
  # bfile and afile. This repeats forever as the server isn't smart enough
365
421
  # to ask for everything it needs and the client isn't smart enough to send
366
422
  # everything.
367
- depends.each { |depend| @need_orig[depend] = true }
423
+ config[:depend] && config[:depend].each {|depend| @need_orig << depend}
368
424
 
369
425
  # Tell the client to request this file again
370
- @need_orig[file] = true
426
+ @need_orig << file
371
427
 
372
428
  # Strip this file's config down to the bare necessities
373
- filter_xml_completely!(config_xml, ['depend', 'setup'])
429
+ filter_config_completely!(config, [:depend, :setup])
374
430
 
375
431
  # And hit the eject button
376
432
  generation_status = false
@@ -385,23 +441,23 @@ class Etch
385
441
 
386
442
  # Check to see if the user has requested that we revert back to the
387
443
  # original file.
388
- if Etch.xmlfindfirst(config_xml, '/config/revert')
444
+ if config[:revert]
389
445
  # Pass the revert action back to the client
390
- filter_xml!(config_xml, ['revert'])
446
+ filter_config!(config, [:revert])
391
447
  generation_status = :success
392
448
  throw :generate_done
393
449
  end
394
450
 
395
451
  # Perform any server setup commands
396
- if Etch.xmlfindfirst(config_xml, '/config/server_setup')
452
+ if config[:server_setup]
397
453
  @dlogger.debug "Processing server setup commands"
398
- Etch.xmleach(config_xml, '/config/server_setup/exec') do |cmd|
399
- @dlogger.debug " Executing #{Etch.xmltext(cmd)}"
454
+ config[:server_setup].each do |cmd|
455
+ @dlogger.debug " Executing #{cmd}"
400
456
  # Explicitly invoke using /bin/sh so that syntax like
401
457
  # "FOO=bar myprogram" works.
402
- success = system('/bin/sh', '-c', Etch.xmltext(cmd))
458
+ success = system('/bin/sh', '-c', cmd)
403
459
  if !success
404
- raise "Server setup command #{Etch.xmltext(cmd)} for file #{file} exited with non-zero value"
460
+ raise "Server setup command #{cmd} for file #{file} exited with non-zero value"
405
461
  end
406
462
  end
407
463
  end
@@ -416,42 +472,47 @@ class Etch
416
472
  # Regular file
417
473
  #
418
474
 
419
- if Etch.xmlfindfirst(config_xml, '/config/file')
475
+ if config[:file]
420
476
  #
421
477
  # Assemble the contents for the file
422
478
  #
423
479
  newcontents = ''
424
-
425
- if Etch.xmlfindfirst(config_xml, '/config/file/source/plain')
426
- plain_elements = Etch.xmlarray(config_xml, '/config/file/source/plain')
427
- if check_for_inconsistency(plain_elements)
428
- raise "Inconsistent 'plain' entries for #{file}"
480
+ if config[:file][:plain] && !config[:file][:plain].empty?
481
+ if config[:file][:plain].kind_of?(Array)
482
+ if check_for_inconsistency(config[:file][:plain])
483
+ raise "Inconsistent 'plain' entries for #{file}"
484
+ end
485
+ plain = config[:file][:plain].first
486
+ else
487
+ plain = config[:file][:plain]
429
488
  end
430
-
431
489
  # Just slurp the file in
432
- plain_file = Etch.xmltext(plain_elements.first)
433
- newcontents = IO.read(plain_file)
434
- elsif Etch.xmlfindfirst(config_xml, '/config/file/source/template')
435
- template_elements = Etch.xmlarray(config_xml, '/config/file/source/template')
436
- if check_for_inconsistency(template_elements)
437
- raise "Inconsistent 'template' entries for #{file}"
490
+ newcontents = IO.read(plain)
491
+ elsif config[:file][:template] && !config[:file][:template].empty?
492
+ if config[:file][:template].kind_of?(Array)
493
+ if check_for_inconsistency(config[:file][:template])
494
+ raise "Inconsistent 'template' entries for #{file}"
495
+ end
496
+ template = config[:file][:template].first
497
+ else
498
+ template = config[:file][:template]
438
499
  end
439
-
440
500
  # Run the template through ERB to generate the file contents
441
- template = Etch.xmltext(template_elements.first)
442
501
  external = EtchExternalSource.new(file, original_file, @facts, @groups, local_requests, @sourcebase, @commandsbase, @sitelibbase, @dlogger)
443
502
  newcontents = external.process_template(template)
444
- elsif Etch.xmlfindfirst(config_xml, '/config/file/source/script')
445
- script_elements = Etch.xmlarray(config_xml, '/config/file/source/script')
446
- if check_for_inconsistency(script_elements)
447
- raise "Inconsistent 'script' entries for #{file}"
503
+ elsif config[:file][:script] && !config[:file][:script].empty?
504
+ if config[:file][:script].kind_of?(Array)
505
+ if check_for_inconsistency(config[:file][:script])
506
+ raise "Inconsistent 'script' entries for #{file}"
507
+ end
508
+ script = config[:file][:script].first
509
+ else
510
+ script = config[:file][:script]
448
511
  end
449
-
450
512
  # Run the script to generate the file contents
451
- script = Etch.xmltext(script_elements.first)
452
513
  external = EtchExternalSource.new(file, original_file, @facts, @groups, local_requests, @sourcebase, @commandsbase, @sitelibbase, @dlogger)
453
514
  newcontents = external.run_script(script)
454
- elsif Etch.xmlfindfirst(config_xml, '/config/file/always_manage_metadata')
515
+ elsif config[:file][:always_manage_metadata]
455
516
  # always_manage_metadata is a special case where we proceed
456
517
  # even if we don't have any source for file contents.
457
518
  else
@@ -470,8 +531,8 @@ class Etch
470
531
  # keep empty files or always manage the metadata, then assume
471
532
  # this file is not applicable to this node and do nothing.
472
533
  if newcontents == '' &&
473
- ! Etch.xmlfindfirst(config_xml, '/config/file/allow_empty') &&
474
- ! Etch.xmlfindfirst(config_xml, '/config/file/always_manage_metadata')
534
+ ! config[:file][:allow_empty] &&
535
+ ! config[:file][:always_manage_metadata]
475
536
  @dlogger.debug "New contents for file #{file} empty, doing nothing"
476
537
  else
477
538
  # Finish assembling the file contents as long as we're not
@@ -479,45 +540,33 @@ class Etch
479
540
  # proceeding based only on always_manage_metadata we want to make
480
541
  # sure that the only action we'll take is to manage metadata, not
481
542
  # muck with the file's contents.
482
- if !(newcontents == '' &&
483
- Etch.xmlfindfirst(config_xml, '/config/file/always_manage_metadata'))
543
+ if !(newcontents == '' && config[:file][:always_manage_metadata])
484
544
  # Add the warning message (if defined)
485
545
  warning_file = nil
486
- if Etch.xmlfindfirst(config_xml, '/config/file/warning_file')
487
- if !Etch.xmltext(Etch.xmlfindfirst(config_xml, '/config/file/warning_file')).empty?
488
- warning_file = Etch.xmltext(Etch.xmlfindfirst(config_xml, '/config/file/warning_file'))
489
- end
490
- elsif Etch.xmlfindfirst(@defaults_xml, '/config/file/warning_file')
491
- if !Etch.xmltext(Etch.xmlfindfirst(@defaults_xml, '/config/file/warning_file')).empty?
492
- warning_file = Etch.xmltext(Etch.xmlfindfirst(@defaults_xml, '/config/file/warning_file'))
493
- end
546
+ if config[:file][:warning_file] && !config[:file][:warning_file].empty?
547
+ warning_file = config[:file][:warning_file]
548
+ # This allows the user to set warning_file to false or an empty string in their
549
+ # config file to prevent the use of the default warning file
550
+ elsif !config[:file].include?(:warning_file)
551
+ warning_file = @defaults[:file][:warning_file]
494
552
  end
495
553
  if warning_file
554
+ warnpath = Pathname.new(warning_file)
555
+ if !File.exist?(warning_file) && !warnpath.absolute?
556
+ warning_file = File.expand_path(warning_file, @configdir)
557
+ end
558
+ end
559
+ if warning_file && File.exist?(warning_file)
496
560
  warning = ''
497
561
 
498
562
  # First the comment opener
499
- comment_open = nil
500
- if Etch.xmlfindfirst(config_xml, '/config/file/comment_open')
501
- comment_open = Etch.xmltext(Etch.xmlfindfirst(config_xml, '/config/file/comment_open'))
502
- elsif Etch.xmlfindfirst(@defaults_xml, '/config/file/comment_open')
503
- comment_open = Etch.xmltext(Etch.xmlfindfirst(@defaults_xml, '/config/file/comment_open'))
504
- end
563
+ comment_open = config[:file][:comment_open] || @defaults[:file][:comment_open]
505
564
  if comment_open && !comment_open.empty?
506
565
  warning << comment_open << "\n"
507
566
  end
508
567
 
509
568
  # Then the message
510
- comment_line = '# '
511
- if Etch.xmlfindfirst(config_xml, '/config/file/comment_line')
512
- comment_line = Etch.xmltext(Etch.xmlfindfirst(config_xml, '/config/file/comment_line'))
513
- elsif Etch.xmlfindfirst(@defaults_xml, '/config/file/comment_line')
514
- comment_line = Etch.xmltext(Etch.xmlfindfirst(@defaults_xml, '/config/file/comment_line'))
515
- end
516
-
517
- warnpath = Pathname.new(warning_file)
518
- if !File.exist?(warning_file) && !warnpath.absolute?
519
- warning_file = File.expand_path(warning_file, @configdir)
520
- end
569
+ comment_line = config[:file][:comment_line] || @defaults[:file][:comment_line] || '# '
521
570
 
522
571
  File.open(warning_file) do |warnfile|
523
572
  while line = warnfile.gets
@@ -526,12 +575,7 @@ class Etch
526
575
  end
527
576
 
528
577
  # And last the comment closer
529
- comment_close = nil
530
- if Etch.xmlfindfirst(config_xml, '/config/file/comment_close')
531
- comment_close = Etch.xmltext(Etch.xmlfindfirst(config_xml, '/config/file/comment_close'))
532
- elsif Etch.xmlfindfirst(@defaults_xml, '/config/file/comment_close')
533
- comment_close = Etch.xmltext(Etch.xmlfindfirst(@defaults_xml, '/config/file/comment_close'))
534
- end
578
+ comment_close = config[:file][:comment_close] || @defaults[:file][:comment_close]
535
579
  if comment_close && !comment_close.empty?
536
580
  warning << comment_close << "\n"
537
581
  end
@@ -541,20 +585,20 @@ class Etch
541
585
  # scripts) have a special first line. The user can flag
542
586
  # those files to have the warning inserted starting at the
543
587
  # second line.
544
- if !Etch.xmlfindfirst(config_xml, '/config/file/warning_on_second_line')
588
+ if !config[:file][:warning_on_second_line]
545
589
  # And then other files (notably Solaris crontabs) can't
546
590
  # have any blank lines. Normally we insert a blank
547
591
  # line between the warning message and the generated
548
592
  # file to improve readability. The user can flag us to
549
593
  # not insert that blank line.
550
- if !Etch.xmlfindfirst(config_xml, '/config/file/no_space_around_warning')
551
- newcontents = warning + "\n" + newcontents
594
+ if !config[:file][:no_space_around_warning]
595
+ newcontents = warning << "\n" << newcontents
552
596
  else
553
- newcontents = warning + newcontents
597
+ newcontents = warning << newcontents
554
598
  end
555
599
  else
556
600
  parts = newcontents.split("\n", 2)
557
- if !Etch.xmlfindfirst(config_xml, '/config/file/no_space_around_warning')
601
+ if !config[:file][:no_space_around_warning]
558
602
  newcontents = parts[0] << "\n\n" << warning << "\n" << parts[1]
559
603
  else
560
604
  newcontents = parts[0] << warning << parts[1]
@@ -563,54 +607,50 @@ class Etch
563
607
  end # if warning_file
564
608
 
565
609
  # Add the generated file contents to the XML
566
- Etch.xmladd(config_xml, '/config/file', 'contents', Base64.encode64(newcontents))
610
+ config[:file][:contents] = Base64.encode64(newcontents)
567
611
  end
568
612
 
569
- # Remove the source configuration from the XML, the
613
+ # Remove the source configuration from the config, the
570
614
  # client won't need to see it
571
- Etch.xmlremovepath(config_xml, '/config/file/source')
615
+ config[:file].delete(:plain)
616
+ config[:file].delete(:template)
617
+ config[:file].delete(:script)
572
618
 
573
- # Remove all of the warning related elements from the XML, the
619
+ # Remove all of the warning related elements from the config, the
574
620
  # client won't need to see them
575
- Etch.xmlremovepath(config_xml, '/config/file/warning_file')
576
- Etch.xmlremovepath(config_xml, '/config/file/warning_on_second_line')
577
- Etch.xmlremovepath(config_xml, '/config/file/no_space_around_warning')
578
- Etch.xmlremovepath(config_xml, '/config/file/comment_open')
579
- Etch.xmlremovepath(config_xml, '/config/file/comment_line')
580
- Etch.xmlremovepath(config_xml, '/config/file/comment_close')
621
+ config[:file].delete(:warning_file)
622
+ config[:file].delete(:warning_on_second_line)
623
+ config[:file].delete(:no_space_around_warning)
624
+ config[:file].delete(:comment_open)
625
+ config[:file].delete(:comment_line)
626
+ config[:file].delete(:comment_close)
581
627
 
582
- # If the XML doesn't contain ownership and permissions entries
628
+ # If the config doesn't contain ownership and permissions entries
583
629
  # then add appropriate ones based on the defaults
584
- if !Etch.xmlfindfirst(config_xml, '/config/file/owner')
585
- if Etch.xmlfindfirst(@defaults_xml, '/config/file/owner')
586
- Etch.xmlcopyelem(
587
- Etch.xmlfindfirst(@defaults_xml, '/config/file/owner'),
588
- Etch.xmlfindfirst(config_xml, '/config/file'))
630
+ if !config[:file][:owner]
631
+ if @defaults[:file][:owner]
632
+ config[:file][:owner] = @defaults[:file][:owner]
589
633
  else
590
- raise "defaults.xml needs /config/file/owner"
634
+ raise "defaults needs file->owner"
591
635
  end
592
636
  end
593
- if !Etch.xmlfindfirst(config_xml, '/config/file/group')
594
- if Etch.xmlfindfirst(@defaults_xml, '/config/file/group')
595
- Etch.xmlcopyelem(
596
- Etch.xmlfindfirst(@defaults_xml, '/config/file/group'),
597
- Etch.xmlfindfirst(config_xml, '/config/file'))
637
+ if !config[:file][:group]
638
+ if @defaults[:file][:group]
639
+ config[:file][:group] = @defaults[:file][:group]
598
640
  else
599
- raise "defaults.xml needs /config/file/group"
641
+ raise "defaults needs file->group"
600
642
  end
601
643
  end
602
- if !Etch.xmlfindfirst(config_xml, '/config/file/perms')
603
- if Etch.xmlfindfirst(@defaults_xml, '/config/file/perms')
604
- Etch.xmlcopyelem(
605
- Etch.xmlfindfirst(@defaults_xml, '/config/file/perms'),
606
- Etch.xmlfindfirst(config_xml, '/config/file'))
644
+ if !config[:file][:perms]
645
+ if @defaults[:file][:perms]
646
+ config[:file][:perms] = @defaults[:file][:perms]
607
647
  else
608
- raise "defaults.xml needs /config/file/perms"
648
+ raise "defaults needs file->perms"
609
649
  end
610
650
  end
611
651
 
612
652
  # Send the file contents and metadata to the client
613
- filter_xml!(config_xml, ['file'])
653
+ filter_config!(config, [:file])
614
654
 
615
655
  generation_status = :success
616
656
  throw :generate_done
@@ -621,33 +661,34 @@ class Etch
621
661
  # Symbolic link
622
662
  #
623
663
 
624
- if Etch.xmlfindfirst(config_xml, '/config/link')
664
+ if config[:link]
625
665
  dest = nil
626
-
627
- if Etch.xmlfindfirst(config_xml, '/config/link/dest')
628
- dest_elements = Etch.xmlarray(config_xml, '/config/link/dest')
629
- if check_for_inconsistency(dest_elements)
630
- raise "Inconsistent 'dest' entries for #{file}"
666
+ if config[:link][:dest] && !config[:link][:dest].empty?
667
+ if config[:link][:dest].kind_of?(Array)
668
+ if check_for_inconsistency(config[:link][:dest])
669
+ raise "Inconsistent 'dest' entries for #{file}"
670
+ end
671
+ dest = config[:link][:dest].first
672
+ else
673
+ dest = config[:link][:dest]
631
674
  end
632
-
633
- dest = Etch.xmltext(dest_elements.first)
634
- elsif Etch.xmlfindfirst(config_xml, '/config/link/script')
675
+ elsif config[:link][:script] && !config[:link][:script].empty?
635
676
  # The user can specify a script to perform more complex
636
677
  # testing to decide whether to create the link or not and
637
678
  # what its destination should be.
638
-
639
- script_elements = Etch.xmlarray(config_xml, '/config/link/script')
640
- if check_for_inconsistency(script_elements)
641
- raise "Inconsistent 'script' entries for #{file}"
679
+ if config[:link][:script].kind_of?(Array)
680
+ if check_for_inconsistency(config[:link][:script])
681
+ raise "Inconsistent 'script' entries for #{file}"
682
+ end
683
+ script = config[:link][:script].first
684
+ else
685
+ script = config[:link][:script]
642
686
  end
643
-
644
- script = Etch.xmltext(script_elements.first)
645
687
  external = EtchExternalSource.new(file, original_file, @facts, @groups, local_requests, @sourcebase, @commandsbase, @sitelibbase, @dlogger)
646
688
  dest = external.run_script(script)
647
-
648
- # Remove the script element(s) from the XML, the client won't need
649
- # to see them
650
- script_elements.each { |se| Etch.xmlremove(config_xml, se) }
689
+ # Remove the script entry from the config, the client won't need
690
+ # to see it
691
+ config[:link].delete(:script)
651
692
  else
652
693
  # If the filtering has removed the destination for the link,
653
694
  # that means it doesn't apply to this node.
@@ -657,44 +698,34 @@ class Etch
657
698
  if !dest || dest.empty?
658
699
  @dlogger.debug "Destination for link #{file} empty, doing nothing"
659
700
  else
660
- # If there isn't a dest element in the XML (if the user used a
661
- # script) then insert one for the benefit of the client
662
- if !Etch.xmlfindfirst(config_xml, '/config/link/dest')
663
- Etch.xmladd(config_xml, '/config/link', 'dest', dest)
664
- end
701
+ config[:link][:dest] = dest
665
702
 
666
- # If the XML doesn't contain ownership and permissions entries
703
+ # If the config doesn't contain ownership and permissions entries
667
704
  # then add appropriate ones based on the defaults
668
- if !Etch.xmlfindfirst(config_xml, '/config/link/owner')
669
- if Etch.xmlfindfirst(@defaults_xml, '/config/link/owner')
670
- Etch.xmlcopyelem(
671
- Etch.xmlfindfirst(@defaults_xml, '/config/link/owner'),
672
- Etch.xmlfindfirst(config_xml, '/config/link'))
705
+ if !config[:link][:owner]
706
+ if @defaults[:link][:owner]
707
+ config[:link][:owner] = @defaults[:link][:owner]
673
708
  else
674
- raise "defaults.xml needs /config/link/owner"
709
+ raise "defaults needs link->owner"
675
710
  end
676
711
  end
677
- if !Etch.xmlfindfirst(config_xml, '/config/link/group')
678
- if Etch.xmlfindfirst(@defaults_xml, '/config/link/group')
679
- Etch.xmlcopyelem(
680
- Etch.xmlfindfirst(@defaults_xml, '/config/link/group'),
681
- Etch.xmlfindfirst(config_xml, '/config/link'))
712
+ if !config[:link][:group]
713
+ if @defaults[:link][:group]
714
+ config[:link][:group] = @defaults[:link][:group]
682
715
  else
683
- raise "defaults.xml needs /config/link/group"
716
+ raise "defaults needs link->group"
684
717
  end
685
718
  end
686
- if !Etch.xmlfindfirst(config_xml, '/config/link/perms')
687
- if Etch.xmlfindfirst(@defaults_xml, '/config/link/perms')
688
- Etch.xmlcopyelem(
689
- Etch.xmlfindfirst(@defaults_xml, '/config/link/perms'),
690
- Etch.xmlfindfirst(config_xml, '/config/link'))
719
+ if !config[:link][:perms]
720
+ if @defaults[:link][:perms]
721
+ config[:link][:perms] = @defaults[:link][:perms]
691
722
  else
692
- raise "defaults.xml needs /config/link/perms"
723
+ raise "defaults needs link->perms"
693
724
  end
694
725
  end
695
726
 
696
727
  # Send the file contents and metadata to the client
697
- filter_xml!(config_xml, ['link'])
728
+ filter_config!(config, [:link])
698
729
 
699
730
  generation_status = :success
700
731
  throw :generate_done
@@ -705,26 +736,35 @@ class Etch
705
736
  # Directory
706
737
  #
707
738
 
708
- if Etch.xmlfindfirst(config_xml, '/config/directory')
739
+ if config[:directory]
709
740
  create = false
710
- if Etch.xmlfindfirst(config_xml, '/config/directory/create')
711
- create = true
712
- elsif Etch.xmlfindfirst(config_xml, '/config/directory/script')
741
+ if config[:directory][:create] &&
742
+ (!config[:directory][:create].kind_of?(Array) || !config[:directory][:create].empty?)
743
+ if config[:directory][:create].kind_of?(Array)
744
+ if check_for_inconsistency(config[:directory][:create])
745
+ raise "Inconsistent 'create' entries for #{file}"
746
+ end
747
+ create = config[:directory][:create].first
748
+ else
749
+ create = config[:directory][:create]
750
+ end
751
+ elsif config[:directory][:script] && !config[:directory][:script].empty?
713
752
  # The user can specify a script to perform more complex testing
714
753
  # to decide whether to create the directory or not.
715
- script_elements = Etch.xmlarray(config_xml, '/config/directory/script')
716
- if check_for_inconsistency(script_elements)
717
- raise "Inconsistent 'script' entries for #{file}"
754
+ if config[:directory][:script].kind_of?(Array)
755
+ if check_for_inconsistency(config[:directory][:script])
756
+ raise "Inconsistent 'script' entries for #{file}"
757
+ end
758
+ script = config[:directory][:script].first
759
+ else
760
+ script = config[:directory][:script]
718
761
  end
719
-
720
- script = Etch.xmltext(script_elements.first)
721
762
  external = EtchExternalSource.new(file, original_file, @facts, @groups, local_requests, @sourcebase, @commandsbase, @sitelibbase, @dlogger)
722
763
  create = external.run_script(script)
723
764
  create = false if create.empty?
724
-
725
- # Remove the script element(s) from the XML, the client won't need
726
- # to see them
727
- script_elements.each { |se| Etch.xmlremove(config_xml, se) }
765
+ # Remove the script entry from the config, the client won't need
766
+ # to see it
767
+ config[:directory].delete(:script)
728
768
  else
729
769
  # If the filtering has removed the directive to create this
730
770
  # directory, that means it doesn't apply to this node.
@@ -734,44 +774,34 @@ class Etch
734
774
  if !create
735
775
  @dlogger.debug "Directive to create directory #{file} false, doing nothing"
736
776
  else
737
- # If there isn't a create element in the XML (if the user used a
738
- # script) then insert one for the benefit of the client
739
- if !Etch.xmlfindfirst(config_xml, '/config/directory/create')
740
- Etch.xmladd(config_xml, '/config/directory', 'create', nil)
741
- end
777
+ config[:directory][:create] = create
742
778
 
743
- # If the XML doesn't contain ownership and permissions entries
779
+ # If the config doesn't contain ownership and permissions entries
744
780
  # then add appropriate ones based on the defaults
745
- if !Etch.xmlfindfirst(config_xml, '/config/directory/owner')
746
- if Etch.xmlfindfirst(@defaults_xml, '/config/directory/owner')
747
- Etch.xmlcopyelem(
748
- Etch.xmlfindfirst(@defaults_xml, '/config/directory/owner'),
749
- Etch.xmlfindfirst(config_xml, '/config/directory'))
781
+ if !config[:directory][:owner]
782
+ if @defaults[:directory][:owner]
783
+ config[:directory][:owner] = @defaults[:directory][:owner]
750
784
  else
751
- raise "defaults.xml needs /config/directory/owner"
785
+ raise "defaults.xml needs directory->owner"
752
786
  end
753
787
  end
754
- if !Etch.xmlfindfirst(config_xml, '/config/directory/group')
755
- if Etch.xmlfindfirst(@defaults_xml, '/config/directory/group')
756
- Etch.xmlcopyelem(
757
- Etch.xmlfindfirst(@defaults_xml, '/config/directory/group'),
758
- Etch.xmlfindfirst(config_xml, '/config/directory'))
788
+ if !config[:directory][:group]
789
+ if @defaults[:directory][:group]
790
+ config[:directory][:group] = @defaults[:directory][:group]
759
791
  else
760
- raise "defaults.xml needs /config/directory/group"
792
+ raise "defaults.xml needs directory->group"
761
793
  end
762
794
  end
763
- if !Etch.xmlfindfirst(config_xml, '/config/directory/perms')
764
- if Etch.xmlfindfirst(@defaults_xml, '/config/directory/perms')
765
- Etch.xmlcopyelem(
766
- Etch.xmlfindfirst(@defaults_xml, '/config/directory/perms'),
767
- Etch.xmlfindfirst(config_xml, '/config/directory'))
795
+ if !config[:directory][:perms]
796
+ if @defaults[:directory][:perms]
797
+ config[:directory][:perms] = @defaults[:directory][:perms]
768
798
  else
769
- raise "defaults.xml needs /config/directory/perms"
799
+ raise "defaults.xml needs directory->perms"
770
800
  end
771
801
  end
772
802
 
773
803
  # Send the file contents and metadata to the client
774
- filter_xml!(config_xml, ['directory'])
804
+ filter_config!(config, [:directory])
775
805
 
776
806
  generation_status = :success
777
807
  throw :generate_done
@@ -782,26 +812,35 @@ class Etch
782
812
  # Delete whatever is there
783
813
  #
784
814
 
785
- if Etch.xmlfindfirst(config_xml, '/config/delete')
815
+ if config[:delete]
786
816
  proceed = false
787
- if Etch.xmlfindfirst(config_xml, '/config/delete/proceed')
788
- proceed = true
789
- elsif Etch.xmlfindfirst(config_xml, '/config/delete/script')
817
+ if config[:delete][:proceed] &&
818
+ (!config[:delete][:proceed].kind_of?(Array) || !config[:delete][:proceed].empty?)
819
+ if config[:delete][:proceed].kind_of?(Array)
820
+ if check_for_inconsistency(config[:delete][:proceed])
821
+ raise "Inconsistent 'proceed' entries for #{file}"
822
+ end
823
+ proceed = config[:delete][:proceed].first
824
+ else
825
+ proceed = config[:delete][:proceed]
826
+ end
827
+ elsif config[:delete][:script] && !config[:delete][:script].empty?
790
828
  # The user can specify a script to perform more complex testing
791
829
  # to decide whether to delete the file or not.
792
- script_elements = Etch.xmlarray(config_xml, '/config/delete/script')
793
- if check_for_inconsistency(script_elements)
794
- raise "Inconsistent 'script' entries for #{file}"
830
+ if config[:delete][:script].kind_of?(Array)
831
+ if check_for_inconsistency(config[:delete][:script])
832
+ raise "Inconsistent 'script' entries for #{file}"
833
+ end
834
+ script = config[:delete][:script].first
835
+ else
836
+ script = config[:delete][:script]
795
837
  end
796
-
797
- script = Etch.xmltext(script_elements.first)
798
838
  external = EtchExternalSource.new(file, original_file, @facts, @groups, local_requests, @sourcebase, @commandsbase, @sitelibbase, @dlogger)
799
839
  proceed = external.run_script(script)
800
840
  proceed = false if proceed.empty?
801
-
802
- # Remove the script element(s) from the XML, the client won't need
803
- # to see them
804
- script_elements.each { |se| Etch.xmlremove(config_xml, se) }
841
+ # Remove the script entry from the config, the client won't need
842
+ # to see it
843
+ config[:delete].delete(:script)
805
844
  else
806
845
  # If the filtering has removed the directive to remove this
807
846
  # file, that means it doesn't apply to this node.
@@ -811,14 +850,10 @@ class Etch
811
850
  if !proceed
812
851
  @dlogger.debug "Directive to delete #{file} false, doing nothing"
813
852
  else
814
- # If there isn't a proceed element in the XML (if the user used a
815
- # script) then insert one for the benefit of the client
816
- if !Etch.xmlfindfirst(config_xml, '/config/delete/proceed')
817
- Etch.xmladd(config_xml, '/config/delete', 'proceed', nil)
818
- end
853
+ config[:delete][:proceed] = true
819
854
 
820
855
  # Send the file contents and metadata to the client
821
- filter_xml!(config_xml, ['delete'])
856
+ filter_config!(config, [:delete])
822
857
 
823
858
  generation_status = :success
824
859
  throw :generate_done
@@ -833,12 +868,8 @@ class Etch
833
868
  # In addition to successful configs return configs for files that need
834
869
  # orig data (generation_status==false) because any setup commands might be
835
870
  # needed to create the original file.
836
- if generation_status != :unknown &&
837
- Etch.xmlfindfirst(config_xml, '/config/*')
838
- # The client needs this attribute to know to which file
839
- # this chunk of XML refers
840
- Etch.xmlattradd(Etch.xmlroot(config_xml), 'filename', file)
841
- @configs[file] = config_xml
871
+ if generation_status != :unknown && !config.empty?
872
+ @configs[file] = config
842
873
  end
843
874
 
844
875
  @already_generated[file] = true
@@ -848,6 +879,45 @@ class Etch
848
879
  generation_status
849
880
  end
850
881
 
882
+ def load_config(file)
883
+ yamlconfig = "#{@sourcebase}/#{file}/config.yml"
884
+ xmlconfig = "#{@sourcebase}/#{file}/config.xml"
885
+ if File.exist?(yamlconfig)
886
+ config = symbolize_etch_keys(YAML.load(File.read(yamlconfig)))
887
+ config ||= {}
888
+ begin
889
+ yamlfilter!(config)
890
+ rescue Exception => e
891
+ raise Etch.wrap_exception(e, "Error filtering config.yml for #{file}:\n" + e.message)
892
+ end
893
+ elsif File.exist?(xmlconfig)
894
+ # Load the config.xml file
895
+ config_xml = nil
896
+ begin
897
+ config_xml = Etch.xmlload(xmlconfig)
898
+ rescue Exception => e
899
+ raise Etch.wrap_exception(e, "Error loading config.xml for #{file}:\n" + e.message)
900
+ end
901
+ # Filter the config.xml file by looking for attributes
902
+ begin
903
+ xmlfilter!(Etch.xmlroot(config_xml))
904
+ rescue Exception => e
905
+ raise Etch.wrap_exception(e, "Error filtering config.xml for #{file}:\n" + e.message)
906
+ end
907
+ # Validate the filtered file against config.dtd
908
+ @config_dtd ||= Etch.xmlloaddtd(@config_dtd_file)
909
+ begin
910
+ Etch.xmlvalidate(config_xml, @config_dtd)
911
+ rescue Exception => e
912
+ raise Etch.wrap_exception(e, "Filtered config.xml for #{file} fails validation:\n" + e.message)
913
+ end
914
+ config = Etch.config_xml_to_hash(config_xml)
915
+ else
916
+ raise "config.yml or config.xml for #{file} does not exist"
917
+ end
918
+ config
919
+ end
920
+
851
921
  # Returns the value of the generation_status variable, see comments in
852
922
  # method for possible values.
853
923
  def generate_commands(command, request)
@@ -855,7 +925,8 @@ class Etch
855
925
  # statements.
856
926
  if @already_generated[command]
857
927
  @dlogger.debug "Skipping already generated command #{command}"
858
- return
928
+ # Return the status of that previous generation
929
+ return @generation_status[command]
859
930
  end
860
931
 
861
932
  # Check for circular dependencies, otherwise we're vulnerable
@@ -865,27 +936,7 @@ class Etch
865
936
  end
866
937
  @filestack[command] = true
867
938
 
868
- commands_xml_file = File.join(@commandsbase, command, 'commands.xml')
869
- if !File.exist?(commands_xml_file)
870
- raise "commands.xml for #{command} does not exist"
871
- end
872
-
873
- # Load the commands.xml file
874
- commands_xml = Etch.xmlload(commands_xml_file)
875
-
876
- # Filter the commands.xml file by looking for attributes
877
- begin
878
- configfilter!(Etch.xmlroot(commands_xml))
879
- rescue Exception => e
880
- raise Etch.wrap_exception(e, "Error filtering commands.xml for #{command}:\n" + e.message)
881
- end
882
-
883
- # Validate the filtered file against commands.dtd
884
- begin
885
- Etch.xmlvalidate(commands_xml, @commands_dtd)
886
- rescue Exception => e
887
- raise Etch.wrap_exception(e, "Filtered commands.xml for #{command} fails validation:\n" + e.message)
888
- end
939
+ cmd = load_command(command)
889
940
 
890
941
  generation_status = :unknown
891
942
  # As we go through the process of generating the command we'll end up with
@@ -895,7 +946,7 @@ class Etch
895
946
  # generally the original file for a file this command depends on
896
947
  # success: we successfully processed a valid configuration
897
948
  # unknown: no valid configuration nor errors encountered, probably because
898
- # filtering removed everything from the commands.xml file. This
949
+ # filtering removed everything from the commands file. This
899
950
  # should be considered a successful outcome, it indicates the
900
951
  # caller/client provided us with all required data and our result
901
952
  # is that no action needs to be taken.
@@ -906,17 +957,15 @@ class Etch
906
957
  # Generate any other commands that this command depends on
907
958
  dependfiles = []
908
959
  proceed = true
909
- Etch.xmleach(commands_xml, '/commands/depend') do |depend|
910
- @dlogger.debug "Generating command dependency #{Etch.xmltext(depend)}"
911
- r = generate_commands(Etch.xmltext(depend), request)
912
- proceed = proceed && r
960
+ cmd[:depend] && cmd[:depend].each do |depend|
961
+ @dlogger.debug "Generating command dependency #{depend}"
962
+ proceed &= generate_commands(depend, request)
913
963
  end
914
964
  # Also generate any files that this command depends on
915
- Etch.xmleach(commands_xml, '/commands/dependfile') do |dependfile|
916
- @dlogger.debug "Generating file dependency #{Etch.xmltext(dependfile)}"
917
- dependfiles << Etch.xmltext(dependfile)
918
- r = generate_file(Etch.xmltext(dependfile), request)
919
- proceed = proceed && r
965
+ cmd[:dependfile] && cmd[:dependfile].each do |dependfile|
966
+ @dlogger.debug "Generating file dependency #{dependfile}"
967
+ dependfiles << dependfile
968
+ proceed &= generate_file(dependfile, request)
920
969
  end
921
970
  if !proceed
922
971
  @dlogger.debug "One or more dependencies of #{command} need data from client"
@@ -924,63 +973,47 @@ class Etch
924
973
  # contents from the client) then we need to tell the client to request
925
974
  # all of the files in the dependency tree again. See the big comment
926
975
  # in generate_file for further explanation.
927
- dependfiles.each { |dependfile| @need_orig[dependfile] = true }
976
+ dependfiles.each { |dependfile| @need_orig << dependfile }
928
977
  # Try again next time
929
978
  @retrycommands[command] = true
930
979
  generation_status = false
931
980
  throw :generate_done
932
981
  end
933
982
 
934
- # Change into the corresponding directory so that the user can
935
- # refer to source files and scripts by their relative pathnames.
936
- Dir.chdir "#{@commandsbase}/#{command}"
937
-
938
- # Check that the resulting document is consistent after filtering
939
- remove = []
940
- Etch.xmleach(commands_xml, '/commands/step') do |step|
941
- guard_exec_elements = Etch.xmlarray(step, 'guard/exec')
942
- if check_for_inconsistency(guard_exec_elements)
943
- raise "Inconsistent guard 'exec' entries for #{command}: " +
944
- guard_exec_elements.collect {|elem| Etch.xmltext(elem)}.join(',')
945
- end
946
- command_exec_elements = Etch.xmlarray(step, 'command/exec')
947
- if check_for_inconsistency(command_exec_elements)
948
- raise "Inconsistent command 'exec' entries for #{command}: " +
949
- command_exec_elements.collect {|elem| Etch.xmltext(elem)}.join(',')
950
- end
951
- # If filtering has removed both the guard and command elements
952
- # we can remove this step.
953
- if guard_exec_elements.empty? && command_exec_elements.empty?
954
- remove << step
955
- # If filtering has removed the guard but not the command or vice
956
- # versa that's an error.
957
- elsif guard_exec_elements.empty?
958
- raise "Filtering removed guard, but left command: " +
959
- Etch.xmltext(command_exec_elements.first)
960
- elsif command_exec_elements.empty?
961
- raise "Filtering removed command, but left guard: " +
962
- Etch.xmltext(guard_exec_elements.first)
983
+ if cmd[:steps]
984
+ remove = []
985
+ cmd[:steps].each do |outerstep|
986
+ if step = outerstep[:step]
987
+ if step[:guard] && !step[:guard].kind_of?(Array)
988
+ step[:guard] = [step[:guard]]
989
+ end
990
+ if step[:command] && !step[:command].kind_of?(Array)
991
+ step[:command] = [step[:command]]
992
+ end
993
+ # If filtering has removed both the guard and command elements
994
+ # then we can remove this step.
995
+ if (!step[:guard] || step[:guard].empty?) &&
996
+ (!step[:command] || step[:command].empty?)
997
+ remove << outerstep
998
+ # If filtering has removed the guard but not the command or vice
999
+ # versa that's an error.
1000
+ elsif !step[:guard] || step[:guard].empty?
1001
+ raise "Filtering removed guard, but left command: #{step[:command].join(';')}"
1002
+ elsif !step[:command] || step[:command].empty?
1003
+ raise "Filtering removed command, but left guard: #{step[:guard].join(';')}"
1004
+ else
1005
+ generation_status = :success
1006
+ end
1007
+ end
963
1008
  end
1009
+ remove.each{|outerstep| cmd[:steps].delete(outerstep)}
964
1010
  end
965
- remove.each { |elem| Etch.xmlremove(commands_xml, elem) }
966
-
967
- # I'm not sure if we'd benefit from further checking the XML for
968
- # validity. For now we declare success if we got this far.
969
- generation_status = :success
970
1011
  end
971
1012
 
972
- # Earlier we chdir'd into the command's directory in the repository. It
973
- # seems best not to leave this process with that as the cwd.
974
- Dir.chdir('/')
975
-
976
1013
  # If filtering didn't remove all the content then add this to the list of
977
1014
  # commands to be returned to the client.
978
- if generation_status && generation_status != :unknown &&
979
- Etch.xmlfindfirst(commands_xml, '/commands/*')
980
- # Include the commands directory name to aid troubleshooting on the
981
- # client side.
982
- Etch.xmlattradd(Etch.xmlroot(commands_xml), 'commandname', command)
983
- @allcommands[command] = commands_xml
1015
+ if generation_status && generation_status != :unknown && !cmd.empty?
1016
+ @commands[command] = cmd
984
1017
  end
985
1018
 
986
1019
  @already_generated[command] = true
@@ -990,22 +1023,152 @@ class Etch
990
1023
  generation_status
991
1024
  end
992
1025
 
993
- ALWAYS_KEEP = ['depend', 'setup', 'pre', 'test_before_post', 'post', 'test']
994
- def filter_xml_completely!(config_xml, keepers=[])
995
- remove = []
996
- Etch.xmleachall(config_xml) do |elem|
997
- if !keepers.include?(elem.name)
998
- remove << elem
1026
+ def load_command(command)
1027
+ yamlcommand = "#{@commandsbase}/#{command}/commands.yml"
1028
+ xmlcommand = "#{@commandsbase}/#{command}/commands.xml"
1029
+ if File.exist?(yamlcommand)
1030
+ cmd = symbolize_etch_keys(YAML.load(File.read(yamlcommand)))
1031
+ cmd ||= {}
1032
+ begin
1033
+ yamlfilter!(cmd)
1034
+ rescue Exception => e
1035
+ raise Etch.wrap_exception(e, "Error filtering commands.yml for #{command}:\n" + e.message)
999
1036
  end
1037
+ elsif File.exist?(xmlcommand)
1038
+ # Load the commands.xml file
1039
+ begin
1040
+ command_xml = Etch.xmlload(xmlcommand)
1041
+ rescue Exception => e
1042
+ raise Etch.wrap_exception(e, "Error loading commands.xml for #{command}:\n" + e.message)
1043
+ end
1044
+ # Filter the commands.xml file by looking for attributes
1045
+ begin
1046
+ xmlfilter!(Etch.xmlroot(command_xml))
1047
+ rescue Exception => e
1048
+ raise Etch.wrap_exception(e, "Error filtering commands.xml for #{command}:\n" + e.message)
1049
+ end
1050
+ # Validate the filtered file against commands.dtd
1051
+ @commands_dtd ||= Etch.xmlloaddtd(@commands_dtd_file)
1052
+ begin
1053
+ Etch.xmlvalidate(command_xml, @commands_dtd)
1054
+ rescue Exception => e
1055
+ raise Etch.wrap_exception(e, "Filtered commands.xml for #{command} fails validation:\n" + e.message)
1056
+ end
1057
+ # Convert the filtered XML to a hash
1058
+ cmd = Etch.command_xml_to_hash(command_xml)
1059
+ else
1060
+ raise "commands.yml or commands.xml for #{command} does not exist"
1000
1061
  end
1001
- remove.each { |elem| Etch.xmlremove(config_xml, elem) }
1002
- # FIXME: strip comments
1062
+ cmd
1063
+ end
1064
+
1065
+ ALWAYS_KEEP = [:depend, :setup, :pre, :test_before_post, :post, :post_once, :post_once_per_run, :test]
1066
+ def filter_config_completely!(config, keepers=[])
1067
+ config.reject!{|k,v| !keepers.include?(k)}
1003
1068
  end
1004
- def filter_xml!(config_xml, keepers=[])
1005
- filter_xml_completely!(config_xml, keepers.concat(ALWAYS_KEEP))
1069
+ def filter_config!(config, keepers=[])
1070
+ filter_config_completely!(config, keepers.concat(ALWAYS_KEEP))
1006
1071
  end
1007
1072
 
1008
- def configfilter!(element)
1073
+ def yamlfilter!(yaml)
1074
+ result = false
1075
+ case yaml
1076
+ when Hash
1077
+ remove = []
1078
+ yaml.each do |k,v|
1079
+ if v.kind_of?(Hash) &&
1080
+ v.length == 1 &&
1081
+ v.keys.first =~ /\Awhere (.*)/
1082
+ if eval_yaml_condition($1)
1083
+ yaml[k] = v.values.first
1084
+ else
1085
+ remove << k
1086
+ end
1087
+ end
1088
+ yamlfilter!(v)
1089
+ end
1090
+ remove.each{|k| yaml.delete(k)}
1091
+ when Array
1092
+ keep = []
1093
+ yaml.each do |e|
1094
+ if e.kind_of?(Hash) &&
1095
+ e.length == 1 &&
1096
+ e.keys.first =~ /\Awhere (.*)/
1097
+ if eval_yaml_condition($1)
1098
+ keep << e.values.first
1099
+ end
1100
+ else
1101
+ keep << e
1102
+ end
1103
+ yamlfilter!(e)
1104
+ end
1105
+ yaml.replace(keep)
1106
+ end
1107
+ end
1108
+ # Examples:
1109
+ # operatingsystem==Solaris
1110
+ # operatingsystem=~RedHat|CentOS and group==bar
1111
+ # operatingsystem=~RedHat|CentOS or kernel == SunOS and group==bar
1112
+ def eval_yaml_condition(condition)
1113
+ exprs = condition.split(/\s+(and|or)\s+/)
1114
+ prevcond = nil
1115
+ result = nil
1116
+ exprs.each do |expr|
1117
+ case expr
1118
+ when 'and'
1119
+ prevcond = :and
1120
+ when 'or'
1121
+ prevcond = :or
1122
+ else
1123
+ value = nil
1124
+ case
1125
+ when expr =~ /(.+?)\s*=~\s*(.+)/
1126
+ comps = comparables($1)
1127
+ regexp = Regexp.new($2)
1128
+ value = comps.any?{|c| c =~ regexp}
1129
+ when expr =~ /(.+?)\s*!~\s*(.+)/
1130
+ comps = comparables($1)
1131
+ regexp = Regexp.new($2)
1132
+ value = comps.any?{|c| c !~ regexp}
1133
+ when expr =~ /(.+?)\s*(<|<=|>=|>)\s*(.+)/
1134
+ comps = comparables($1)
1135
+ operator = $2.to_sym
1136
+ valueversion = Version.new($3)
1137
+ value = comps.any?{|c| Version.new(c).send(operator, valueversion)}
1138
+ when expr =~ /(.+?)\s*==\s*(.+)/
1139
+ comps = comparables($1)
1140
+ value = comps.include?($2)
1141
+ when expr =~ /(.+?)\s*!=\s*(.+)/
1142
+ comps = comparables($1)
1143
+ value = !comps.include?($2)
1144
+ when expr =~ /(.+?)\s+in\s+(.+)/
1145
+ comps = comparables($1)
1146
+ list = $2.split(/\s*,\s*/)
1147
+ value = list.any?{|item| comps.include?(item)}
1148
+ when expr =~ /(.+?)\s+!in\s+(.+)/
1149
+ comps = comparables($1)
1150
+ list = $2.split(/\s*,\s*/)
1151
+ value = list.none?{|item| comps.include?(item)}
1152
+ else
1153
+ raise "Unable to parse '#{condition}'"
1154
+ end
1155
+ case prevcond
1156
+ when :and
1157
+ result = result && value
1158
+ # False ands short circuit
1159
+ if !result
1160
+ return result
1161
+ end
1162
+ when :or
1163
+ result = result || value
1164
+ else
1165
+ result = value
1166
+ end
1167
+ end
1168
+ end
1169
+ result
1170
+ end
1171
+ def xmlfilter!(element)
1009
1172
  elem_remove = []
1010
1173
  Etch.xmleachall(element) do |elem|
1011
1174
  catch :next_element do
@@ -1020,12 +1183,19 @@ class Etch
1020
1183
  end
1021
1184
  attr_remove.each { |attribute| Etch.xmlattrremove(element, attribute) }
1022
1185
  # Then check any children of this element
1023
- configfilter!(elem)
1186
+ xmlfilter!(elem)
1024
1187
  end
1025
1188
  end
1026
1189
  elem_remove.each { |elem| Etch.xmlremove(element, elem) }
1027
1190
  end
1028
1191
 
1192
+ def comparables(name)
1193
+ if name == 'group'
1194
+ @groups
1195
+ elsif @facts[name]
1196
+ [@facts[name]]
1197
+ end
1198
+ end
1029
1199
  # Used when parsing each config.xml to filter out any elements which
1030
1200
  # don't match the configuration of this node. If the attribute matches
1031
1201
  # then we just remove the attribute but leave the element it is attached
@@ -1039,13 +1209,6 @@ class Etch
1039
1209
  # Not yet:
1040
1210
  # - Flow control (if/else)
1041
1211
  def check_attribute(name, value)
1042
- comparables = []
1043
- if name == 'group'
1044
- comparables = @groups
1045
- elsif @facts[name]
1046
- comparables = [@facts[name]]
1047
- end
1048
-
1049
1212
  result = false
1050
1213
  negate = false
1051
1214
 
@@ -1056,7 +1219,7 @@ class Etch
1056
1219
  value.sub!(/^\!/, '') # Strip off the bang
1057
1220
  end
1058
1221
 
1059
- comparables.each do |comp|
1222
+ comparables(name).each do |comp|
1060
1223
  # Numerical comparisons
1061
1224
  # i.e. <plain os="SunOS" osversion=">=5.8"></plain>
1062
1225
  # Note that the standard for XML requires that the < character be
@@ -1092,6 +1255,354 @@ class Etch
1092
1255
  end
1093
1256
  end
1094
1257
 
1258
+ def self.config_xml_to_hash(config_xml)
1259
+ config = {}
1260
+
1261
+ if Etch.xmlfindfirst(config_xml, '/config/revert')
1262
+ config[:revert] = true
1263
+ end
1264
+
1265
+ Etch.xmleach(config_xml, '/config/depend') do |depend|
1266
+ config[:depend] ||= []
1267
+ config[:depend] << Etch.xmltext(depend)
1268
+ end
1269
+ Etch.xmleach(config_xml, '/config/dependcommand') do |dependcommand|
1270
+ config[:dependcommand] ||= []
1271
+ config[:dependcommand] << Etch.xmltext(dependcommand)
1272
+ end
1273
+
1274
+ Etch.xmleach(config_xml, '/config/server_setup/exec') do |cmd|
1275
+ config[:server_setup] ||= []
1276
+ config[:server_setup] << Etch.xmltext(cmd)
1277
+ end
1278
+ Etch.xmleach(config_xml, '/config/setup/exec') do |cmd|
1279
+ config[:setup] ||= []
1280
+ config[:setup] << Etch.xmltext(cmd)
1281
+ end
1282
+ Etch.xmleach(config_xml, '/config/pre/exec') do |cmd|
1283
+ config[:pre] ||= []
1284
+ config[:pre] << Etch.xmltext(cmd)
1285
+ end
1286
+
1287
+ if Etch.xmlfindfirst(config_xml, '/config/file')
1288
+ config[:file] = {}
1289
+ end
1290
+ [:owner, :group, :perms, :warning_file, :comment_open,
1291
+ :comment_line, :comment_close].each do |meta|
1292
+ if metaelem = Etch.xmlfindfirst(config_xml, "/config/file/#{meta}")
1293
+ config[:file][meta] = Etch.xmltext(metaelem)
1294
+ end
1295
+ end
1296
+ [:always_manage_metadata, :warning_on_second_line,
1297
+ :no_space_around_warning, :allow_empty,
1298
+ :overwrite_directory].each do |bool|
1299
+ if Etch.xmlfindfirst(config_xml, "/config/file/#{bool}")
1300
+ config[:file][bool] = true
1301
+ end
1302
+ end
1303
+ [:plain, :template, :script].each do |sourcetype|
1304
+ Etch.xmleach(config_xml, "/config/file/source/#{sourcetype}") do |sourceelem|
1305
+ config[:file][sourcetype] ||= []
1306
+ config[:file][sourcetype] << Etch.xmltext(sourceelem)
1307
+ end
1308
+ end
1309
+
1310
+ if Etch.xmlfindfirst(config_xml, '/config/link')
1311
+ config[:link] = {}
1312
+ end
1313
+ [:owner, :group, :perms].each do |meta|
1314
+ if metaelem = Etch.xmlfindfirst(config_xml, "/config/link/#{meta}")
1315
+ config[:link][meta] = Etch.xmltext(metaelem)
1316
+ end
1317
+ end
1318
+ [:allow_nonexistent_dest, :overwrite_directory].each do |bool|
1319
+ if Etch.xmlfindfirst(config_xml, "/config/link/#{bool}")
1320
+ config[:link][bool] = true
1321
+ end
1322
+ end
1323
+ [:dest, :script].each do |sourcetype|
1324
+ Etch.xmleach(config_xml, "/config/link/#{sourcetype}") do |sourceelem|
1325
+ config[:link][sourcetype] ||= []
1326
+ config[:link][sourcetype] << Etch.xmltext(sourceelem)
1327
+ end
1328
+ end
1329
+
1330
+ if Etch.xmlfindfirst(config_xml, '/config/directory')
1331
+ config[:directory] = {}
1332
+ end
1333
+ [:owner, :group, :perms].each do |meta|
1334
+ if metaelem = Etch.xmlfindfirst(config_xml, "/config/directory/#{meta}")
1335
+ config[:directory][meta] = Etch.xmltext(metaelem)
1336
+ end
1337
+ end
1338
+ [:create].each do |bool|
1339
+ if Etch.xmlfindfirst(config_xml, "/config/directory/#{bool}")
1340
+ config[:directory][bool] = true
1341
+ end
1342
+ end
1343
+ [:script].each do |sourcetype|
1344
+ Etch.xmleach(config_xml, "/config/directory/#{sourcetype}") do |sourceelem|
1345
+ config[:directory][sourcetype] ||= []
1346
+ config[:directory][sourcetype] << Etch.xmltext(sourceelem)
1347
+ end
1348
+ end
1349
+
1350
+ if Etch.xmlfindfirst(config_xml, '/config/delete')
1351
+ config[:delete] = {}
1352
+ end
1353
+ [:overwrite_directory, :proceed].each do |bool|
1354
+ if Etch.xmlfindfirst(config_xml, "/config/delete/#{bool}")
1355
+ config[:delete][bool] = true
1356
+ end
1357
+ end
1358
+ [:script].each do |sourcetype|
1359
+ Etch.xmleach(config_xml, "/config/delete/#{sourcetype}") do |sourceelem|
1360
+ config[:delete][sourcetype] ||= []
1361
+ config[:delete][sourcetype] << Etch.xmltext(sourceelem)
1362
+ end
1363
+ end
1364
+
1365
+ Etch.xmleach(config_xml, '/config/test_before_post/exec') do |cmd|
1366
+ config[:test_before_post] ||= []
1367
+ config[:test_before_post] << Etch.xmltext(cmd)
1368
+ end
1369
+ Etch.xmleach(config_xml, '/config/post/exec') do |cmd|
1370
+ config[:post] ||= []
1371
+ config[:post] << Etch.xmltext(cmd)
1372
+ end
1373
+ Etch.xmleach(config_xml, '/config/post/exec_once') do |cmd|
1374
+ config[:post_once] ||= []
1375
+ config[:post_once] << Etch.xmltext(cmd)
1376
+ end
1377
+ Etch.xmleach(config_xml, '/config/post/exec_once_per_run') do |cmd|
1378
+ config[:post_once_per_run] ||= []
1379
+ config[:post_once_per_run] << Etch.xmltext(cmd)
1380
+ end
1381
+ Etch.xmleach(config_xml, '/config/test/exec') do |cmd|
1382
+ config[:test] ||= []
1383
+ config[:test] << Etch.xmltext(cmd)
1384
+ end
1385
+
1386
+ config
1387
+ end
1388
+ def self.config_hash_to_xml(config, file)
1389
+ doc = Etch.xmlnewdoc
1390
+ root = Etch.xmlnewelem('config', doc)
1391
+ Etch.xmlattradd(root, 'filename', file)
1392
+ Etch.xmlsetroot(doc, root)
1393
+ if config[:revert]
1394
+ root << Etch.xmlnewelem('revert', doc)
1395
+ end
1396
+ if config[:depend]
1397
+ config[:depend].each do |depend|
1398
+ depelem = Etch.xmlnewelem('depend', doc)
1399
+ Etch.xmlsettext(depelem, depend)
1400
+ root << depelem
1401
+ end
1402
+ end
1403
+ if config[:dependcommand]
1404
+ config[:dependcommand].each do |dependcommand|
1405
+ depelem = Etch.xmlnewelem('dependcommand', doc)
1406
+ Etch.xmlsettext(depelem, dependcommand)
1407
+ root << depelem
1408
+ end
1409
+ end
1410
+ if config[:setup]
1411
+ elem = Etch.xmlnewelem('setup', doc)
1412
+ config[:setup].each do |exec|
1413
+ execelem = Etch.xmlnewelem('exec', doc)
1414
+ Etch.xmlsettext(execelem, exec)
1415
+ elem << execelem
1416
+ end
1417
+ root << elem
1418
+ end
1419
+ if config[:pre]
1420
+ elem = Etch.xmlnewelem('pre', doc)
1421
+ config[:pre].each do |exec|
1422
+ execelem = Etch.xmlnewelem('exec', doc)
1423
+ Etch.xmlsettext(execelem, exec)
1424
+ elem << execelem
1425
+ end
1426
+ root << elem
1427
+ end
1428
+ if config[:file]
1429
+ fileelem = Etch.xmlnewelem('file', doc)
1430
+ root << fileelem
1431
+ [:owner, :group, :perms].each do |text|
1432
+ if config[:file][text]
1433
+ textelem = Etch.xmlnewelem(text.to_s, doc)
1434
+ Etch.xmlsettext(textelem, config[:file][text])
1435
+ fileelem << textelem
1436
+ end
1437
+ end
1438
+ [:overwrite_directory].each do |bool|
1439
+ if config[:file][bool]
1440
+ boolelem = Etch.xmlnewelem(bool.to_s, doc)
1441
+ fileelem << boolelem
1442
+ end
1443
+ end
1444
+ if config[:file][:contents]
1445
+ elem = Etch.xmlnewelem('contents', doc)
1446
+ Etch.xmlsettext(elem, config[:file][:contents])
1447
+ fileelem << elem
1448
+ end
1449
+ end
1450
+ if config[:link]
1451
+ linkelem = Etch.xmlnewelem('link', doc)
1452
+ root << linkelem
1453
+ [:owner, :group, :perms].each do |text|
1454
+ if config[:link][text]
1455
+ textelem = Etch.xmlnewelem(text.to_s, doc)
1456
+ Etch.xmlsettext(textelem, config[:link][text])
1457
+ linkelem << textelem
1458
+ end
1459
+ end
1460
+ [:allow_nonexistent_dest, :overwrite_directory].each do |bool|
1461
+ if config[:link][bool]
1462
+ boolelem = Etch.xmlnewelem(bool.to_s, doc)
1463
+ linkelem << boolelem
1464
+ end
1465
+ end
1466
+ if config[:link][:dest]
1467
+ elem = Etch.xmlnewelem('dest', doc)
1468
+ Etch.xmlsettext(elem, config[:link][:dest])
1469
+ linkelem << elem
1470
+ end
1471
+ end
1472
+ if config[:directory]
1473
+ direlem = Etch.xmlnewelem('directory', doc)
1474
+ root << direlem
1475
+ [:owner, :group, :perms].each do |text|
1476
+ if config[:directory][text]
1477
+ textelem = Etch.xmlnewelem(text.to_s, doc)
1478
+ Etch.xmlsettext(textelem, config[:directory][text])
1479
+ direlem << textelem
1480
+ end
1481
+ end
1482
+ [:create].each do |bool|
1483
+ if config[:directory][bool]
1484
+ boolelem = Etch.xmlnewelem(bool.to_s, doc)
1485
+ direlem << boolelem
1486
+ end
1487
+ end
1488
+ end
1489
+ if config[:delete]
1490
+ deleteelem = Etch.xmlnewelem('delete', doc)
1491
+ root << deleteelem
1492
+ [:overwrite_directory, :proceed].each do |bool|
1493
+ if config[:delete][bool]
1494
+ boolelem = Etch.xmlnewelem(bool.to_s, doc)
1495
+ deleteelem << boolelem
1496
+ end
1497
+ end
1498
+ end
1499
+ if config[:test_before_post]
1500
+ elem = Etch.xmlnewelem('test_before_post', doc)
1501
+ config[:test_before_post].each do |exec|
1502
+ execelem = Etch.xmlnewelem('exec', doc)
1503
+ Etch.xmlsettext(execelem, exec)
1504
+ elem << execelem
1505
+ end
1506
+ root << elem
1507
+ end
1508
+ postelem = nil
1509
+ {
1510
+ :post_once => :exec_once,
1511
+ :post_once_per_run => :exec_once_per_run,
1512
+ :post => :exec,
1513
+ }.each do |posttype, xmltype|
1514
+ if config[posttype]
1515
+ if !postelem
1516
+ postelem = Etch.xmlnewelem('post', doc)
1517
+ root << postelem
1518
+ end
1519
+ config[posttype].each do |postexec|
1520
+ execelem = Etch.xmlnewelem(xmltype.to_s, doc)
1521
+ Etch.xmlsettext(execelem, postexec)
1522
+ postelem << execelem
1523
+ end
1524
+ end
1525
+ end
1526
+ if config[:test]
1527
+ elem = Etch.xmlnewelem('test', doc)
1528
+ config[:test].each do |exec|
1529
+ execelem = Etch.xmlnewelem('exec', doc)
1530
+ Etch.xmlsettext(execelem, exec)
1531
+ elem << execelem
1532
+ end
1533
+ root << elem
1534
+ end
1535
+ doc
1536
+ end
1537
+ def self.command_xml_to_hash(command_xml)
1538
+ cmd = {}
1539
+ Etch.xmleach(command_xml, '/commands/depend') do |depend|
1540
+ cmd[:depend] ||= []
1541
+ cmd[:depend] << Etch.xmltext(depend)
1542
+ end
1543
+ Etch.xmleach(command_xml, '/commands/dependfile') do |dependfile|
1544
+ cmd[:dependfile] ||= []
1545
+ cmd[:dependfile] << Etch.xmltext(dependfile)
1546
+ end
1547
+ Etch.xmleach(command_xml, '/commands/step') do |step_xml|
1548
+ cmd[:steps] ||= []
1549
+ step = {}
1550
+ cmd[:steps] << {step: step}
1551
+ Etch.xmleach(step_xml, 'guard/exec') do |gexec|
1552
+ step[:guard] ||= []
1553
+ step[:guard] << Etch.xmltext(gexec)
1554
+ end
1555
+ Etch.xmleach(step_xml, 'command/exec') do |cexec|
1556
+ step[:command] ||= []
1557
+ step[:command] << Etch.xmltext(cexec)
1558
+ end
1559
+ end
1560
+ cmd
1561
+ end
1562
+ def self.command_hash_to_xml(cmd, commandname)
1563
+ doc = Etch.xmlnewdoc
1564
+ root = Etch.xmlnewelem('commands', doc)
1565
+ Etch.xmlattradd(root, 'commandname', commandname)
1566
+ Etch.xmlsetroot(doc, root)
1567
+ if cmd[:depend]
1568
+ cmd[:depend].each do |depend|
1569
+ depelem = Etch.xmlnewelem('depend', doc)
1570
+ Etch.xmlsettext(depelem, depend)
1571
+ root << depelem
1572
+ end
1573
+ end
1574
+ if cmd[:dependfile]
1575
+ cmd[:dependfile].each do |dependfile|
1576
+ depelem = Etch.xmlnewelem('dependfile', doc)
1577
+ Etch.xmlsettext(depelem, dependfile)
1578
+ root << depelem
1579
+ end
1580
+ end
1581
+ if cmd[:steps]
1582
+ cmd[:steps].each do |outerstep|
1583
+ if step = outerstep[:step]
1584
+ stepelem = Etch.xmlnewelem('step', doc)
1585
+ guardelem = Etch.xmlnewelem('guard', doc)
1586
+ step[:guard] && step[:guard].each do |exec|
1587
+ execelem = Etch.xmlnewelem('exec', doc)
1588
+ Etch.xmlsettext(execelem, exec)
1589
+ guardelem << execelem
1590
+ end
1591
+ stepelem << guardelem
1592
+ commandelem = Etch.xmlnewelem('command', doc)
1593
+ step[:command] && step[:command].each do |exec|
1594
+ execelem = Etch.xmlnewelem('exec', doc)
1595
+ Etch.xmlsettext(execelem, exec)
1596
+ commandelem << execelem
1597
+ end
1598
+ stepelem << commandelem
1599
+ root << stepelem
1600
+ end
1601
+ end
1602
+ end
1603
+ doc
1604
+ end
1605
+
1095
1606
  # We let users specify a source multiple times in a config.xml. This is
1096
1607
  # necessary if multiple groups require the same file, for example.
1097
1608
  # However, the user needs to be consistent. So this is valid on a
@@ -1109,12 +1620,7 @@ class Etch
1109
1620
  # This subroutine checks a list of XML elements to determine if they all
1110
1621
  # contain the same value. Returns true if there is inconsistency.
1111
1622
  def check_for_inconsistency(elements)
1112
- elements_as_text = elements.collect { |elem| Etch.xmltext(elem) }
1113
- if elements_as_text.uniq.length > 1
1114
- return true
1115
- else
1116
- return false
1117
- end
1623
+ elements.any?{|e| e != elements.first}
1118
1624
  end
1119
1625
 
1120
1626
  # These methods provide an abstraction from the underlying XML library in
@@ -1160,6 +1666,24 @@ class Etch
1160
1666
  end
1161
1667
  end
1162
1668
 
1669
+ def self.xmlloadstr(string)
1670
+ case Etch.xmllib
1671
+ when :libxml
1672
+ LibXML::XML::Document.string(string)
1673
+ when :nokogiri
1674
+ Nokogiri::XML(string) do |config|
1675
+ # Nokogiri is tolerant of malformed documents by default. Good when
1676
+ # parsing HTML, but there's no reason for us to tolerate errors. We
1677
+ # want to ensure that the user's instructions to us are clear.
1678
+ config.options = Nokogiri::XML::ParseOptions::STRICT
1679
+ end
1680
+ when :rexml
1681
+ REXML::Document.new(string)
1682
+ else
1683
+ raise "Unknown XML library #{Etch.xmllib}"
1684
+ end
1685
+ end
1686
+
1163
1687
  def self.xmlload(file)
1164
1688
  case Etch.xmllib
1165
1689
  when :libxml
@@ -1476,6 +2000,9 @@ class Etch
1476
2000
  end
1477
2001
 
1478
2002
  class EtchExternalSource
2003
+ # Save the original $LOAD_PATH ($:) to be restored later.
2004
+ @@load_path_org = $LOAD_PATH.clone
2005
+
1479
2006
  def initialize(file, original_file, facts, groups, local_requests, sourcebase, commandsbase, sitelibbase, dlogger)
1480
2007
  # The external source is going to be processed within the same Ruby
1481
2008
  # instance as etch. We want to make it clear what variables we are
@@ -1485,7 +2012,15 @@ class EtchExternalSource
1485
2012
  @original_file = original_file
1486
2013
  @facts = facts
1487
2014
  @groups = groups
1488
- @local_requests = local_requests
2015
+ # In the olden days all local requests were XML snippits that the etch client
2016
+ # smashed into a single XML document to send over the wire. This supports
2017
+ # scripts expecting the old interface.
2018
+ @local_requests = nil
2019
+ if local_requests
2020
+ @local_requests = "<requests>\n#{local_requests.join('')}\n</requests>"
2021
+ end
2022
+ # And this is a new interface where we just pass them as an array
2023
+ @local_requests_array = local_requests || []
1489
2024
  @sourcebase = sourcebase
1490
2025
  @commandsbase = commandsbase
1491
2026
  @sitelibbase = sitelibbase
@@ -1509,6 +2044,8 @@ class EtchExternalSource
1509
2044
  # Help the user figure out where the exception occurred, otherwise they
1510
2045
  # just get told it happened here, which isn't very helpful.
1511
2046
  raise Etch.wrap_exception(e, "Exception while processing template #{template} for file #{@file}:\n" + e.message)
2047
+ ensure
2048
+ restore_globals
1512
2049
  end
1513
2050
  end
1514
2051
 
@@ -1529,9 +2066,32 @@ class EtchExternalSource
1529
2066
  # just get told it happened here in eval, which isn't very helpful.
1530
2067
  raise Etch.wrap_exception(e, "Exception while processing script #{script} for file #{@file}:\n" + e.message)
1531
2068
  end
2069
+ ensure
2070
+ restore_globals
1532
2071
  end
1533
2072
  @contents
1534
2073
  end
2074
+
2075
+ #
2076
+ # Private subroutines
2077
+ #
2078
+ private
2079
+
2080
+ # Changes made to some global variables by the external sources can cause
2081
+ # serious complications because they are executed repeatedly in a single
2082
+ # worker process.
2083
+ # We need to initialize them after each execution in order to make them
2084
+ # "to act as much like a real script as possible".
2085
+ def restore_globals
2086
+ # Restore the original $LOAD_PATH to negate any changes made.
2087
+ $LOAD_PATH.replace @@load_path_org
2088
+ # Could restore the original $LOADED_FEATURES ($"), but this worker process
2089
+ # acculumates many gems and modules over time and it's not practical to
2090
+ # reload them every time.
2091
+ # So, just deleting those in @sitelibbase or @sourcebase directory.
2092
+ $LOADED_FEATURES.reject! {|x| x.start_with?(@sitelibbase, @sourcebase)}
2093
+ end
2094
+
1535
2095
  # The user might call return within a script. We want the scripts to act as
1536
2096
  # much like a real script as possible. Wrapping the eval in an extra method
1537
2097
  # allows us to handle a return within the script seamlessly. If the user