etch 3.17.0 → 3.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (6) hide show
  1. data/Rakefile +1 -1
  2. data/bin/etch +7 -0
  3. data/bin/etch_to_trunk +9 -1
  4. data/lib/etch.rb +177 -58
  5. data/lib/etchclient.rb +81 -17
  6. metadata +9 -26
data/Rakefile CHANGED
@@ -3,7 +3,7 @@ spec = Gem::Specification.new do |s|
3
3
  s.name = 'etch'
4
4
  s.summary = 'Etch system configuration management client'
5
5
  s.add_dependency('facter')
6
- s.version = '3.17.0'
6
+ s.version = '3.19.0'
7
7
  s.author = 'Jason Heiss'
8
8
  s.email = 'etch-users@lists.sourceforge.net'
9
9
  s.homepage = 'http://etch.sourceforge.net'
data/bin/etch CHANGED
@@ -35,6 +35,13 @@ opts.on('--damp-run', "Perform a dry run but run 'setup' entries for files.") do
35
35
  # entries.
36
36
  options[:dryrun] = 'damp'
37
37
  end
38
+ opts.on('--list-files', 'Just list the files that would be configured') do |opt|
39
+ options[:listfiles] = opt
40
+ # generate all is implied
41
+ @generateall = true
42
+ # Set :dryrun as a extra measure to make sure we don't change anything
43
+ options[:dryrun] = 'listfiles'
44
+ end
38
45
  opts.on('--interactive', 'Prompt for confirmation before each change.') do |opt|
39
46
  options[:interactive] = opt
40
47
  end
data/bin/etch_to_trunk CHANGED
@@ -15,6 +15,9 @@ end
15
15
  opts.on('-t', '--timezone TIMEZONE', 'Time zone of etch server.') do |opt|
16
16
  options[:timezone] = opt
17
17
  end
18
+ opts.on('--nv SERVER', 'Where nVentory server is running.') do |opt|
19
+ options[:nv_server] = opt
20
+ end
18
21
  opts.on_tail('-h', '--help', 'Show this message.') do
19
22
  puts opts
20
23
  exit
@@ -37,7 +40,12 @@ else # if no timezone is specified then just use local time for the tag
37
40
  end
38
41
 
39
42
  # Find the requested clients
40
- nvclient = NVentory::Client.new
43
+ nv_server = options[:nv_server]
44
+ if nv_server
45
+ nvclient = NVentory::Client.new(:server=>"http://#{nv_server}")
46
+ else
47
+ nvclient = NVentory::Client.new
48
+ end
41
49
  results = nvclient.get_objects('nodes', {}, { 'name' => nodes }, {}, {})
42
50
  nodes.each do |name|
43
51
  if results.empty? && results[name].nil?
data/lib/etch.rb CHANGED
@@ -3,6 +3,19 @@ require 'pathname' # absolute?
3
3
  require 'digest/sha1' # hexdigest
4
4
  require 'base64' # decode64, encode64
5
5
  require 'fileutils' # mkdir_p
6
+ require 'erb'
7
+ require 'versiontype' # Version
8
+ require 'logger'
9
+
10
+ class Etch
11
+ def self.xmllib
12
+ @@xmllib
13
+ end
14
+ def self.xmllib=(lib)
15
+ @@xmllib=lib
16
+ end
17
+ end
18
+
6
19
  # By default we try to use libxml, falling back to rexml if it is not
7
20
  # available. The xmllib environment variable can be used to force one library
8
21
  # or the other, mostly for testing purposes.
@@ -10,24 +23,24 @@ begin
10
23
  if !ENV['xmllib'] || ENV['xmllib'] == 'libxml'
11
24
  require 'rubygems' # libxml is a gem
12
25
  require 'libxml'
13
- @@xmllib = :libxml
26
+ Etch.xmllib = :libxml
27
+ elsif ENV['xmllib'] == 'nokogiri'
28
+ require 'rubygems' # nokogiri is a gem
29
+ require 'nokogiri'
30
+ Etch.xmllib = :nokogiri
14
31
  else
15
32
  raise LoadError
16
33
  end
17
34
  rescue LoadError
18
35
  if !ENV['xmllib'] || ENV['xmllib'] == 'rexml'
19
36
  require 'rexml/document'
20
- @@xmllib = :rexml
37
+ Etch.xmllib = :rexml
21
38
  else
22
39
  raise
23
40
  end
