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.
- checksums.yaml +7 -0
- data/Rakefile +7 -8
- data/lib/etch.rb +959 -399
- data/lib/etch/client.rb +265 -335
- metadata +12 -16
checksums.yaml
ADDED
@@ -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 '
|
1
|
+
require 'rubygems/package_task'
|
2
2
|
spec = Gem::Specification.new do |s|
|
3
|
-
s.name
|
4
|
-
s.summary
|
3
|
+
s.name = 'etch'
|
4
|
+
s.summary = 'Etch system configuration management client'
|
5
5
|
s.add_dependency('facter')
|
6
|
-
s.version = '
|
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.
|
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
|
13
|
+
s.files = Dir['**/**']
|
14
14
|
s.executables = [ 'etch', 'etch_to_trunk', 'etch_cron_wrapper' ]
|
15
15
|
end
|
16
|
-
|
17
|
-
|
16
|
+
Gem::PackageTask.new(spec).define
|
data/lib/etch.rb
CHANGED
@@ -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
|
-
#
|
104
|
+
# These will be loaded on demand so that all-YAML configurations don't require them
|
106
105
|
#
|
107
106
|
|
108
|
-
@config_dtd =
|
109
|
-
@commands_dtd =
|
107
|
+
@config_dtd = nil
|
108
|
+
@commands_dtd = nil
|
110
109
|
|
111
110
|
#
|
112
|
-
# Load the defaults
|
113
|
-
# users don't specify in their config
|
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
|
-
@
|
115
|
+
@defaults = load_defaults
|
117
116
|
|
118
117
|
#
|
119
118
|
# Load the nodes file
|
120
119
|
#
|
121
120
|
|
122
|
-
|
121
|
+
groups = Set.new
|
122
|
+
@nodes, nodesfile = load_nodes
|
123
123
|
# Extract the groups for this node
|
124
|
-
|
125
|
-
|
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
|
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
|
129
|
+
#raise "No entry found for node #{@fqdn} in #{nodesfile}"
|
132
130
|
end
|
133
|
-
@dlogger.debug "Native groups for node #{@fqdn}: #{
|
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
|
-
@
|
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
|
-
|
152
|
-
|
153
|
-
parents
|
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
|
-
|
157
|
-
@dlogger.debug "Added groups for node #{@fqdn} due to node group hierarchy: #{
|
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
|
-
|
149
|
+
#
|
150
|
+
|
151
|
+
externals = Set.new
|
161
152
|
IO.popen(File.join(@configdir, 'nodegrouper') + ' ' + @fqdn) do |pipe|
|
162
|
-
pipe.each
|
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
|
-
|
168
|
-
@dlogger.debug "Added groups for node #{@fqdn} due to external node grouper: #{
|
158
|
+
groups.merge externals
|
159
|
+
@dlogger.debug "Added groups for node #{@fqdn} due to external node grouper: #{externals.sort.join(',')}"
|
169
160
|
|
170
|
-
@groups =
|
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) &&
|
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('
|
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
|
-
@
|
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) &&
|
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
|
-
:
|
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
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
328
|
-
@dlogger.debug "Generating dependency #{
|
329
|
-
|
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
|
-
|
335
|
-
@dlogger.debug "Generating command dependency #{
|
336
|
-
r = generate_commands(
|
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
|
-
|
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
|
426
|
+
@need_orig << file
|
371
427
|
|
372
428
|
# Strip this file's config down to the bare necessities
|
373
|
-
|
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
|
444
|
+
if config[:revert]
|
389
445
|
# Pass the revert action back to the client
|
390
|
-
|
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
|
452
|
+
if config[:server_setup]
|
397
453
|
@dlogger.debug "Processing server setup commands"
|
398
|
-
|
399
|
-
@dlogger.debug " Executing #{
|
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',
|
458
|
+
success = system('/bin/sh', '-c', cmd)
|
403
459
|
if !success
|
404
|
-
raise "Server setup command #{
|
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
|
475
|
+
if config[:file]
|
420
476
|
#
|
421
477
|
# Assemble the contents for the file
|
422
478
|
#
|
423
479
|
newcontents = ''
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
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
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
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
|
445
|
-
|
446
|
-
|
447
|
-
|
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
|
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
|
-
!
|
474
|
-
!
|
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
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
elsif
|
491
|
-
|
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 =
|
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 =
|
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 !
|
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 !
|
551
|
-
newcontents = warning
|
594
|
+
if !config[:file][:no_space_around_warning]
|
595
|
+
newcontents = warning << "\n" << newcontents
|
552
596
|
else
|
553
|
-
newcontents = warning
|
597
|
+
newcontents = warning << newcontents
|
554
598
|
end
|
555
599
|
else
|
556
600
|
parts = newcontents.split("\n", 2)
|
557
|
-
if !
|
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
|
-
|
610
|
+
config[:file][:contents] = Base64.encode64(newcontents)
|
567
611
|
end
|
568
612
|
|
569
|
-
# Remove the source configuration from the
|
613
|
+
# Remove the source configuration from the config, the
|
570
614
|
# client won't need to see it
|
571
|
-
|
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
|
619
|
+
# Remove all of the warning related elements from the config, the
|
574
620
|
# client won't need to see them
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
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
|
628
|
+
# If the config doesn't contain ownership and permissions entries
|
583
629
|
# then add appropriate ones based on the defaults
|
584
|
-
if !
|
585
|
-
if
|
586
|
-
|
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
|
634
|
+
raise "defaults needs file->owner"
|
591
635
|
end
|
592
636
|
end
|
593
|
-
if !
|
594
|
-
if
|
595
|
-
|
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
|
641
|
+
raise "defaults needs file->group"
|
600
642
|
end
|
601
643
|
end
|
602
|
-
if !
|
603
|
-
if
|
604
|
-
|
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
|
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
|
-
|
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
|
664
|
+
if config[:link]
|
625
665
|
dest = nil
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
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
|
-
|
640
|
-
|
641
|
-
|
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
|
-
#
|
649
|
-
|
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
|
-
|
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
|
703
|
+
# If the config doesn't contain ownership and permissions entries
|
667
704
|
# then add appropriate ones based on the defaults
|
668
|
-
if !
|
669
|
-
if
|
670
|
-
|
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
|
709
|
+
raise "defaults needs link->owner"
|
675
710
|
end
|
676
711
|
end
|
677
|
-
if !
|
678
|
-
if
|
679
|
-
|
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
|
716
|
+
raise "defaults needs link->group"
|
684
717
|
end
|
685
718
|
end
|
686
|
-
if !
|
687
|
-
if
|
688
|
-
|
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
|
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
|
-
|
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
|
739
|
+
if config[:directory]
|
709
740
|
create = false
|
710
|
-
if
|
711
|
-
|
712
|
-
|
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
|
-
|
716
|
-
|
717
|
-
|
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
|
-
#
|
726
|
-
|
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
|
-
|
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
|
779
|
+
# If the config doesn't contain ownership and permissions entries
|
744
780
|
# then add appropriate ones based on the defaults
|
745
|
-
if !
|
746
|
-
if
|
747
|
-
|
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
|
785
|
+
raise "defaults.xml needs directory->owner"
|
752
786
|
end
|
753
787
|
end
|
754
|
-
if !
|
755
|
-
if
|
756
|
-
|
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
|
792
|
+
raise "defaults.xml needs directory->group"
|
761
793
|
end
|
762
794
|
end
|
763
|
-
if !
|
764
|
-
if
|
765
|
-
|
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
|
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
|
-
|
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
|
815
|
+
if config[:delete]
|
786
816
|
proceed = false
|
787
|
-
if
|
788
|
-
|
789
|
-
|
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
|
-
|
793
|
-
|
794
|
-
|
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
|
-
#
|
803
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
910
|
-
@dlogger.debug "Generating command dependency #{
|
911
|
-
|
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
|
-
|
916
|
-
@dlogger.debug "Generating file dependency #{
|
917
|
-
dependfiles <<
|
918
|
-
|
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
|
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
|
-
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
|
944
|
-
|
945
|
-
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
|
958
|
-
|
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
|
-
|
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
|
-
|
994
|
-
|
995
|
-
|
996
|
-
|
997
|
-
|
998
|
-
|
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
|
-
|
1002
|
-
|
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
|
1005
|
-
|
1069
|
+
def filter_config!(config, keepers=[])
|
1070
|
+
filter_config_completely!(config, keepers.concat(ALWAYS_KEEP))
|
1006
1071
|
end
|
1007
1072
|
|
1008
|
-
def
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|