etch 4.0.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
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