24
41
  end
25
- require 'erb'
26
- require 'versiontype' # Version
27
- require 'logger'
28
42
 
29
43
  class Etch
30
-
31
44
  # FIXME: I'm not really proud of this, it seems like there ought to be a way
32
45
  # to just use one logger. The problem is that on the server we'd like to
33
46
  # use RAILS_DEFAULT_LOGGER for general logging (which is logging to
@@ -122,7 +135,7 @@ class Etch
122
135
  Etch.xmleach(@nodegroups_xml, '/nodegroups/nodegroup') do |parent|
123
136
  Etch.xmleach(parent, 'child') do |child|
124
137
  @group_hierarchy[Etch.xmltext(child)] = [] if !@group_hierarchy[Etch.xmltext(child)]
125
- @group_hierarchy[Etch.xmltext(child)] << parent.attributes['name']
138
+ @group_hierarchy[Etch.xmltext(child)] << Etch.xmlattrvalue(parent, 'name')
126
139
  end
127
140
  end
128
141
 
@@ -270,8 +283,10 @@ class Etch
270
283
  end
271
284
 
272
285
  # Validate the filtered file against config.dtd
273
- if !Etch.xmlvalidate(config_xml, @config_dtd)
274
- raise "Filtered config.xml for #{file} fails validation"
286
+ begin
287
+ Etch.xmlvalidate(config_xml, @config_dtd)
288
+ rescue Exception => e
289
+ raise Etch.wrap_exception(e, "Filtered config.xml for #{file} fails validation:\n" + e.message)
275
290
  end
276
291
 
277
292
  generation_status = :unknown
@@ -846,8 +861,10 @@ class Etch
846
861
  end
847
862
 
848
863
  # Validate the filtered file against commands.dtd
849
- if !Etch.xmlvalidate(commands_xml, @commands_dtd)
850
- raise "Filtered commands.xml for #{command} fails validation"
864
+ begin
865
+ Etch.xmlvalidate(commands_xml, @commands_dtd)
866
+ rescue Exception => e
867
+ raise Etch.wrap_exception(e, "Filtered commands.xml for #{command} fails validation:\n" + e.message)
851
868
  end
852
869
 
853
870
  generation_status = :unknown
@@ -1076,102 +1093,154 @@ class Etch
1076
1093
  end
1077
1094
  end
1078
1095
 
1096
+ # These methods provide an abstraction from the underlying XML library in
1097
+ # use, allowing us to use whatever the user has available and switch between
1098
+ # libraries easily.
1099
+
1079
1100
  def self.xmlnewdoc
1080
- case @@xmllib
1101
+ case Etch.xmllib
1081
1102
  when :libxml
1082
1103
  LibXML::XML::Document.new
1104
+ when :nokogiri
1105
+ Nokogiri::XML::Document.new
1083
1106
  when :rexml
1084
1107
  REXML::Document.new
1085
1108
  else
1086
- raise "Unknown @xmllib #{@xmllib}"
1109
+ raise "Unknown XML library #{Etch.xmllib}"
1087
1110
  end
1088
1111
  end
1089
1112
 
1090
1113
  def self.xmlroot(doc)
1091
- case @@xmllib
1114
+ case Etch.xmllib
1092
1115
  when :libxml
1093
1116
  doc.root
1117
+ when :nokogiri
1118
+ doc.root
1094
1119
  when :rexml
1095
1120
  doc.root
1096
1121
  else
1097
- raise "Unknown @xmllib #{@xmllib}"
1122
+ raise "Unknown XML library #{Etch.xmllib}"
1098
1123
  end
1099
1124
  end
1100
1125
 
1101
1126
  def self.xmlsetroot(doc, root)
1102
- case @@xmllib
1127
+ case Etch.xmllib
1103
1128
  when :libxml
1104
1129
  doc.root = root
1130
+ when :nokogiri
1131
+ doc.root = root
1105
1132
  when :rexml
1106
1133
  doc << root
1107
1134
  else
1108
- raise "Unknown @xmllib #{@xmllib}"
1135
+ raise "Unknown XML library #{Etch.xmllib}"
1109
1136
  end
1110
1137
  end
1111
1138
 
1112
1139
  def self.xmlload(file)
1113
- case @@xmllib
1140
+ case Etch.xmllib
1114
1141
  when :libxml
1115
1142
  LibXML::XML::Document.file(file)
