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