ruby-augeas 0.4.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,326 @@
1
+ # -*- coding: utf-8 -*-
2
+ ##
3
+ # augeas.rb: Ruby wrapper for augeas
4
+ #
5
+ # Copyright (C) 2008 Red Hat Inc.
6
+ # Copyright (C) 2011 SUSE LINUX Products GmbH, Nuernberg, Germany.
7
+ #
8
+ # This library is free software; you can redistribute it and/or
9
+ # modify it under the terms of the GNU Lesser General Public
10
+ # License as published by the Free Software Foundation; either
11
+ # version 2.1 of the License, or (at your option) any later version.
12
+ #
13
+ # This library is distributed in the hope that it will be useful,
14
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16
+ # Lesser General Public License for more details.
17
+ #
18
+ # You should have received a copy of the GNU Lesser General Public
19
+ # License along with this library; if not, write to the Free Software
20
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21
+ #
22
+ # Authors: Ionuț Arțăriși <iartarisi@suse.cz>
23
+ # Bryan Kearney <bkearney@redhat.com>
24
+ # Artem Sheremet <dot.doom@gmail.com>
25
+ ##
26
+
27
+ # Do not require this file explicitly; instead require "augeas"
28
+
29
+ # Wrapper class for the augeas[http://augeas.net] library.
30
+ class Augeas::Facade
31
+ private_class_method :new
32
+
33
+ def self.create(opts={}, &block)
34
+ # aug_flags is a bitmask in the underlying library, we add all the
35
+ # values of the flags which were set to true to the default value
36
+ # Augeas::NONE (which is 0)
37
+ aug_flags = defined?(Augeas::NO_ERR_CLOSE) ? Augeas::NO_ERR_CLOSE : Augeas::NONE
38
+
39
+ flags = {
40
+ :type_check => Augeas::TYPE_CHECK,
41
+ :no_stdinc => Augeas::NO_STDINC,
42
+ :no_load => Augeas::NO_LOAD,
43
+ :no_modl_autoload => Augeas::NO_MODL_AUTOLOAD,
44
+ :enable_span => Augeas::ENABLE_SPAN
45
+ }
46
+ save_modes = {
47
+ :backup => Augeas::SAVE_BACKUP,
48
+ :newfile => Augeas::SAVE_NEWFILE,
49
+ :noop => Augeas::SAVE_NOOP
50
+ }
51
+ opts.each_key do |key|
52
+ if flags.key? key
53
+ aug_flags |= flags[key]
54
+ elsif key == :save_mode
55
+ if save_modes[opts[:save_mode]]
56
+ aug_flags |= save_modes[opts[:save_mode]]
57
+ else
58
+ raise ArgumentError, "Invalid save mode #{opts[:save_mode]}."
59
+ end
60
+ elsif key != :root && key != :loadpath
61
+ raise ArgumentError, "Unknown argument #{key}."
62
+ end
63
+ end
64
+
65
+ aug = Augeas::Facade::open3(opts[:root], opts[:loadpath], aug_flags)
66
+
67
+ begin
68
+ aug.send(:raise_last_error)
69
+ rescue
70
+ aug.close
71
+ raise
72
+ end
73
+
74
+ if block_given?
75
+ begin
76
+ yield aug
77
+ ensure
78
+ aug.close
79
+ end
80
+ else
81
+ return aug
82
+ end
83
+ end
84
+
85
+ # Get the value associated with +path+.
86
+ def get(path)
87
+ run_command :augeas_get, path
88
+ end
89
+
90
+ # Return true if there is an entry for this path, false otherwise
91
+ def exists(path)
92
+ run_command :augeas_exists, path
93
+ end
94
+
95
+ # Set one or multiple elements to path.
96
+ # Multiple elements are mainly sensible with a path like
97
+ # .../array[last()+1], since this will append all elements.
98
+ def set(path, *values)
99
+ values.flatten.each { |v| run_command :augeas_set, path, v }
100
+ end
101
+
102
+ # Set multiple nodes in one operation. Find or create a node matching SUB
103
+ # by interpreting SUB as a path expression relative to each node matching
104
+ # BASE. If SUB is '.', the nodes matching BASE will be modified.
105
+
106
+ # +base+ the base node
107
+ # +sub+ the subtree relative to the base
108
+ # +value+ the value for the nodes
109
+ def setm(base, sub, value)
110
+ run_command :augeas_setm, base, sub, value
111
+ end
112
+
113
+ # Remove all nodes matching path expression +path+ and all their
114
+ # children.
115
+ # Raises an <tt>Augeas::InvalidPathError</tt> when the +path+ is invalid.
116
+ def rm(path)
117
+ run_command :augeas_rm, path
118
+ end
119
+
120
+ # Return an Array of all the paths that match the path expression +path+
121
+ #
122
+ # Returns an empty Array if no paths were found.
123
+ # Raises an <tt>Augeas::InvalidPathError</tt> when the +path+ is invalid.
124
+ def match(path)
125
+ run_command :augeas_match, path
126
+ end
127
+
128
+ # Create the +path+ with empty value if it doesn't exist
129
+ def touch(path)
130
+ set(path, nil) if match(path).empty?
131
+ end
132
+
133
+ # Evaluate +expr+ and set the variable +name+ to the resulting
134
+ # nodeset. The variable can be used in path expressions as $name.
135
+ # Note that +expr+ is evaluated when the variable is defined, not when
136
+ # it is used.
137
+ def defvar(name, expr)
138
+ run_command :augeas_defvar, name, expr
139
+ end
140
+
141
+ # Define the variable +name+ to the result of evaluating +expr+, which
142
+ # must be a nodeset. If no node matching +expr+ exists yet, one is
143
+ # created and +name+ will refer to it. When a node is created and
144
+ # +value+ is given, the new node's value is set to +value+.
145
+ def defnode(name, expr, value=nil)
146
+ run_command :augeas_defnode, name, expr, value
147
+ end
148
+
149
+ # Clear the +path+, i.e. make its value +nil+
150
+ def clear(path)
151
+ augeas_set(path, nil)
152
+ end
153
+
154
+ # Add a transform under <tt>/augeas/load</tt>
155
+ #
156
+ # The HASH can contain the following entries
157
+ # * <tt>:lens</tt> - the name of the lens to use
158
+ # * <tt>:name</tt> - a unique name; use the module name of the LENS
159
+ # when omitted
160
+ # * <tt>:incl</tt> - a list of glob patterns for the files to transform
161
+ # * <tt>:excl</tt> - a list of the glob patterns to remove from the
162
+ # list that matches <tt>:incl</tt>
163
+ def transform(hash)
164
+ lens = hash[:lens]
165
+ name = hash[:name]
166
+ incl = hash[:incl]
167
+ excl = hash[:excl]
168
+ raise ArgumentError, "No lens specified" unless lens
169
+ raise ArgumentError, "No files to include" unless incl
170
+ name = lens.split(".")[0].sub("@", "") unless name
171
+
172
+ xfm = "/augeas/load/#{name}/"
173
+ set(xfm + "lens", lens)
174
+ set(xfm + "incl[last()+1]", incl)
175
+ set(xfm + "excl[last()+1]", excl) if excl
176
+ end
177
+
178
+ # Clear all transforms under <tt>/augeas/load</tt>. If +load+
179
+ # is called right after this, there will be no files
180
+ # under +/files+
181
+ def clear_transforms
182
+ rm("/augeas/load/*")
183
+ end
184
+
185
+ # Write all pending changes to disk.
186
+ # Raises <tt>Augeas::CommandExecutionError</tt> if saving fails.
187
+ def save
188
+ begin
189
+ run_command :augeas_save
190
+ rescue Augeas::CommandExecutionError => e
191
+ raise e, 'Saving failed. Search the augeas tree in /augeas//error ' <<
192
+ 'for the actual errors.'
193
+ end
194
+
195
+ nil
196
+ end
197
+
198
+ def clearm(path, sub)
199
+ setm(path, sub, nil)
200
+ end
201
+
202
+ # Load files according to the transforms in /augeas/load or those
203
+ # defined via <tt>transform</tt>. A transform Foo is represented
204
+ # with a subtree /augeas/load/Foo. Underneath /augeas/load/Foo, one
205
+ # node labeled 'lens' must exist, whose value is the fully
206
+ # qualified name of a lens, for example 'Foo.lns', and multiple
207
+ # nodes 'incl' and 'excl' whose values are globs that determine
208
+ # which files are transformed by that lens. It is an error if one
209
+ # file can be processed by multiple transforms.
210
+ def load
211
+ begin
212
+ run_command :augeas_load
213
+ rescue Augeas::CommandExecutionError => e
214
+ raise e, "Loading failed. Search the augeas tree in /augeas//error"+
215
+ "for the actual errors."
216
+ end
217
+
218
+ nil
219
+ end
220
+
221
+ # Move node +src+ to +dst+. +src+ must match exactly one node in
222
+ # the tree. +dst+ must either match exactly one node in the tree,
223
+ # or may not exist yet. If +dst+ exists already, it and all its
224
+ # descendants are deleted. If +dst+ does not exist yet, it and all
225
+ # its missing ancestors are created.
226
+ #
227
+ # Raises <tt>Augeas::NoMatchError</tt> if the +src+ node does not exist
228
+ # Raises <tt>Augeas::MultipleMatchesError</tt> if there were
229
+ # multiple matches in +src+
230
+ # Raises <tt>Augeas::DescendantError</tt> if the +dst+ node is a
231
+ # descendant of the +src+ node.
232
+ def mv(src, dst)
233
+ run_command :augeas_mv, src, dst
234
+ end
235
+
236
+ # Get the filename, label and value position in the text of this node
237
+ #
238
+ # Raises <tt>Augeas::NoMatchError</tt> if the node could not be found
239
+ # Raises <tt>Augeas::NoSpanInfo</tt> if the node associated with
240
+ # +path+ doesn't belong to a file or doesn't exist
241
+ def span(path)
242
+ run_command :augeas_span, path
243
+ end
244
+
245
+ # Run one or more newline-separated commands specified by +text+,
246
+ # returns an array of [successful_commands_number, output] or
247
+ # [-2, output] in case 'quit' command has been encountered.
248
+ # Raises <tt>Augeas::CommandExecutionError</tt> if gets an invalid command
249
+ def srun(text)
250
+ run_command(:augeas_srun, text)
251
+ end
252
+
253
+ # Lookup the label associated with +path+
254
+ # Raises <tt>Augeas::NoMatchError</tt> if the +path+ node does not exist
255
+ def label(path)
256
+ run_command :augeas_label, path
257
+ end
258
+
259
+ # Rename the label of all nodes matching +path+ to +label+
260
+ # Raises <tt>Augeas::NoMatchError</tt> if the +path+ node does not exist
261
+ # Raises <tt>Augeas::InvalidLabelError</tt> if +label+ is invalid
262
+ def rename(path, label)
263
+ run_command :augeas_rename, path, label
264
+ end
265
+
266
+ # Use the value of node +node+ as a string and transform it into a tree
267
+ # using the lens +lens+ and store it in the tree at +path+,
268
+ # which will be overwritten. +path+ and +node+ are path expressions.
269
+ def text_store(lens, node, path)
270
+ run_command :augeas_text_store, lens, node, path
271
+ end
272
+
273
+ # Transform the tree at +path+ into a string lens +lens+ and store it
274
+ # in the node +node_out+, assuming the tree was initially generated using
275
+ # the value of node +node_in+. +path+, +node_in+ and +node_out+ are path expressions.
276
+ def text_retrieve(lens, node_in, path, node_out)
277
+ run_command :augeas_text_retrieve, lens, node_in, path, node_out
278
+ end
279
+
280
+ # Make +label+ a sibling of +path+ by inserting it directly before
281
+ # or after +path+.
282
+ # The boolean +before+ determines if +label+ is inserted before or
283
+ # after +path+.
284
+ def insert(path, label, before)
285
+ run_command :augeas_insert, path, label, before
286
+ end
287
+
288
+ # Set path expression context to +path+ (in /augeas/context)
289
+ def context=(path)
290
+ set('/augeas/context', path)
291
+ end
292
+
293
+ # Get path expression context (from /augeas/context)
294
+ def context
295
+ get('/augeas/context')
296
+ end
297
+
298
+ private
299
+
300
+ # Run a command and raise any errors that happen due to execution.
301
+ #
302
+ # +cmd+ name of the Augeas command to run
303
+ # +params+ parameters with which +cmd+ will be called
304
+ #
305
+ # Returns whatever the original +cmd+ returns
306
+ def run_command(cmd, *params)
307
+ result = self.send cmd, *params
308
+
309
+ raise_last_error
310
+
311
+ if result.kind_of? Integer and result < 0
312
+ # we raise CommandExecutionError here, because this is the error that
313
+ # augtool raises in this case as well
314
+ raise Augeas::CommandExecutionError, "Command failed. Return code was #{result}."
315
+ end
316
+
317
+ return result
318
+ end
319
+
320
+ def raise_last_error
321
+ error_cache = error
322
+ unless error_cache[:code].zero?
323
+ raise Augeas::ERRORS_HASH[error_cache[:code]], "#{error_cache[:message]} #{error_cache[:details]}"
324
+ end
325
+ end
326
+ end
data/lib/augeas.rb CHANGED
@@ -21,12 +21,83 @@
21
21
  ##