1143
+ when :nokogiri
1144
+ Nokogiri::XML(File.open(file)) do |config|
1145
+ # Nokogiri is tolerant of malformed documents by default. Good when
1146
+ # parsing HTML, but there's no reason for us to tolerate errors. We
1147
+ # want to ensure that the user's instructions to us are clear.
1148
+ config.options = Nokogiri::XML::ParseOptions::STRICT
1149
+ end
1116
1150
  when :rexml
1117
1151
  REXML::Document.new(File.open(file))
1118
1152
  else
1119
- raise "Unknown @xmllib #{@xmllib}"
1153
+ raise "Unknown XML library #{Etch.xmllib}"
1120
1154
  end
1121
1155
  end
1122
1156
 
1123
1157
  def self.xmlloaddtd(dtdfile)
1124
- case @@xmllib
1158
+ case Etch.xmllib
1125
1159
  when :libxml
1126
1160
  LibXML::XML::Dtd.new(IO.read(dtdfile))
1161
+ when :nokogiri
1162
+ # For some reason there isn't a straightforward way to load a standalone
1163
+ # DTD in Nokogiri
1164
+ dtddoctext = '<!DOCTYPE dtd [' + File.read(dtdfile) + ']'
1165
+ dtddoc = Nokogiri::XML(dtddoctext)
1166
+ dtddoc.children.first
1127
1167
  when :rexml
1128
1168
  nil
1129
1169
  else
1130
- raise "Unknown @xmllib #{@xmllib}"
1170
+ raise "Unknown XML library #{Etch.xmllib}"
1131
1171
  end
1132
1172
  end
1133
1173
 
1174
+ # Returns true if validation is successful, or if validation is not
1175
+ # supported by the XML library in use. Raises an exception if validation
1176
+ # fails.
1134
1177
  def self.xmlvalidate(xmldoc, dtd)
1135
- case @@xmllib
1178
+ case Etch.xmllib
1136
1179
  when :libxml
1137
- xmldoc.validate(dtd)
1180
+ result = xmldoc.validate(dtd)
1181
+ # LibXML::XML::Document#validate is documented to return false if
1182
+ # validation fails. However, as currently implemented it raises an
1183
+ # exception instead. Just in case that behavior ever changes raise an
1184
+ # exception if a false value is returned.
1185
+ if result
1186
+ true
1187
+ else
1188
+ raise "Validation failed"
1189
+ end
1190
+ when :nokogiri
1191
+ errors = dtd.validate(xmldoc)
1192
+ if errors.empty?
1193
+ true
1194
+ else
1195
+ raise errors.join('|')
1196
+ end
1138
1197
  when :rexml
1139
1198
  true
1140
1199
  else
1141
- raise "Unknown @xmllib #{@xmllib}"
1200
+ raise "Unknown XML library #{Etch.xmllib}"
1142
1201
  end
1143
1202
  end
1144
1203
 
1145
- def self.xmlnewelem(name)
1146
- case @@xmllib
1204
+ def self.xmlnewelem(name, doc)
1205
+ case Etch.xmllib
1147
1206
  when :libxml
1148
1207
  LibXML::XML::Node.new(name)
1208
+ when :nokogiri
1209
+ Nokogiri::XML::Element.new(name, doc)
1149
1210
  when :rexml
1150
1211
  REXML::Element.new(name)
1151
1212
  else
1152
- raise "Unknown @xmllib #{@xmllib}"
1213
+ raise "Unknown XML library #{Etch.xmllib}"
1153
1214
  end
1154
1215
  end
1155
1216
 
1156
1217
  def self.xmleach(xmldoc, xpath, &block)
1157
- case @@xmllib
1218
+ case Etch.xmllib
1158
1219
  when :libxml
1159
1220
  xmldoc.find(xpath).each(&block)
1221
+ when :nokogiri
1222
+ xmldoc.xpath(xpath).each(&block)
1160
1223
  when :rexml
1161
1224
  xmldoc.elements.each(xpath, &block)
1162
1225
  else
1163
- raise "Unknown @xmllib #{@xmllib}"
1226
+ raise "Unknown XML library #{Etch.xmllib}"
1164
1227
  end
1165
1228
  end
1166
1229
 
1167
1230
  def self.xmleachall(xmldoc, &block)
1168
- case @@xmllib
1231
+ case Etch.xmllib
1169
1232
  when :libxml
1170
1233
  if xmldoc.kind_of?(LibXML::XML::Document)
