cfa 0.4.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/cfa/augeas_parser.rb +114 -146
- data/lib/cfa/augeas_parser/keys_cache.rb +41 -0
- data/lib/cfa/augeas_parser/reader.rb +67 -0
- data/lib/cfa/augeas_parser/writer.rb +324 -0
- data/lib/cfa/base_model.rb +13 -12
- data/lib/cfa/placer.rb +25 -14
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a4abe1cefacc4777f6016b432b3a306f16b1b7ac
|
4
|
+
data.tar.gz: 359c1f958762e205c5313e0027dda5a09a10bc5c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c8bdb5a5aa8391dfb8c46448b760868e257b71e195daa38cc7d272f1729e52d7c20be9acd6fa2632b63ec4bfb736c7bba41a3293b6e7d3e12d319d0a786fd885
|
7
|
+
data.tar.gz: 69296e9dbd626125d6e0612f580cbbd12266e004794231c9b61c980bd66114ee90f84cafd22393fc6b1000ce4b42d92f6ff724b2ce34c6ff96d77feba2c604cb
|
data/lib/cfa/augeas_parser.rb
CHANGED
@@ -2,6 +2,7 @@ require "augeas"
|
|
2
2
|
require "forwardable"
|
3
3
|
require "cfa/placer"
|
4
4
|
|
5
|
+
# CFA: Configuration Files API
|
5
6
|
module CFA
|
6
7
|
# A building block for {AugeasTree}.
|
7
8
|
#
|
@@ -16,6 +17,17 @@ module CFA
|
|
16
17
|
# A `:value` is either a String, or an {AugeasTree},
|
17
18
|
# or an {AugeasTreeValue} (which combines both).
|
18
19
|
#
|
20
|
+
# An `:operation` is an internal variable holding modification of Augeas
|
21
|
+
# structure. It is used for minimizing modifications of source files. Its
|
22
|
+
# possible values are
|
23
|
+
# - `:keep` when the value is untouched
|
24
|
+
# - `:modify` when the `:value` changed but the `:key` is the same
|
25
|
+
# - `:remove` when it is going to be removed, and
|
26
|
+
# - `:add` when a new element is added.
|
27
|
+
#
|
28
|
+
# An `:orig_key` is an internal variable used to hold the original key
|
29
|
+
# including its index.
|
30
|
+
#
|
19
31
|
# @return [Hash{Symbol => String, AugeasTree}]
|
20
32
|
#
|
21
33
|
# @todo Unify naming: entry, element
|
@@ -38,19 +50,16 @@ module CFA
|
|
38
50
|
element = placer.new_element(@tree)
|
39
51
|
element[:key] = augeas_name
|
40
52
|
element[:value] = value
|
53
|
+
element[:operation] = :add
|
41
54
|
# FIXME: load_collection missing here
|
42
55
|
end
|
43
56
|
|
44
57
|
def delete(value)
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
else
|
51
|
-
value == entry[:value]
|
52
|
-
end
|
53
|
-
end
|
58
|
+
to_delete, to_mark = to_remove(value)
|
59
|
+
.partition { |e| e[:operation] == :add }
|
60
|
+
@tree.all_data.delete_if { |e| to_delete.include?(e) }
|
61
|
+
|
62
|
+
to_mark.each { |e| e[:operation] = :remove }
|
54
63
|
|
55
64
|
load_collection
|
56
65
|
end
|
@@ -58,26 +67,45 @@ module CFA
|
|
58
67
|
private
|
59
68
|
|
60
69
|
def load_collection
|
61
|
-
entries = @tree.data.select
|
70
|
+
entries = @tree.data.select do |entry|
|
71
|
+
entry[:key] == augeas_name && entry[:operation] != :remove
|
72
|
+
end
|
62
73
|
@collection = entries.map { |e| e[:value] }.freeze
|
63
74
|
end
|
64
75
|
|
65
76
|
def augeas_name
|
66
77
|
@name + "[]"
|
67
78
|
end
|
79
|
+
|
80
|
+
def to_remove(value)
|
81
|
+
key = augeas_name
|
82
|
+
|
83
|
+
@tree.data.select do |entry|
|
84
|
+
entry[:key] == key && value_match?(entry[:value], value)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def value_match?(value, match)
|
89
|
+
if match.is_a?(Regexp)
|
90
|
+
value =~ match
|
91
|
+
else
|
92
|
+
value == match
|
93
|
+
end
|
94
|
+
end
|
68
95
|
end
|
69
96
|
|
70
97
|
# Represents a node that contains both a value and a subtree below it.
|
71
98
|
# For easier traversal it forwards `#[]` to the subtree.
|
72
99
|
class AugeasTreeValue
|
73
100
|
# @return [String] the value in the node
|
74
|
-
|
101
|
+
attr_reader :value
|
75
102
|
# @return [AugeasTree] the subtree below the node
|
76
103
|
attr_accessor :tree
|
77
104
|
|
78
105
|
def initialize(tree, value)
|
79
106
|
@tree = tree
|
80
107
|
@value = value
|
108
|
+
@modified = false
|
81
109
|
end
|
82
110
|
|
83
111
|
# (see AugeasTree#[])
|
@@ -85,12 +113,22 @@ module CFA
|
|
85
113
|
tree[key]
|
86
114
|
end
|
87
115
|
|
116
|
+
def value=(value)
|
117
|
+
@value = value
|
118
|
+
@modified = true
|
119
|
+
end
|
120
|
+
|
88
121
|
def ==(other)
|
89
122
|
[:class, :value, :tree].all? do |a|
|
90
123
|
public_send(a) == other.public_send(a)
|
91
124
|
end
|
92
125
|
end
|
93
126
|
|
127
|
+
# @return true if the value has been modified
|
128
|
+
def modified?
|
129
|
+
@modified
|
130
|
+
end
|
131
|
+
|
94
132
|
# For objects of class Object, eql? is synonymous with ==:
|
95
133
|
# http://ruby-doc.org/core-2.3.3/Object.html#method-i-eql-3F
|
96
134
|
alias_method :eql?, :==
|
@@ -100,13 +138,21 @@ module CFA
|
|
100
138
|
class AugeasTree
|
101
139
|
# Low level access to Augeas structure
|
102
140
|
#
|
103
|
-
# An ordered mapping, represented by an Array of
|
104
|
-
#
|
141
|
+
# An ordered mapping, represented by an Array of AugeasElement, but without
|
142
|
+
# any removed elements.
|
105
143
|
#
|
106
144
|
# @see AugeasElement
|
107
145
|
#
|
108
|
-
# @return [Array<Hash{Symbol =>
|
109
|
-
|
146
|
+
# @return [Array<Hash{Symbol => Object}>] a frozen array as it is
|
147
|
+
# just a copy of the real data
|
148
|
+
def data
|
149
|
+
@data.select { |e| e[:operation] != :remove }.freeze
|
150
|
+
end
|
151
|
+
|
152
|
+
# low level access to all AugeasElement including ones marked for removal
|
153
|
+
def all_data
|
154
|
+
@data
|
155
|
+
end
|
110
156
|
|
111
157
|
def initialize
|
112
158
|
@data = []
|
@@ -117,13 +163,17 @@ module CFA
|
|
117
163
|
AugeasCollection.new(self, key)
|
118
164
|
end
|
119
165
|
|
120
|
-
# @param [String, Matcher]
|
166
|
+
# @param [String, Matcher] matcher
|
121
167
|
def delete(matcher)
|
122
168
|
return if matcher.nil?
|
123
169
|
unless matcher.is_a?(CFA::Matcher)
|
124
170
|
matcher = CFA::Matcher.new(key: matcher)
|
125
171
|
end
|
126
|
-
@data.
|
172
|
+
to_remove = @data.select(&matcher)
|
173
|
+
|
174
|
+
to_delete, to_mark = to_remove.partition { |e| e[:operation] == :add }
|
175
|
+
@data -= to_delete
|
176
|
+
to_mark.each { |e| e[:operation] = :remove }
|
127
177
|
end
|
128
178
|
|
129
179
|
# Adds the given *value* for *key* in the tree.
|
@@ -139,6 +189,7 @@ module CFA
|
|
139
189
|
element = placer.new_element(self)
|
140
190
|
element[:key] = key
|
141
191
|
element[:value] = value
|
192
|
+
element[:operation] = :add
|
142
193
|
end
|
143
194
|
|
144
195
|
# Finds given *key* in tree.
|
@@ -146,7 +197,7 @@ module CFA
|
|
146
197
|
# @return [String,AugeasTree,AugeasTreeValue,nil] the first value for *key*,
|
147
198
|
# or `nil` if not found
|
148
199
|
def [](key)
|
149
|
-
entry = @data.find { |d| d[:key] == key }
|
200
|
+
entry = @data.find { |d| d[:key] == key && d[:operation] != :remove }
|
150
201
|
return entry[:value] if entry
|
151
202
|
|
152
203
|
nil
|
@@ -154,18 +205,13 @@ module CFA
|
|
154
205
|
|
155
206
|
# Replace the first value for *key* with *value*.
|
156
207
|
# Append a new element if *key* did not exist.
|
208
|
+
# If *key* was previously removed, then put it back to its old position.
|
157
209
|
# @param key [String]
|
158
210
|
# @param value [String, AugeasTree, AugeasTreeValue]
|
159
211
|
def []=(key, value)
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
else
|
164
|
-
@data << {
|
165
|
-
key: key,
|
166
|
-
value: value
|
167
|
-
}
|
168
|
-
end
|
212
|
+
new_entry = entry_to_modify(key, value)
|
213
|
+
new_entry[:key] = key
|
214
|
+
new_entry[:value] = value
|
169
215
|
end
|
170
216
|
|
171
217
|
# @param matcher [Matcher]
|
@@ -174,41 +220,15 @@ module CFA
|
|
174
220
|
@data.select(&matcher)
|
175
221
|
end
|
176
222
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
# @param keys_cache [AugeasKeysCache]
|
184
|
-
# @return [void]
|
185
|
-
def load_from_augeas(aug, prefix, keys_cache)
|
186
|
-
@data = keys_cache.keys_for_prefix(prefix).map do |key|
|
187
|
-
aug_key = prefix + "/" + key
|
188
|
-
{
|
189
|
-
key: load_key(prefix, aug_key),
|
190
|
-
value: load_value(aug, aug_key, keys_cache)
|
191
|
-
}
|
192
|
-
end
|
193
|
-
end
|
194
|
-
|
195
|
-
# @note for internal usage only
|
196
|
-
# @api private
|
197
|
-
#
|
198
|
-
# Saves {#data} to *prefix* in *aug*.
|
199
|
-
# @param aug [::Augeas]
|
200
|
-
# @param prefix [String] Augeas path prefix
|
201
|
-
# @return [void]
|
202
|
-
def save_to_augeas(aug, prefix)
|
203
|
-
arrays = {}
|
204
|
-
|
205
|
-
@data.each do |entry|
|
206
|
-
save_entry(entry[:key], entry[:value], arrays, aug, prefix)
|
223
|
+
def ==(other)
|
224
|
+
return false if self.class != other.class
|
225
|
+
other_data = other.data # do not compute again
|
226
|
+
data.each_with_index do |entry, index|
|
227
|
+
return false if entry[:key] != other_data[index][:key]
|
228
|
+
return false if entry[:value] != other_data[index][:value]
|
207
229
|
end
|
208
|
-
end
|
209
230
|
|
210
|
-
|
211
|
-
[:class, :data].all? { |a| public_send(a) == other.public_send(a) }
|
231
|
+
true
|
212
232
|
end
|
213
233
|
|
214
234
|
# For objects of class Object, eql? is synonymous with ==:
|
@@ -217,54 +237,44 @@ module CFA
|
|
217
237
|
|
218
238
|
private
|
219
239
|
|
220
|
-
def
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
240
|
+
def replace_entry(old_entry)
|
241
|
+
index = @data.index(old_entry)
|
242
|
+
new_entry = { operation: :add }
|
243
|
+
# insert the replacement to the same location
|
244
|
+
@data.insert(index, new_entry)
|
245
|
+
# the entry is not yet in the tree
|
246
|
+
if old_entry[:operation] == :add
|
247
|
+
@data.delete_if { |d| d[:key] == key }
|
227
248
|
else
|
228
|
-
|
229
|
-
end
|
230
|
-
end
|
231
|
-
|
232
|
-
def obtain_aug_key(prefix, key, arrays)
|
233
|
-
if key.end_with?("[]")
|
234
|
-
array_key = key[0..-3] # remove trailing []
|
235
|
-
arrays[array_key] ||= 0
|
236
|
-
arrays[array_key] += 1
|
237
|
-
key = array_key + "[#{arrays[array_key]}]"
|
249
|
+
old_entry[:operation] = :remove
|
238
250
|
end
|
239
251
|
|
240
|
-
|
252
|
+
new_entry
|
241
253
|
end
|
242
254
|
|
243
|
-
def
|
244
|
-
|
245
|
-
|
246
|
-
|
255
|
+
def mark_new_entry(new_entry, old_entry)
|
256
|
+
# if an entry already exists then just modify it,
|
257
|
+
# but only if we previously did not add it
|
258
|
+
new_entry[:operation] = if old_entry && old_entry[:operation] != :add
|
259
|
+
:modify
|
260
|
+
else
|
261
|
+
:add
|
262
|
+
end
|
247
263
|
end
|
248
264
|
|
249
|
-
def
|
250
|
-
|
251
|
-
#
|
252
|
-
key
|
253
|
-
|
254
|
-
|
265
|
+
def entry_to_modify(key, value)
|
266
|
+
entry = @data.find { |d| d[:key] == key }
|
267
|
+
# we are switching from tree to value or treevalue to value only
|
268
|
+
# like change from key=value to key=value#comment
|
269
|
+
if entry && entry[:value].class != value.class
|
270
|
+
entry = replace_entry(entry)
|
271
|
+
end
|
272
|
+
new_entry = entry || {}
|
273
|
+
mark_new_entry(new_entry, entry)
|
255
274
|
|
256
|
-
|
257
|
-
subkeys = keys_cache.keys_for_prefix(aug_key)
|
275
|
+
@data << new_entry unless entry
|
258
276
|
|
259
|
-
|
260
|
-
value = aug.get(aug_key)
|
261
|
-
if nested
|
262
|
-
subtree = AugeasTree.new
|
263
|
-
subtree.load_from_augeas(aug, aug_key, keys_cache)
|
264
|
-
value ? AugeasTreeValue.new(subtree, value) : subtree
|
265
|
-
else
|
266
|
-
value
|
267
|
-
end
|
277
|
+
new_entry
|
268
278
|
end
|
269
279
|
end
|
270
280
|
|
@@ -286,6 +296,7 @@ module CFA
|
|
286
296
|
# @param raw_string [String] a string to be parsed
|
287
297
|
# @return [AugeasTree] the parsed data
|
288
298
|
def parse(raw_string)
|
299
|
+
require "cfa/augeas_parser/reader"
|
289
300
|
@old_content = raw_string
|
290
301
|
|
291
302
|
# open augeas without any autoloading and it should not touch disk and
|
@@ -295,24 +306,21 @@ module CFA
|
|
295
306
|
aug.set("/input", raw_string)
|
296
307
|
report_error(aug) unless aug.text_store(@lens, "/input", "/store")
|
297
308
|
|
298
|
-
|
299
|
-
|
300
|
-
tree = AugeasTree.new
|
301
|
-
tree.load_from_augeas(aug, "/store", keys_cache)
|
302
|
-
|
303
|
-
return tree
|
309
|
+
return AugeasReader.read(aug, "/store")
|
304
310
|
end
|
305
311
|
end
|
306
312
|
|
307
313
|
# @param data [AugeasTree] the data to be serialized
|
308
314
|
# @return [String] a string to be written
|
309
315
|
def serialize(data)
|
316
|
+
require "cfa/augeas_parser/writer"
|
310
317
|
# open augeas without any autoloading and it should not touch disk and
|
311
318
|
# load lenses as needed only
|
312
319
|
root = load_path = nil
|
313
320
|
Augeas.open(root, load_path, Augeas::NO_MODL_AUTOLOAD) do |aug|
|
314
321
|
aug.set("/input", @old_content || "")
|
315
|
-
|
322
|
+
aug.text_store(@lens, "/input", "/store") if @old_content
|
323
|
+
AugeasWriter.new(aug).write("/store", data)
|
316
324
|
|
317
325
|
res = aug.text_retrieve(@lens, "/input", "/store", "/output")
|
318
326
|
report_error(aug) unless res
|
@@ -342,44 +350,4 @@ module CFA
|
|
342
350
|
raise "Augeas parsing/serializing error: #{msg} at #{location}"
|
343
351
|
end
|
344
352
|
end
|
345
|
-
|
346
|
-
# Cache that holds all avaiable keys in augeas tree. It is used to
|
347
|
-
# prevent too many aug.match calls which are expensive.
|
348
|
-
class AugeasKeysCache
|
349
|
-
STORE_PREFIX = "/store".freeze
|
350
|
-
|
351
|
-
# initialize cache from passed augeas object
|
352
|
-
def initialize(aug)
|
353
|
-
fill_cache(aug)
|
354
|
-
end
|
355
|
-
|
356
|
-
# returns list of keys available on given prefix
|
357
|
-
def keys_for_prefix(prefix)
|
358
|
-
@cache[prefix] || []
|
359
|
-
end
|
360
|
-
|
361
|
-
private
|
362
|
-
|
363
|
-
def fill_cache(aug)
|
364
|
-
@cache = {}
|
365
|
-
search_path = "#{STORE_PREFIX}/*"
|
366
|
-
loop do
|
367
|
-
matches = aug.match(search_path)
|
368
|
-
break if matches.empty?
|
369
|
-
assign_matches(matches, @cache)
|
370
|
-
|
371
|
-
search_path += "/*"
|
372
|
-
end
|
373
|
-
end
|
374
|
-
|
375
|
-
def assign_matches(matches, cache)
|
376
|
-
matches.each do |match|
|
377
|
-
split_index = match.rindex("/")
|
378
|
-
prefix = match[0..(split_index - 1)]
|
379
|
-
key = match[(split_index + 1)..-1]
|
380
|
-
cache[prefix] ||= []
|
381
|
-
cache[prefix] << key
|
382
|
-
end
|
383
|
-
end
|
384
|
-
end
|
385
353
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module CFA
|
2
|
+
# A cache that holds all avaiable keys in an Augeas tree. It is used to
|
3
|
+
# prevent too many `aug.match` calls which are expensive.
|
4
|
+
class AugeasKeysCache
|
5
|
+
# initialize cache from passed Augeas object
|
6
|
+
# @param aug [::Augeas]
|
7
|
+
# @param prefix [String] Augeas path for which cache should be created
|
8
|
+
def initialize(aug, prefix)
|
9
|
+
fill_cache(aug, prefix)
|
10
|
+
end
|
11
|
+
|
12
|
+
# @return list of keys available on given prefix
|
13
|
+
def keys_for_prefix(prefix)
|
14
|
+
@cache[prefix] || []
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def fill_cache(aug, prefix)
|
20
|
+
@cache = {}
|
21
|
+
search_path = "#{prefix}/*"
|
22
|
+
loop do
|
23
|
+
matches = aug.match(search_path)
|
24
|
+
break if matches.empty?
|
25
|
+
assign_matches(matches, @cache)
|
26
|
+
|
27
|
+
search_path += "/*"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def assign_matches(matches, cache)
|
32
|
+
matches.each do |match|
|
33
|
+
split_index = match.rindex("/")
|
34
|
+
prefix = match[0..(split_index - 1)]
|
35
|
+
key = match[(split_index + 1)..-1]
|
36
|
+
cache[prefix] ||= []
|
37
|
+
cache[prefix] << key
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require "cfa/augeas_parser/keys_cache"
|
2
|
+
require "cfa/augeas_parser"
|
3
|
+
|
4
|
+
module CFA
|
5
|
+
# A class responsible for reading {AugeasTree} from Augeas
|
6
|
+
class AugeasReader
|
7
|
+
class << self
|
8
|
+
# Creates *tree* from *prefix* in *aug*.
|
9
|
+
# @param aug [::Augeas]
|
10
|
+
# @param prefix [String] Augeas path prefix
|
11
|
+
# @return [AugeasTree]
|
12
|
+
def read(aug, prefix)
|
13
|
+
keys_cache = AugeasKeysCache.new(aug, prefix)
|
14
|
+
|
15
|
+
tree = AugeasTree.new
|
16
|
+
load_tree(aug, prefix, tree, keys_cache)
|
17
|
+
|
18
|
+
tree
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# fills *tree* with data
|
24
|
+
def load_tree(aug, prefix, tree, keys_cache)
|
25
|
+
data = keys_cache.keys_for_prefix(prefix).map do |key|
|
26
|
+
aug_key = prefix + "/" + key
|
27
|
+
{
|
28
|
+
key: load_key(prefix, aug_key),
|
29
|
+
value: load_value(aug, aug_key, keys_cache),
|
30
|
+
orig_key: stripped_path(prefix, aug_key),
|
31
|
+
operation: :keep
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
tree.all_data.concat(data)
|
36
|
+
end
|
37
|
+
|
38
|
+
# loads a key in a format that AugeasTree expects
|
39
|
+
def load_key(prefix, aug_key)
|
40
|
+
# clean from key prefix and for collection remove number inside []
|
41
|
+
key = stripped_path(prefix, aug_key)
|
42
|
+
key.end_with?("]") ? key.sub(/\[\d+\]$/, "[]") : key
|
43
|
+
end
|
44
|
+
|
45
|
+
# path without prefix we are not interested in
|
46
|
+
def stripped_path(prefix, aug_key)
|
47
|
+
# +1 for size due to ending '/' not part of prefix
|
48
|
+
aug_key[(prefix.size + 1)..-1]
|
49
|
+
end
|
50
|
+
|
51
|
+
# loads value from auges. If value have tree under, it will also read it
|
52
|
+
def load_value(aug, aug_key, keys_cache)
|
53
|
+
subkeys = keys_cache.keys_for_prefix(aug_key)
|
54
|
+
|
55
|
+
nested = !subkeys.empty?
|
56
|
+
value = aug.get(aug_key)
|
57
|
+
if nested
|
58
|
+
subtree = AugeasTree.new
|
59
|
+
load_tree(aug, aug_key, subtree, keys_cache)
|
60
|
+
value ? AugeasTreeValue.new(subtree, value) : subtree
|
61
|
+
else
|
62
|
+
value
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,324 @@
|
|
1
|
+
module CFA
|
2
|
+
# The goal of this class is to write the data stored in {AugeasTree}
|
3
|
+
# back to Augeas.
|
4
|
+
#
|
5
|
+
# It tries to make only the needed changes, as internally Augeas keeps
|
6
|
+
# a flag whether data has been modified,
|
7
|
+
# and keeps the unmodified parts of the file untouched.
|
8
|
+
#
|
9
|
+
# @note internal only, unstable API
|
10
|
+
# @api private
|
11
|
+
class AugeasWriter
|
12
|
+
# @param aug result of Augeas.create
|
13
|
+
def initialize(aug)
|
14
|
+
@aug = aug
|
15
|
+
end
|
16
|
+
|
17
|
+
# Writes the data in *tree* to a given *prefix* in Augeas
|
18
|
+
# @param prefix [String] where to write *tree* in Augeas
|
19
|
+
# @param tree [CFA::AugeasTree] tree to write
|
20
|
+
def write(prefix, tree, top_level: true)
|
21
|
+
@lazy_operations = LazyOperations.new(aug) if top_level
|
22
|
+
tree.all_data.each do |entry|
|
23
|
+
located_entry = LocatedEntry.new(tree, entry, prefix)
|
24
|
+
process_operation(located_entry)
|
25
|
+
end
|
26
|
+
@lazy_operations.run if top_level
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# {AugeasElement} together with information about its location and a few
|
32
|
+
# helper methods to detect siblings.
|
33
|
+
#
|
34
|
+
# @example data for an already existing comment living under /main
|
35
|
+
# entry.orig_key # => "#comment[15]"
|
36
|
+
# entry.path # => "/main/#comment[15]"
|
37
|
+
# entry.key # => "#comment"
|
38
|
+
# entry.entry_tree # => AugeasTree.new
|
39
|
+
# entry.entry_value # => "old boring comment"
|
40
|
+
#
|
41
|
+
# @example data for a new comment under /main
|
42
|
+
# entry.orig_key # => nil
|
43
|
+
# entry.path # => nil
|
44
|
+
# entry.key # => "#comment"
|
45
|
+
# entry.entry_tree # => AugeasTree.new
|
46
|
+
# entry.entry_value # => "new boring comment"
|
47
|
+
#
|
48
|
+
# @example data for new tree placed at /main
|
49
|
+
# entry.orig_key # => "main"
|
50
|
+
# entry.path # => "/main"
|
51
|
+
# entry.key # => "main"
|
52
|
+
# entry.entry_tree # => entry[:value]
|
53
|
+
# entry.entry_value # => nil
|
54
|
+
#
|
55
|
+
class LocatedEntry
|
56
|
+
attr_reader :prefix
|
57
|
+
attr_reader :entry
|
58
|
+
attr_reader :tree
|
59
|
+
|
60
|
+
def initialize(tree, entry, prefix)
|
61
|
+
@tree = tree
|
62
|
+
@entry = entry
|
63
|
+
@prefix = prefix
|
64
|
+
detect_tree_value_modification
|
65
|
+
end
|
66
|
+
|
67
|
+
def orig_key
|
68
|
+
entry[:orig_key]
|
69
|
+
end
|
70
|
+
|
71
|
+
def path
|
72
|
+
return @path if @path
|
73
|
+
return nil unless orig_key
|
74
|
+
|
75
|
+
@path = @prefix + "/" + orig_key
|
76
|
+
end
|
77
|
+
|
78
|
+
def key
|
79
|
+
return @key if @key
|
80
|
+
|
81
|
+
@key = @entry[:key]
|
82
|
+
@key = @key[0..-3] if @key.end_with?("[]")
|
83
|
+
@key
|
84
|
+
end
|
85
|
+
|
86
|
+
# @return [LocatedEntry, nil]
|
87
|
+
# a preceding entry that already exists in the Augeas tree
|
88
|
+
# or nil if it does not exist.
|
89
|
+
def preceding_existing
|
90
|
+
preceding_entry = preceding_entries.reverse_each.find do |entry|
|
91
|
+
entry[:operation] != :add
|
92
|
+
end
|
93
|
+
|
94
|
+
return nil unless preceding_entry
|
95
|
+
|
96
|
+
LocatedEntry.new(tree, preceding_entry, prefix)
|
97
|
+
end
|
98
|
+
|
99
|
+
# @return [true, false] returns true if there is any following entry
|
100
|
+
# in the Augeas tree
|
101
|
+
def any_following?
|
102
|
+
following_entries.any? { |e| e[:operation] != :remove }
|
103
|
+
end
|
104
|
+
|
105
|
+
# @return [AugeasTree] the Augeas tree nested under this entry.
|
106
|
+
# If there is no such tree, it creates an empty one.
|
107
|
+
def entry_tree
|
108
|
+
value = entry[:value]
|
109
|
+
case value
|
110
|
+
when AugeasTree then value
|
111
|
+
when AugeasTreeValue then value.tree
|
112
|
+
else AugeasTree.new
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# @return [String, nil] the Augeas value of this entry. Can be nil.
|
117
|
+
# If the value is an {AugeasTree} then return nil.
|
118
|
+
def entry_value
|
119
|
+
value = entry[:value]
|
120
|
+
case value
|
121
|
+
when AugeasTree then nil
|
122
|
+
when AugeasTreeValue then value.value
|
123
|
+
else value
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
# For {AugeasTreeValue} we have a problem with detection of
|
130
|
+
# value modification as it is enclosed in a diferent object.
|
131
|
+
# So propagate it to this entry here.
|
132
|
+
def detect_tree_value_modification
|
133
|
+
return unless entry[:value].is_a?(AugeasTreeValue)
|
134
|
+
return if entry[:operation] != :keep
|
135
|
+
|
136
|
+
entry[:operation] = entry[:value].modified? ? :modify : :keep
|
137
|
+
end
|
138
|
+
|
139
|
+
# the entries preceding this entry
|
140
|
+
def preceding_entries
|
141
|
+
return [] if index.zero? # first entry
|
142
|
+
tree.all_data[0..(index - 1)]
|
143
|
+
end
|
144
|
+
|
145
|
+
# the entries following this entry
|
146
|
+
def following_entries
|
147
|
+
tree.all_data[(index + 1)..-1]
|
148
|
+
end
|
149
|
+
|
150
|
+
# the index of this entry in its tree
|
151
|
+
def index
|
152
|
+
@index ||= tree.all_data.index(entry)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Represents an operation that needs to be done after all modifications.
|
157
|
+
#
|
158
|
+
# The reason to have this class is that Augeas renumbers its arrays after
|
159
|
+
# some operations like `rm` or `insert` so previous paths are no longer
|
160
|
+
# valid. For this reason these sensitive operations that change paths need
|
161
|
+
# to be done at the end and with careful order.
|
162
|
+
# See https://www.redhat.com/archives/augeas-devel/2017-March/msg00002.html
|
163
|
+
#
|
164
|
+
# @note This class depends on ordered operations. So adding and removing
|
165
|
+
# entries has to be done in order how they are placed in tree.
|
166
|
+
class LazyOperations
|
167
|
+
# @param aug result of Augeas.create
|
168
|
+
def initialize(aug)
|
169
|
+
@aug = aug
|
170
|
+
@operations = []
|
171
|
+
end
|
172
|
+
|
173
|
+
def add(located_entry)
|
174
|
+
@operations << { type: :add, located_entry: located_entry }
|
175
|
+
end
|
176
|
+
|
177
|
+
def remove(located_entry)
|
178
|
+
@operations << { type: :remove, path: located_entry.path }
|
179
|
+
end
|
180
|
+
|
181
|
+
# starts all previously inserted operations
|
182
|
+
def run
|
183
|
+
# the reverse order is needed because if there are two operations
|
184
|
+
# one after another then the latter cannot affect the former
|
185
|
+
@operations.reverse_each do |operation|
|
186
|
+
case operation[:type]
|
187
|
+
when :remove then aug.rm(operation[:path])
|
188
|
+
when :add
|
189
|
+
located_entry = operation[:located_entry]
|
190
|
+
add_entry(located_entry)
|
191
|
+
else
|
192
|
+
raise "Invalid lazy operation #{operation.inspect}"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
private
|
198
|
+
|
199
|
+
attr_reader :aug
|
200
|
+
|
201
|
+
# Adds entry to tree. At first it finds where to add it to be in correct
|
202
|
+
# place and then sets its value. Recursive if needed. In recursive case
|
203
|
+
# it is already known that whole sub-tree is also new and just added.
|
204
|
+
def add_entry(located_entry)
|
205
|
+
path = insert_entry(located_entry)
|
206
|
+
set_new_value(path, located_entry)
|
207
|
+
end
|
208
|
+
|
209
|
+
# Sets new value to given path. It is used for values that are not yet in
|
210
|
+
# Augeas tree. If needed it does recursive adding.
|
211
|
+
# @param path [String] path which can contain Augeas path expression for
|
212
|
+
# key of new value
|
213
|
+
# @param located_entry [LocatedEntry] entry to write
|
214
|
+
# @see https://github.com/hercules-team/augeas/wiki/Path-expressions
|
215
|
+
def set_new_value(path, located_entry)
|
216
|
+
aug.set(path, located_entry.entry_value)
|
217
|
+
prefix = path[/(^.*)\/[^\/]+/, 1]
|
218
|
+
# we need to get new path as set can look like [last() + 1]
|
219
|
+
# which creates new entry and we do not want to add subtree to new
|
220
|
+
# entries
|
221
|
+
new_path = aug.match(prefix + "/*[last()]").first
|
222
|
+
add_subtree(located_entry.entry_tree, new_path)
|
223
|
+
end
|
224
|
+
|
225
|
+
# Adds new subtree. Simplified version of common write as it is known
|
226
|
+
# that all entries will be just added.
|
227
|
+
# @param tree [CFA::AugeasTree] to add
|
228
|
+
# @param prefix [String] prefix where to place *tree*
|
229
|
+
def add_subtree(tree, prefix)
|
230
|
+
tree.all_data.each do |entry|
|
231
|
+
located_entry = LocatedEntry.new(tree, entry, prefix)
|
232
|
+
# universal path that handles also new elements for arrays
|
233
|
+
path = "#{prefix}/#{located_entry.key}[last()+1]"
|
234
|
+
set_new_value(path, located_entry)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
# It inserts a key at given position without setting its value.
|
239
|
+
# Its logic is to set it after the last valid entry. If it is not defined
|
240
|
+
# then tries to place it before the first valid entry in tree. If there is
|
241
|
+
# no entry in tree, then does not insert a position, which means that
|
242
|
+
# subsequent setting of value appends it to the end.
|
243
|
+
#
|
244
|
+
# @param located_entry [LocatedEntry] entry to insert
|
245
|
+
# @return [String] where value should be written. Can
|
246
|
+
# contain path expressions.
|
247
|
+
# See https://github.com/hercules-team/augeas/wiki/Path-expressions
|
248
|
+
def insert_entry(located_entry)
|
249
|
+
# entries with add not exist yet
|
250
|
+
preceding = located_entry.preceding_existing
|
251
|
+
prefix = located_entry.prefix
|
252
|
+
if preceding
|
253
|
+
insert_after(preceding, located_entry)
|
254
|
+
# entries with remove is already removed, otherwise find previously
|
255
|
+
elsif located_entry.any_following?
|
256
|
+
aug.insert(prefix + "/*[1]", located_entry.key, true)
|
257
|
+
aug.match(prefix + "/*[1]").first
|
258
|
+
else
|
259
|
+
"#{prefix}/#{located_entry.key}"
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
# Insert key after preceding.
|
264
|
+
# @see insert_entry
|
265
|
+
# @param preceding [LocatedEntry] entry after which the new one goes
|
266
|
+
# @param located_entry [LocatedEntry] entry to insert
|
267
|
+
# @return [String] where value should be written.
|
268
|
+
def insert_after(preceding, located_entry)
|
269
|
+
aug.insert(preceding.path, located_entry.key, false)
|
270
|
+
paths = aug.match(located_entry.prefix + "/*")
|
271
|
+
paths_index = paths.index(preceding.path) + 1
|
272
|
+
paths[paths_index]
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
attr_reader :aug
|
277
|
+
|
278
|
+
# Does modification according to the operation defined in {AugeasElement}
|
279
|
+
# @param located_entry [LocatedEntry] entry to process
|
280
|
+
def process_operation(located_entry)
|
281
|
+
case located_entry.entry[:operation]
|
282
|
+
when :add, nil then @lazy_operations.add(located_entry)
|
283
|
+
when :remove then @lazy_operations.remove(located_entry)
|
284
|
+
when :modify then modify_entry(located_entry)
|
285
|
+
when :keep then recurse_write(located_entry)
|
286
|
+
else raise "invalid :operation in #{located_entry.inspect}"
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
# Writes value of entry to path and if it has a sub-tree
|
291
|
+
# then it calls {#write} on it
|
292
|
+
# @param located_entry [LocatedEntry] entry to modify
|
293
|
+
def modify_entry(located_entry)
|
294
|
+
value = located_entry.entry_value
|
295
|
+
aug.set(located_entry.path, value)
|
296
|
+
report_error { aug.set(located_entry.path, value) }
|
297
|
+
recurse_write(located_entry)
|
298
|
+
end
|
299
|
+
|
300
|
+
# calls write on entry if entry have sub-tree
|
301
|
+
# @param located_entry [LocatedEntry] entry to recursive write
|
302
|
+
def recurse_write(located_entry)
|
303
|
+
write(located_entry.path, located_entry.entry_tree, top_level: false)
|
304
|
+
end
|
305
|
+
|
306
|
+
# Calls block and if it failed, raise exception with details from augeas
|
307
|
+
# why it failed
|
308
|
+
# @yield call to aug that is secured
|
309
|
+
# @raise [RuntimeError]
|
310
|
+
def report_error
|
311
|
+
return if yield
|
312
|
+
|
313
|
+
error = aug.error
|
314
|
+
# zero is no error, so problem in lense
|
315
|
+
if aug.error[:code].nonzero?
|
316
|
+
raise "Augeas error #{error[:message]}. Details: #{error[:details]}."
|
317
|
+
end
|
318
|
+
|
319
|
+
msg = aug.get("/augeas/text/store/error/message")
|
320
|
+
location = aug.get("/augeas/text/store/error/lens")
|
321
|
+
raise "Augeas serializing error: #{msg} at #{location}"
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
data/lib/cfa/base_model.rb
CHANGED
@@ -53,14 +53,15 @@ module CFA
|
|
53
53
|
# smart to at first modify existing value, then replace commented out code
|
54
54
|
# and if even that doesn't work, then append it at the end
|
55
55
|
# @note prefer to use specialized methods of children
|
56
|
-
def generic_set(key, value)
|
57
|
-
modify(key, value) || uncomment(key, value) ||
|
56
|
+
def generic_set(key, value, tree = data)
|
57
|
+
modify(key, value, tree) || uncomment(key, value, tree) ||
|
58
|
+
add_new(key, value, tree)
|
58
59
|
end
|
59
60
|
|
60
61
|
# powerfull method that gets unformatted any value in config.
|
61
62
|
# @note prefer to use specialized methods of children
|
62
|
-
def generic_get(key)
|
63
|
-
|
63
|
+
def generic_get(key, tree = data)
|
64
|
+
tree[key]
|
64
65
|
end
|
65
66
|
|
66
67
|
# rubocop:disable Style/TrivialAccessors
|
@@ -120,18 +121,18 @@ module CFA
|
|
120
121
|
# Modify an **existing** entry and return `true`,
|
121
122
|
# or do nothing and return `false`.
|
122
123
|
# @return [Boolean]
|
123
|
-
def modify(key, value)
|
124
|
+
def modify(key, value, tree)
|
124
125
|
# if already set, just change value
|
125
|
-
return false unless
|
126
|
+
return false unless tree[key]
|
126
127
|
|
127
|
-
|
128
|
+
tree[key] = value
|
128
129
|
true
|
129
130
|
end
|
130
131
|
|
131
132
|
# Replace a commented out entry and return `true`,
|
132
133
|
# or do nothing and return `false`.
|
133
134
|
# @return [Boolean]
|
134
|
-
def uncomment(key, value)
|
135
|
+
def uncomment(key, value, tree)
|
135
136
|
# Try to find if it is commented out, so we can replace line
|
136
137
|
matcher = Matcher.new(
|
137
138
|
collection: "#comment",
|
@@ -139,15 +140,15 @@ module CFA
|
|
139
140
|
# FIXME: this will match also "# If you set FOO=bar then..."
|
140
141
|
value_matcher: /(\s|^)#{key}\s*=/
|
141
142
|
)
|
142
|
-
return false unless
|
143
|
+
return false unless tree.data.any?(&matcher)
|
143
144
|
|
144
145
|
# FIXME: this assumes that *data* is an AugeasTree
|
145
|
-
|
146
|
+
tree.add(key, value, ReplacePlacer.new(matcher))
|
146
147
|
true
|
147
148
|
end
|
148
149
|
|
149
|
-
def add_new(key, value)
|
150
|
-
|
150
|
+
def add_new(key, value, tree)
|
151
|
+
tree.add(key, value)
|
151
152
|
end
|
152
153
|
end
|
153
154
|
|
data/lib/cfa/placer.rb
CHANGED
@@ -11,14 +11,20 @@ module CFA
|
|
11
11
|
raise NotImplementedError,
|
12
12
|
"Subclasses of #{Module.nesting.first} must override #{__method__}"
|
13
13
|
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def create_element
|
18
|
+
{ operation: :add }
|
19
|
+
end
|
14
20
|
end
|
15
21
|
|
16
22
|
# Places the new element at the end of the tree.
|
17
23
|
class AppendPlacer < Placer
|
18
24
|
# (see Placer#new_element)
|
19
25
|
def new_element(tree)
|
20
|
-
res =
|
21
|
-
tree.
|
26
|
+
res = create_element
|
27
|
+
tree.all_data << res
|
22
28
|
|
23
29
|
res
|
24
30
|
end
|
@@ -37,14 +43,15 @@ module CFA
|
|
37
43
|
|
38
44
|
# (see Placer#new_element)
|
39
45
|
def new_element(tree)
|
40
|
-
index = tree.
|
46
|
+
index = tree.all_data.index(&@matcher)
|
41
47
|
|
42
|
-
res =
|
48
|
+
res = create_element
|
43
49
|
if index
|
44
|
-
tree.
|
50
|
+
tree.all_data.insert(index, res)
|
45
51
|
else
|
46
|
-
tree.
|
52
|
+
tree.all_data << res
|
47
53
|
end
|
54
|
+
|
48
55
|
res
|
49
56
|
end
|
50
57
|
end
|
@@ -61,14 +68,15 @@ module CFA
|
|
61
68
|
|
62
69
|
# (see Placer#new_element)
|
63
70
|
def new_element(tree)
|
64
|
-
index = tree.
|
71
|
+
index = tree.all_data.index(&@matcher)
|
65
72
|
|
66
|
-
res =
|
73
|
+
res = create_element
|
67
74
|
if index
|
68
|
-
tree.
|
75
|
+
tree.all_data.insert(index + 1, res)
|
69
76
|
else
|
70
|
-
tree.
|
77
|
+
tree.all_data << res
|
71
78
|
end
|
79
|
+
|
72
80
|
res
|
73
81
|
end
|
74
82
|
end
|
@@ -86,13 +94,16 @@ module CFA
|
|
86
94
|
|
87
95
|
# (see Placer#new_element)
|
88
96
|
def new_element(tree)
|
89
|
-
index = tree.
|
90
|
-
res =
|
97
|
+
index = tree.all_data.index(&@matcher)
|
98
|
+
res = create_element
|
91
99
|
|
92
100
|
if index
|
93
|
-
|
101
|
+
# remove old one and add new one, as it can have different key
|
102
|
+
# which cause problem to simple modify
|
103
|
+
tree.all_data[index][:operation] = :remove
|
104
|
+
tree.all_data.insert(index + 1, res)
|
94
105
|
else
|
95
|
-
tree.
|
106
|
+
tree.all_data << res
|
96
107
|
end
|
97
108
|
|
98
109
|
res
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cfa
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josef Reidinger
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-03-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ruby-augeas
|
@@ -34,6 +34,9 @@ extensions: []
|
|
34
34
|
extra_rdoc_files: []
|
35
35
|
files:
|
36
36
|
- lib/cfa/augeas_parser.rb
|
37
|
+
- lib/cfa/augeas_parser/keys_cache.rb
|
38
|
+
- lib/cfa/augeas_parser/reader.rb
|
39
|
+
- lib/cfa/augeas_parser/writer.rb
|
37
40
|
- lib/cfa/base_model.rb
|
38
41
|
- lib/cfa/matcher.rb
|
39
42
|
- lib/cfa/memory_file.rb
|
@@ -58,9 +61,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
58
61
|
version: 1.3.6
|
59
62
|
requirements: []
|
60
63
|
rubyforge_project:
|
61
|
-
rubygems_version: 2.
|
64
|
+
rubygems_version: 2.2.2
|
62
65
|
signing_key:
|
63
66
|
specification_version: 4
|
64
67
|
summary: CFA (Config Files API) provides an easy way to create models on top of configuration
|
65
68
|
files
|
66
69
|
test_files: []
|
70
|
+
has_rdoc:
|