22
22
 
23
23
  require "_augeas"
24
+ require "augeas/facade"
24
25
 
25
26
  # Wrapper class for the augeas[http://augeas.net] library.
26
27
  class Augeas
27
28
  private_class_method :new
28
29
 
29
30
  class Error < RuntimeError; end
31
+ class NoMemoryError < Error; end
32
+ class InternalError < Error; end
33
+ class InvalidPathError < Error; end
34
+ class NoMatchError < Error; end
35
+ class MultipleMatchesError < Error; end
36
+ class LensSyntaxError < Error; end
37
+ class LensNotFoundError < Error; end
38
+ class MultipleTransformsError < Error; end
39
+ class NoSpanInfoError < Error; end
40
+ class DescendantError < Error; end
41
+ class CommandExecutionError < Error; end
42
+ class InvalidArgumentError < Error; end
43
+ class InvalidLabelError < Error; end
44
+ ERRORS_HASH = Hash[{
45
+ # the cryptic error names come from the C library, we just make
46
+ # them more ruby and more human
47
+ :ENOMEM => NoMemoryError,
48
+ :EINTERNAL => InternalError,
49
+ :EPATHX => InvalidPathError,
50
+ :ENOMATCH => NoMatchError,
51
+ :EMMATCH => MultipleMatchesError,
52
+ :ESYNTAX => LensSyntaxError,
53
+ :ENOLENS => LensNotFoundError,
54
+ :EMXFM => MultipleTransformsError,
55
+ :ENOSPAN => NoSpanInfoError,
56
+ :EMVDESC => DescendantError,
57
+ :ECMDRUN => CommandExecutionError,
58
+ :EBADARG => InvalidArgumentError,
59
+ :ELABEL => InvalidLabelError,
60
+ }.map { |k, v| [(const_get(k) rescue nil), v] }].freeze
61
+
62
+ # Create a new Augeas instance and return it.
63
+ #
64
+ # Use +:root+ as the filesystem root. If +:root+ is +nil+, use the value
65
+ # of the environment variable +AUGEAS_ROOT+. If that doesn't exist
66
+ # either, use "/".
67
+ #
68
+ # +:loadpath+ is a colon-spearated list of directories that modules
69
+ # should be searched in. This is in addition to the standard load path
70
+ # and the directories in +AUGEAS_LENS_LIB+
71
+ #
72
+ # The following flags can be specified in a hash. They all default to
73
+ # false and can be enabled by setting them to true
74
+ #
75
+ # :type_check - typecheck lenses (since it can be very expensive it is
76
+ # not done by default)
77
+ #
78
+ # :no_stdinc - do not use the builtin load path for modules
79
+ #
80
+ # :no_load - do not load the tree during the initialization phase
81
+ #
82
+ # :no_modl_autoload - do not load the tree during the initialization phase
83
+ #
84
+ # :enable_span - track the span in the input nodes
85
+ #
86
+ # :save_mode can be one of :backup, :newfile, :noop as explained below.
87
+ #
88
+ # :noop - make save a no-op process, just record what would have changed
89
+ #
90
+ # :backup - keep the original file with an .augsave extension
91
+ #
92
+ # :newfile - save changes into a file with an .augnew extension and
93
+ # do not overwrite the original file.
94
+ #
95
+ # When a block is given, the Augeas instance is passed as the only
96
+ # argument into the block and closed when the block exits.
97
+ # With no block, the Augeas instance is returned.
98
+ def self.create(opts={}, &block)
99
+ Augeas::Facade::create(opts, &block)
100
+ end
30
101
 