1171
1234
  xmldoc.root.each_element(&block)
1172
1235
  else
1173
1236
  xmldoc.each_element(&block)
1174
1237
  end
1238
+ when :nokogiri
1239
+ if xmldoc.kind_of?(Nokogiri::XML::Document)
1240
+ xmldoc.root.element_children.each(&block)
1241
+ else
1242
+ xmldoc.element_children.each(&block)
1243
+ end
1175
1244
  when :rexml
1176
1245
  if xmldoc.node_type == :document
1177
1246
  xmldoc.root.elements.each(&block)
@@ -1179,23 +1248,25 @@ class Etch
1179
1248
  xmldoc.elements.each(&block)
1180
1249
  end
1181
1250
  else
1182
- raise "Unknown @xmllib #{@xmllib}"
1251
+ raise "Unknown XML library #{Etch.xmllib}"
1183
1252
  end
1184
1253
  end
1185
1254
 
1186
1255
  def self.xmleachattrall(elem, &block)
1187
- case @@xmllib
1256
+ case Etch.xmllib
1188
1257
  when :libxml
1189
1258
  elem.attributes.each(&block)
1259
+ when :nokogiri
1260
+ elem.attribute_nodes.each(&block)
1190
1261
  when :rexml
1191
1262
  elem.attributes.each_attribute(&block)
1192
1263
  else
1193
- raise "Unknown @xmllib #{@xmllib}"
1264
+ raise "Unknown XML library #{Etch.xmllib}"
1194
1265
  end
1195
1266
  end
1196
1267
 
1197
1268
  def self.xmlarray(xmldoc, xpath)
1198
- case @@xmllib
1269
+ case Etch.xmllib
1199
1270
  when :libxml
1200
1271
  elements = xmldoc.find(xpath)
1201
1272
  if elements
@@ -1203,28 +1274,34 @@ class Etch
1203
1274
  else
1204
1275
  []
1205
1276
  end
1277
+ when :nokogiri
1278
+ xmldoc.xpath(xpath).to_a
1206
1279
  when :rexml
1207
1280
  xmldoc.elements.to_a(xpath)
1208
1281
  else
1209
- raise "Unknown @xmllib #{@xmllib}"
1282
+ raise "Unknown XML library #{Etch.xmllib}"
1210
1283
  end
1211
1284
  end
1212
1285
 
1213
1286
  def self.xmlfindfirst(xmldoc, xpath)
1214
- case @@xmllib
1287
+ case Etch.xmllib
1215
1288
  when :libxml
1216
1289
  xmldoc.find_first(xpath)
1290
+ when :nokogiri
1291
+ xmldoc.at_xpath(xpath)
1217
1292
  when :rexml
1218
1293
  xmldoc.elements[xpath]
1219
1294
  else
1220
- raise "Unknown @xmllib #{@xmllib}"
1295
+ raise "Unknown XML library #{Etch.xmllib}"
1221
1296
  end
1222
1297
  end
1223
1298
 
1224
1299
  def self.xmltext(elem)
1225
- case @@xmllib
1300
+ case Etch.xmllib
1226
1301
  when :libxml
1227
1302
  elem.content
1303
+ when :nokogiri
1304
+ elem.content
1228
1305
  when :rexml
1229
1306
  text = elem.text
1230
1307
  # REXML returns nil rather than '' if there is no text
@@ -1234,57 +1311,67 @@ class Etch
1234
1311
  ''
1235
1312
  end
1236
1313
  else
1237
- raise "Unknown @xmllib #{@xmllib}"
1314
+ raise "Unknown XML library #{Etch.xmllib}"
1238
1315
  end
1239
1316
  end
1240
1317
 
1241
1318
  def self.xmlsettext(elem, text)
1242
- case @@xmllib
1319
+ case Etch.xmllib
1243
1320
  when :libxml
1244
1321
  elem.content = text
1322
+ when :nokogiri
1323
+ elem.content = text
1245
1324
  when :rexml
1246
1325
  elem.text = text
1247
1326
  else
1248
- raise "Unknown @xmllib #{@xmllib}"
1327
+ raise "Unknown XML library #{Etch.xmllib}"
1249
1328
  end
1250
1329
  end
1251
1330
 
1252
1331
  def self.xmladd(xmldoc, xpath, name, contents=nil)
1253
- case @@xmllib
1332
+ case Etch.xmllib
1254
1333
  when :libxml
