etch 3.17.0 → 3.19.0

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