31
102
  # Create a new Augeas instance and return it.
32
103
  #
@@ -77,6 +148,18 @@ class Augeas
77
148
  set_internal(path, nil)
78
149
  end
79
150
 
151
+ # Clear multiple nodes values in one operation. Find or create a node matching +sub+
152
+ # by interpreting +sub+ as a path expression relative to each node matching
153
+ # +base+. If +sub+ is '.', the nodes matching +base+ will be modified.
154
+ def clearm(base, sub)
155
+ setm(base, sub, nil)
156
+ end
157
+
158
+ # Create the +path+ with empty value if it doesn't exist
159
+ def touch(path)
160
+ set_internal(path, nil) if match(path).empty?
161
+ end
162
+
80
163
  # Clear all transforms under <tt>/augeas/load</tt>. If +load+
81
164
  # is called right after this, there will be no files
82
165
  # under +/files+
@@ -98,6 +181,7 @@ class Augeas
98
181
  excl = hash[:excl]
99
182
  raise ArgumentError, "No lens specified" unless lens
100
183
  raise ArgumentError, "No files to include" unless incl
184
+ lens = "#{lens}.lns" unless lens.include? '.'
101
185
  name = lens.split(".")[0].sub("@", "") unless name
102
186
 
103
187
  xfm = "/augeas/load/#{name}/"