1255
1334
  elem = LibXML::XML::Node.new(name)
1256
1335
  if contents
1257
1336
  elem.content = contents
1258
1337
  end
1259
1338
  xmldoc.find_first(xpath) << elem
1260
- elem
1339
+ when :nokogiri
1340
+ elem = Nokogiri::XML::Node.new(name, xmldoc)
1341
+ if contents
1342
+ elem.content = contents
1343
+ end
1344
+ xmldoc.at_xpath(xpath) << elem
1261
1345
  when :rexml
1262
1346
  elem = REXML::Element.new(name)
1263
1347
  if contents
1264
1348
  elem.text = contents
1265
1349
  end
1266
1350
  xmldoc.elements[xpath].add_element(elem)
1267
- elem
1268
1351
  else
1269
- raise "Unknown @xmllib #{@xmllib}"
1352
+ raise "Unknown XML library #{Etch.xmllib}"
1270
1353
  end
1271
1354
  end
1272
1355
 
1273
1356
  def self.xmlcopyelem(elem, destelem)
1274
- case @@xmllib
1357
+ case Etch.xmllib
1275
1358
  when :libxml
1276
1359
  destelem << elem.copy(true)
1360
+ when :nokogiri
1361
+ destelem << elem.dup
1277
1362
  when :rexml
1278
- destelem.add_element(elem.dup)
1363
+ destelem.add_element(elem.clone)
1279
1364
  else
1280
- raise "Unknown @xmllib #{@xmllib}"
1365
+ raise "Unknown XML library #{Etch.xmllib}"
1281
1366
  end
1282
1367
  end
1283
1368
 
1284
1369
  def self.xmlremove(xmldoc, element)
1285
- case @@xmllib
1370
+ case Etch.xmllib
1286
1371
  when :libxml
1287
1372
  element.remove!
1373
+ when :nokogiri
1374
+ element.remove
1288
1375
  when :rexml
1289
1376
  if xmldoc.node_type == :document
1290
1377
  xmldoc.root.elements.delete(element)
@@ -1292,40 +1379,64 @@ class Etch
1292
1379
  xmldoc.elements.delete(element)
1293
1380
  end
1294
1381
  else
1295
- raise "Unknown @xmllib #{@xmllib}"
1382
+ raise "Unknown XML library #{Etch.xmllib}"
1296
1383
  end
1297
1384
  end
1298
1385
 
1299
1386
  def self.xmlremovepath(xmldoc, xpath)
1300
- case @@xmllib
1387
+ case Etch.xmllib
1301
1388
  when :libxml
1302
1389
  xmldoc.find(xpath).each { |elem| elem.remove! }
1390
+ when :nokogiri
1391
+ xmldoc.xpath(xpath).each { |elem| elem.remove }
1303
1392
  when :rexml
1304
- xmldoc.delete_element(xpath)
1393
+ elem = nil
1394
+ # delete_element only removes the first match, so call it in a loop
1395
+ # until it returns nil to indicate no matching element remain
1396
+ begin
1397
+ elem = xmldoc.delete_element(xpath)
1398
+ end while elem != nil
1305
1399
  else
1306
- raise "Unknown @xmllib #{@xmllib}"
1400
+ raise "Unknown XML library #{Etch.xmllib}"
1307
1401
  end
1308
1402
  end
1309
1403
 
1310
1404
  def self.xmlattradd(elem, attrname, attrvalue)
1311
- case @@xmllib
1405
+ case Etch.xmllib
1312
1406
  when :libxml
1313
1407
  elem.attributes[attrname] = attrvalue
1408
+ when :nokogiri
1409
+ elem[attrname] = attrvalue
1314
1410
  when :rexml
1315
1411
  elem.add_attribute(attrname, attrvalue)
1316
1412
  else
1317
- raise "Unknown @xmllib #{@xmllib}"
1413
+ raise "Unknown XML library #{Etch.xmllib}"
1414
+ end
1415
+ end
1416
+
1417
+ def self.xmlattrvalue(elem, attrname)
1418
+ case Etch.xmllib
1419
+ when :libxml
1420
+ elem.attributes[attrname]
1421
+ when :nokogiri
1422
+ elem[attrname]
1423
+ when :rexml
1424
+ elem.attributes[attrname]
1425
+ else
1426
+ raise "Unknown XML library #{Etch.xmllib}"
1318
1427
  end
1319
1428
  end
