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.
- data/Rakefile +1 -1
- data/bin/etch +7 -0
- data/bin/etch_to_trunk +9 -1
- data/lib/etch.rb +177 -58
- data/lib/etchclient.rb +81 -17
- 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.
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
274
|
-
|
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
|
-
|
850
|
-
|
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
|
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
|
1109
|
+
raise "Unknown XML library #{Etch.xmllib}"
|
1087
1110
|
end
|
1088
1111
|
end
|
1089
1112
|
|
1090
1113
|
def self.xmlroot(doc)
|
1091
|
-
case
|
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
|
1122
|
+
raise "Unknown XML library #{Etch.xmllib}"
|
1098
1123
|
end
|
1099
1124
|
end
|
1100
1125
|
|
1101
1126
|
def self.xmlsetroot(doc, root)
|
1102
|
-
case
|
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
|
1135
|
+
raise "Unknown XML library #{Etch.xmllib}"
|
1109
1136
|
end
|
1110
1137
|
end
|
1111
1138
|
|
1112
1139
|
def self.xmlload(file)
|
1113
|
-
case
|
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
|
1153
|
+
raise "Unknown XML library #{Etch.xmllib}"
|
1120
1154
|
end
|
1121
1155
|
end
|
1122
1156
|
|
1123
1157
|
def self.xmlloaddtd(dtdfile)
|
1124
|
-
case
|
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
|
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
|
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
|
1200
|
+
raise "Unknown XML library #{Etch.xmllib}"
|
1142
1201
|
end
|
1143
1202
|
end
|
1144
1203
|
|
1145
|
-
def self.xmlnewelem(name)
|
1146
|
-
case
|
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
|
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
|
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
|
1226
|
+
raise "Unknown XML library #{Etch.xmllib}"
|
1164
1227
|
end
|
1165
1228
|
end
|
1166
1229
|
|
1167
1230
|
def self.xmleachall(xmldoc, &block)
|
1168
|
-
case
|
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
|
1251
|
+
raise "Unknown XML library #{Etch.xmllib}"
|
1183
1252
|
end
|
1184
1253
|
end
|
1185
1254
|
|
1186
1255
|
def self.xmleachattrall(elem, &block)
|
1187
|
-
case
|
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
|
1264
|
+
raise "Unknown XML library #{Etch.xmllib}"
|
1194
1265
|
end
|
1195
1266
|
end
|
1196
1267
|
|
1197
1268
|
def self.xmlarray(xmldoc, xpath)
|
1198
|
-
case
|
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
|
1282
|
+
raise "Unknown XML library #{Etch.xmllib}"
|
1210
1283
|
end
|
1211
1284
|
end
|
1212
1285
|
|
1213
1286
|
def self.xmlfindfirst(xmldoc, xpath)
|
1214
|
-
case
|
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
|
1295
|
+
raise "Unknown XML library #{Etch.xmllib}"
|
1221
1296
|
end
|
1222
1297
|
end
|
1223
1298
|
|
1224
1299
|
def self.xmltext(elem)
|
1225
|
-
case
|
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
|
1314
|
+
raise "Unknown XML library #{Etch.xmllib}"
|
1238
1315
|
end
|
1239
1316
|
end
|
1240
1317
|
|
1241
1318
|
def self.xmlsettext(elem, text)
|
1242
|
-
case
|
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
|
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
|
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
|
-
|
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
|
1352
|
+
raise "Unknown XML library #{Etch.xmllib}"
|
1270
1353
|
end
|
1271
1354
|
end
|
1272
1355
|
|
1273
1356
|
def self.xmlcopyelem(elem, destelem)
|
1274
|
-
case
|
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.
|
1363
|
+
destelem.add_element(elem.clone)
|
1279
1364
|
else
|
1280
|
-
raise "Unknown
|
1365
|
+
raise "Unknown XML library #{Etch.xmllib}"
|
1281
1366
|
end
|
1282
1367
|
end
|
1283
1368
|
|
1284
1369
|
def self.xmlremove(xmldoc, element)
|
1285
|
-
case
|
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
|
1382
|
+
raise "Unknown XML library #{Etch.xmllib}"
|
1296
1383
|
end
|
1297
1384
|
end
|
1298
1385
|
|
1299
1386
|
def self.xmlremovepath(xmldoc, xpath)
|
1300
|
-
case
|
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
|
-
|
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
|
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
|
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
|
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
|
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
|
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
|
-
|
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.
|
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
|
-
|
393
|
-
|
394
|
-
|
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
|
-
@
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
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
|
-
#
|
1931
|
-
#
|
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?
|
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
|
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
|
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
|
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
|
-
|
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:
|
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
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
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.
|
67
|
+
rubygems_version: 1.3.5
|
85
68
|
signing_key:
|
86
69
|
specification_version: 3
|
87
70
|
summary: Etch system configuration management client
|