@@ -116,4 +200,14 @@ class Augeas
116
200
  raise Augeas::Error unless load
117
201
  end
118
202
 
203
+ # Set path expression context to +path+ (in /augeas/context)
204
+ def context=(path)
205
+ set_internal('/augeas/context', path)
206
+ end
207
+
208
+ # Get path expression context (from /augeas/context)
209
+ def context
210
+ get('/augeas/context')
211
+ end
212
+
119
213
  end
data/tests/tc_augeas.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require 'test/unit'
2
2
 
3
- TOPDIR = File::expand_path(File::join(File::dirname(__FILE__), ".."))
3
+ unless defined?(TOPDIR)
4
+ TOPDIR = File::expand_path(File::join(File::dirname(__FILE__), ".."))
5
+ end
4
6
 
5
7
  $:.unshift(File::join(TOPDIR, "lib"))
6
8
  $:.unshift(File::join(TOPDIR, "ext", "augeas"))
@@ -84,7 +86,7 @@ class TestAugeas < Test::Unit::TestCase
84
86
  :incl => [ "/etc/fstab" ],
85
87
  :excl => [ "*~", "*.rpmnew" ])
86
88
  }
87
- aug.transform(:lens => "Inittab.lns",
89
+ aug.transform(:lens => "Inittab",
88
90
  :incl => "/etc/inittab")
89
91
  aug.transform(:lens => "Fstab.lns",
90
92
  :incl => "/etc/fstab*",
@@ -195,6 +197,87 @@ class TestAugeas < Test::Unit::TestCase
195
197
  assert_equal(29..40, span[:span])
196
198
  end
197
199
 
200
+ def test_srun
201
+ aug = aug_open
202
+
203
+ path = "/files/etc/hosts/*[canonical='localhost.localdomain']/ipaddr"
204
+ r, out = aug.srun("get #{path}\n")
205
+ assert_equal(1, r)
206
+ assert_equal("#{path} = 127.0.0.1\n", out)
207
+
208
+ assert_equal(0, aug.srun(" ")[0])
209
+ assert_equal(-1, aug.srun("foo")[0])
210
+ assert_equal(-1, aug.srun("set")[0])
211
+ assert_equal(-2, aug.srun("quit")[0])
212
+ end
213
+
214
+ def test_label
215
+ Augeas::open("/dev/null") do |aug|
216
+ assert_equal 'augeas', aug.label('/augeas')
217
+ assert_equal 'files', aug.label('/files')
218
+ end
219
+ end
220
+
221
+ def test_rename
222
+ Augeas::open("/dev/null") do |aug|
223
+ assert_equal false, aug.rename('/files', 'invalid/label')
224
+ assert_equal 0, aug.rename('/nonexistent', 'label')
225
+ assert_equal ['/files'], aug.match('/files')
226
+ assert_equal 1, aug.rename('/files', 'label')
227
+ end
228
+ end
229
+
230
+ def test_text_store_retrieve
231
+ Augeas::open("/dev/null") do |aug|
232
+ # text_store errors
233
+ assert_equal false, aug.text_store('Simplelines.lns', '/input', '/store')
234
+
235
+ # text_store
236
+ aug.set('/input', "line1\nline2\n")
237
+ assert aug.text_store('Simplelines.lns', '/input', '/store')
238
+ assert_equal 'line2', aug.get('/store/2')
239
+
240
+ # text_retrieve errors
241
+ assert_equal false, aug.text_retrieve('Simplelines.lns', '/unknown', '/store', '/output')
242
+
243
+ # text_retrieve
244
+ aug.set('/store/3', 'line3')
245
+ assert aug.text_retrieve('Simplelines.lns', '/input', '/store', '/output')
246
+ assert_equal "line1\nline2\nline3\n", aug.get('/output')
247
+ end
248
+ end
249
+
250
+ def test_context
251
+ Augeas::open("/dev/null") do |aug|
252
+ aug.context = '/augeas'
253
+ assert_equal '/augeas', aug.get('/augeas/context')
254
+ assert_equal '/augeas', aug.get('context')
255
+ assert_equal '/augeas', aug.context
256
+ end
257
+ end
258
+
259
+ def test_touch
260
+ Augeas::open("/dev/null") do |aug|
261
+ assert_equal [], aug.match('/foo')
262
+ aug.touch '/foo'
263
+ assert_equal ['/foo'], aug.match('/foo')
264
+
265
+ aug.set '/foo', 'bar'
266
+ aug.touch '/foo'
267
+ assert_equal 'bar', aug.get('/foo')
268
+ end
269
+ end
270
+
271
+ def test_clearm
272
+ Augeas::open("/dev/null") do |aug|
273
+ aug.set('/foo/a', '1')
274
+ aug.set('/foo/b', '2')
275
+ aug.clearm('/foo', '*')
276
+ assert_nil aug.get('/foo/a')
277
+ assert_nil aug.get('/foo/b')
278
+ end
279
+ end
280
+
198
281
  private
199
282
  def aug_open(flags = Augeas::NONE)
200
283
  if File::directory?(TST_ROOT)