1320
1429
 
1321
1430
  def self.xmlattrremove(elem, attribute)
1322
- case @@xmllib
1431
+ case Etch.xmllib
1323
1432
  when :libxml
1324
1433
  attribute.remove!
1434
+ when :nokogiri
1435
+ attribute.remove
1325
1436
  when :rexml
1326
1437
  elem.attributes.delete(attribute)
1327
1438
  else
1328
- raise "Unknown @xmllib #{@xmllib}"
1439
+ raise "Unknown XML library #{Etch.xmllib}"
1329
1440
  end
1330
1441
  end
1331
1442
 
@@ -1384,7 +1495,7 @@ class EtchExternalSource
1384
1495
  @dlogger.debug "Processing script #{script} for file #{@file}"
1385
1496
  @contents = ''
1386
1497
  begin
1387
- eval(IO.read(script))
1498
+ run_script_stage2(script)
1388
1499
  rescue Exception => e
1389
1500
  if e.kind_of?(SystemExit)
1390
1501
  # The user might call exit within a script. We want the scripts
@@ -1397,5 +1508,13 @@ class EtchExternalSource
1397
1508
  end
1398
1509
  @contents
1399
1510
  end
1511
+ # The user might call return within a script. We want the scripts to act as
1512
+ # much like a real script as possible. Wrapping the eval in an extra method
1513
+ # allows us to handle a return within the script seamlessly. If the user
1514
+ # calls return it triggers a return from this method. Otherwise this method
1515
+ # returns naturally. Either works for us.
1516
+ def run_script_stage2(script)
1517
+ eval(IO.read(script))
1518
+ end
1400
1519
  end
1401
1520
 
data/lib/etchclient.rb CHANGED
@@ -35,7 +35,7 @@ require 'logger'
35
35
  require 'etch'
36
36
 
37
37
  class Etch::Client
38
- VERSION = '3.17.0'
38
+ VERSION = '3.19.0'
39
39
 
40
40
  CONFIRM_PROCEED = 1
41
41
  CONFIRM_SKIP = 2
@@ -43,6 +43,7 @@ class Etch::Client
43
43
  PRIVATE_KEY_PATHS = ["/etc/ssh/ssh_host_rsa_key", "/etc/ssh_host_rsa_key"]
44
44
  DEFAULT_CONFIGDIR = '/etc'
45
45
  DEFAULT_VARBASE = '/var/etch'
46
+ DEFAULT_DETAILED_RESULTS = ['SERVER']
46
47
 
47
48
  # We need these in relation to the output capturing
48
49
  ORIG_STDOUT = STDOUT.dup
@@ -56,12 +57,15 @@ class Etch::Client
56
57
  @local = options[:local] ? File.expand_path(options[:local]) : nil
57
58
  @debug = options[:debug]
58
59
  @dryrun = options[:dryrun]
60
+ @listfiles = options[:listfiles]
59
61
  @interactive = options[:interactive]
60
62
  @filenameonly = options[:filenameonly]
61
63
  @fullfile = options[:fullfile]
62
64
  @key = options[:key] ? options[:key] : get_private_key_path
63
65
  @disableforce = options[:disableforce]
64
66
  @lockforce = options[:lockforce]
67
+
68
+ @last_response = ""
65
69
 
66
70
  @configdir = DEFAULT_CONFIGDIR
67
71
  @varbase = DEFAULT_VARBASE
@@ -75,6 +79,7 @@ class Etch::Client
75
79
  end
76
80
 
77
81
  @configfile = File.join(@configdir, 'etch.conf')
82
+ @detailed_results = []
78
83
 
79
84
  if File.exist?(@configfile)
80
85
  IO.foreach(@configfile) do |line|
@@ -119,6 +124,9 @@ class Etch::Client
119
124
  end
120
125
  elsif key == 'path'
121
126
  ENV['PATH'] = value
127
+ elsif key == 'detailed_results'
128
+ warn "Adding detailed results destination '#{value}'" if @debug
129
+ @detailed_results << value
122
130
  end
123
131
  end
124
132
  end
@@ -130,6 +138,10 @@ class Etch::Client
130
138
  warn "No readable private key found, messages to server will not be signed and may be rejected depending on server configuration"
131
139
  end
132
140
 
141
+ if @detailed_results.empty?
142
+ @detailed_results = DEFAULT_DETAILED_RESULTS
143
+ end
144
+
133
145
  @origbase = File.join(@varbase, 'orig')
134
146
  @historybase = File.join(@varbase, 'history')
135
147
  @lockbase = File.join(@varbase, 'locks')
@@ -197,6 +209,9 @@ class Etch::Client
197
209
  status = 0
198
210
  message = ''
199
211
 
212
+ # A variable to collect filenames if operating in @listfiles mode
213
+ files_to_list = {}
214
+
200
215
  # Prep http instance
201
216
  http = nil
202
217
  if !@local
@@ -289,6 +304,11 @@ class Etch::Client
289
304
  unlock_all_files
290
305
  end
291
306
 
307
+ # It usually takes a few back and forth exchanges with the server to
308
+ # exchange all needed data and get a complete set of configuration.
309
+ # The number of iterations is capped at 10 to prevent any unplanned
310
+ # infinite loops. The limit of 10 was chosen somewhat arbitrarily but
311
+ # seems fine in practice.
292
312
  10.times do
293
313
  #
294
314
  # Send request to server
@@ -389,9 +409,13 @@ class Etch::Client
389
409
  # needed to create the original files.
390
410
  responsedata[:configs].each_key do |file|
391
411
  puts "Processing config for #{file}" if (@debug)
392
- continue_processing = process_file(file, responsedata)
393
- if !continue_processing
394
- throw :stop_processing
412
+ if !@listfiles
413
+ continue_processing = process_file(file, responsedata)
414
+ if !continue_processing
415
+ throw :stop_processing
416
+ end
417
+ else
418
+ files_to_list[file] = true
395
419
  end
396
420
  end
397
421
  responsedata[:need_sums].each_key do |need_sum|
@@ -471,6 +495,11 @@ class Etch::Client
471
495
  end # begin/rescue
472
496
  end # catch
473
497
 
498
+ if @listfiles
499
+ puts "Files under management:"
500
+ files_to_list.keys.sort.each {|file| puts file}
501
+ end
502
+
474
503
  # Send results to server
475
504
  if !@dryrun && !@local
476
505
  rails_results = []
@@ -480,13 +509,15 @@ class Etch::Client
480
509
  rails_results << "fqdn=#{CGI.escape(@facts['fqdn'])}"
481
510
  rails_results << "status=#{CGI.escape(status.to_s)}"
482
511
  rails_results << "message=#{CGI.escape(message)}"
483
- @results.each do |result|
484
- # Strangely enough this works. Even though the key is not unique to
485
- # each result the Rails parameter parsing code keeps track of keys it
486
- # has seen, and if it sees a duplicate it starts a new hash.
487
- rails_results << "results[][file]=#{CGI.escape(result['file'])}"
488
- rails_results << "results[][success]=#{CGI.escape(result['success'].to_s)}"
489
- rails_results << "results[][message]=#{CGI.escape(result['message'])}"
512
+ if @detailed_results.include?('SERVER')
513
+ @results.each do |result|
514
+ # Strangely enough this works. Even though the key is not unique to
515
+ # each result the Rails parameter parsing code keeps track of keys it
516
+ # has seen, and if it sees a duplicate it starts a new hash.
517
+ rails_results << "results[][file]=#{CGI.escape(result['file'])}"
518
+ rails_results << "results[][success]=#{CGI.escape(result['success'].to_s)}"
519
+ rails_results << "results[][message]=#{CGI.escape(result['message'])}"
520
+ end
490
521
  end
491
522
  puts "Sending results to server #{@resultsuri}" if (@debug)
492
523
  resultspost = Net::HTTP::Post.new(@resultsuri.path)
@@ -507,6 +538,29 @@ class Etch::Client
507
538
  end
508
539
  end
509
540
 
541
+ if !@dryrun
542
+ @detailed_results.each do |detail_dest|
543
+ # If any of the destinations look like a file (start with a /) then we
544
+ # log to that file
545
+ if detail_dest =~ %r{^/}
546
+ FileUtils.mkpath(File.dirname(detail_dest))
547
+ File.open(detail_dest, 'a') do |file|
548
+ # Add a header for the overall status of the run
549
+ file.puts "Etch run at #{Time.now}"
550
+ file.puts "Status: #{status}"
551
+ if !message.empty?
552
+ file.puts "Message:\n#{message}\n"
553
+ end
554
+ # Then the detailed results
555
+ @results.each do |result|
556
+ file.puts "File #{result['file']}, result #{result['success']}:\n"
557
+ file.puts result['message']
558
+ end
559
+ end
560
+ end
561
+ end
562
+ end
563
+
510
564
  status
511
565
  end
512
566
 
@@ -1927,8 +1981,9 @@ class Etch::Client
1927
1981
  requests
1928
1982
  end
1929
1983
 
1930
- # Haven't found a Ruby method for creating temporary directories,
1931
- # so create a temporary file and replace it with a directory.
1984
+ # Ruby 1.8.7 and later have Dir.mktmpdir, but we support ruby 1.8.5 for
1985
+ # RHEL/CentOS 5. So this is a basic substitute.
1986
+ # FIXME: consider "backport" for Dir.mktmpdir
1932
1987
  def tempdir(file)
1933
1988
  filebase = File.basename(file)
1934
1989
  filedir = File.dirname(file)
@@ -2240,13 +2295,22 @@ class Etch::Client
2240
2295
 
2241
2296
  def get_user_confirmation
2242
2297
  while true
2243
- print "Proceed/Skip/Quit? [p|s|q] "
2298
+ print "Proceed/Skip/Quit? "
2299
+ case @last_response
2300
+ when /p|P/ then print "[P|s|q] "
2301
+ when /s|S/ then print "[p|S|q] "
2302
+ when /q|Q/ then print "[p|s|Q] "
2303
+ else print "[p|s|q] "
2304
+ end
2244
2305
  response = $stdin.gets.chomp
2245
- if response == 'p'
2306
+ if response =~ /p/i || @last_response =~ /p/i
2307
+ @last_response = response if !response.strip.empty?
2246
2308
  return CONFIRM_PROCEED
2247
- elsif response == 's'
2309
+ elsif response =~ /s/i || @last_response =~ /s/i
2310
+ @last_response = response if !response.strip.empty?
2248
2311
  return CONFIRM_SKIP
2249
- elsif response == 'q'
2312
+ elsif response =~ /q/i || @last_response =~ /q/i
2313
+ @last_response = response if !response.strip.empty?
2250
2314
  return CONFIRM_QUIT
2251
2315
  end
2252
2316
  end
metadata CHANGED
@@ -1,13 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: etch
3
3
  version: !ruby/object:Gem::Version
4
- hash: 67
5
- prerelease: false
6
- segments:
7
- - 3
8
- - 17
9
- - 0
10
- version: 3.17.0
4
+ version: 3.19.0
11
5
  platform: ruby
12
6
  authors:
13
7
  - Jason Heiss
@@ -15,23 +9,19 @@ autorequire:
15
9
  bindir: bin
16
10
  cert_chain: []
17
11
 
18
- date: 2010-12-22 00:00:00 -08:00
12
+ date: 2011-04-12 00:00:00 -07:00
19
13
  default_executable:
20
14
  dependencies:
21
15
  - !ruby/object:Gem::Dependency
22
16
  name: facter
23
- prerelease: false
24
- requirement: &id001 !ruby/object:Gem::Requirement
25
- none: false
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
26
20
  requirements:
27
21
  - - ">="
28
22
  - !ruby/object:Gem::Version
29
- hash: 3
30
- segments:
31
- - 0
32
23
  version: "0"
33
- type: :runtime
34
- version_requirements: *id001
24
+ version:
35
25
  description:
36
26
  email: etch-users@lists.sourceforge.net
37
27
  executables:
@@ -60,28 +50,21 @@ rdoc_options: []
60
50
  require_paths:
61
51
  - lib
62
52
  required_ruby_version: !ruby/object:Gem::Requirement
63
- none: false
64
53
  requirements:
65
54
  - - ">="
66
55
  - !ruby/object:Gem::Version
67
- hash: 31
68
- segments:
69
- - 1
70
- - 8
71
56
  version: "1.8"
57
+ version:
72
58
  required_rubygems_version: !ruby/object:Gem::Requirement
73
- none: false
74
59
  requirements:
75
60
  - - ">="
76
61
  - !ruby/object:Gem::Version
77
- hash: 3
78
- segments:
79
- - 0
80
62
  version: "0"
63
+ version:
81
64
  requirements: []
82
65
 
83
66
  rubyforge_project: etchsyscm
84
- rubygems_version: 1.3.7
67
+ rubygems_version: 1.3.5
85
68
  signing_key:
86
69
  specification_version: 3
87
70
  summary: Etch system configuration management client