File Coverage

lib/Rex/Commands/Augeas.pm
Criterion Covered Total %
statement 39 190 20.5
branch 1 76 1.3
condition 0 65 0.0
subroutine 14 17 82.3
pod 1 1 100.0
total 55 349 15.7


line stmt bran cond sub pod time code
1             #
2             # (c) Jan Gehring
3             #
4              
5             =head1 NAME
6              
7             Rex::Commands::Augeas - An augeas module for (R)?ex
8              
9             =head1 DESCRIPTION
10              
11             This is a simple module to manipulate configuration files with the help of augeas.
12              
13             =head1 SYNOPSIS
14              
15             my $k = augeas exists => "/files/etc/hosts/*/ipaddr", "127.0.0.1";
16              
17             augeas insert => "/files/etc/hosts",
18             label => "01",
19             after => "/7",
20             ipaddr => "192.168.2.23",
21             canonical => "test";
22              
23             augeas dump => "/files/etc/hosts";
24              
25             augeas modify =>
26             "/files/etc/ssh/sshd_config/PermitRootLogin" => "without-password",
27             on_change => sub {
28             service ssh => "restart";
29             };
30              
31             =head1 EXPORTED FUNCTIONS
32              
33             =cut
34              
35             package Rex::Commands::Augeas;
36              
37 1     1   42 use v5.12.5;
  1         19  
38 1     1   16 use warnings;
  1         10  
  1         103  
39              
40             our $VERSION = '1.14.2.3'; # TRIAL VERSION
41              
42             require Exporter;
43              
44 1     1   15 use base qw(Exporter);
  1         14  
  1         171  
45 1     1   16 use vars qw(@EXPORT);
  1         10  
  1         104  
46              
47 1     1   19 use Rex::Logger;
  1         6  
  1         11  
48 1     1   45 use Rex::Commands;
  1         10  
  1         30  
49 1     1   27 use Rex::Commands::Run;
  1         6  
  1         22  
50 1     1   13 use Rex::Commands::Fs;
  1         13  
  1         19  
51 1     1   18 use Rex::Commands::File;
  1         10  
  1         11  
52 1     1   12 use Rex::Helper::Path;
  1         7  
  1         193  
53 1     1   15 use Rex::Helper::Run;
  1         3  
  1         150  
54 1     1   22 use IO::String;
  1         3049  
  1         26  
55              
56             my $has_config_augeas = 0;
57              
58             BEGIN {
59 1     1   66 use Rex::Require;
  1         4  
  1         13  
60 1 50   1   70 if ( Config::Augeas->is_loadable ) {
61 0           Config::Augeas->use;
62 0           $has_config_augeas = 1;
63             }
64             }
65              
66             @EXPORT = qw(augeas);
67              
68             =head2 augeas($action, @options)
69              
70             It returns 1 on success and 0 on failure.
71              
72             Actions:
73              
74             =over 4
75              
76             =cut
77              
78             sub augeas {
79 0     0 1   my ( $action, @options ) = @_;
80 0           my $ret;
81              
82 0           my $is_ssh = Rex::is_ssh();
83 0           my $aug; # Augeas object (non-SSH only)
84 0 0 0       if ( !$is_ssh && $has_config_augeas ) {
85 0           Rex::Logger::debug("Creating Config::Augeas Object");
86 0           $aug = Config::Augeas->new;
87             }
88              
89 0           my $on_change; # Any code to run on change
90             my $changed; # Whether any changes have taken place
91              
92             =item modify
93              
94             This modifies the keys given in @options in $file.
95              
96             augeas modify =>
97             "/files/etc/hosts/7/ipaddr" => "127.0.0.2",
98             "/files/etc/hosts/7/canonical" => "test01",
99             on_change => sub { say "I changed!" };
100              
101             =cut
102              
103 0 0         if ( $action eq "modify" ) {
    0          
    0          
    0          
    0          
    0          
104 0           my $config_option = {@options};
105              
106             # Code to run on a change being made
107             $on_change = delete $config_option->{on_change}
108 0 0         if ref $config_option->{on_change} eq 'CODE';
109              
110 0 0 0       if ( $is_ssh || !$has_config_augeas ) {
111 0           my @commands;
112 0           for my $key ( keys %{$config_option} ) {
  0            
113 0           Rex::Logger::debug( "modifying $key -> " . $config_option->{$key} );
114 0           push @commands, qq(set $key "$config_option->{$key}"\n);
115             }
116 0           my $result = _run_augtool(@commands);
117 0           $ret = $result->{return};
118 0           $changed = $result->{changed};
119             }
120             else {
121 0           for my $key ( keys %{$config_option} ) {
  0            
122 0           Rex::Logger::debug( "modifying $key -> " . $config_option->{$key} );
123 0           $aug->set( $key, $config_option->{$key} );
124             }
125 0           $ret = $aug->save;
126 0           Rex::Logger::debug("Augeas set status: $ret");
127 0 0 0       $changed = 1 if $ret && $aug->get('/augeas/events/saved'); # Any files changed?
128             }
129             }
130              
131             =item remove
132              
133             Remove an entry.
134              
135             augeas remove => "/files/etc/hosts/2",
136             on_change => sub { say "I changed!" };
137              
138             =cut
139              
140             elsif ( $action eq "remove" ) {
141              
142             # Code to run on a change being made
143 0 0 0       if ( $options[-2]
      0        
144             && $options[-2] eq 'on_change'
145             && ref $options[-1] eq 'CODE' )
146             {
147 0           $on_change = pop @options;
148 0           pop @options;
149             }
150              
151 0           my @commands;
152 0           for my $aug_key (@options) {
153 0           Rex::Logger::debug("deleting $aug_key");
154              
155 0 0 0       if ( $is_ssh || !$has_config_augeas ) {
156 0           push @commands, "rm $aug_key\n";
157             }
158             else {
159 0           my $_r = $aug->remove($aug_key);
160 0           Rex::Logger::debug("Augeas delete status: $_r");
161             }
162             }
163              
164 0 0 0       if ( $is_ssh || !$has_config_augeas ) {
165 0           my $result = _run_augtool(@commands);
166 0           $ret = $result->{return};
167 0           $changed = $result->{changed};
168             }
169             else {
170 0           $ret = $aug->save;
171 0 0 0       $changed = 1 if $ret && $aug->get('/augeas/events/saved'); # Any files changed?
172             }
173              
174             }
175              
176             =item insert
177              
178             Insert an item into the file. Here, the order of the options is important. If the order is wrong it won't save your changes.
179              
180             augeas insert => "/files/etc/hosts",
181             label => "01",
182             after => "/7",
183             ipaddr => "192.168.2.23",
184             alias => "test02",
185             on_change => sub { say "I changed!" };
186              
187             =cut
188              
189             elsif ( $action eq "insert" ) {
190 0           my $file = shift @options;
191 0           my $opts = {@options};
192              
193 0           my $label = $opts->{"label"};
194 0           delete $opts->{"label"};
195              
196             # Code to run on a change being made
197 0 0 0       if ( $options[-2]
      0        
198             && $options[-2] eq 'on_change'
199             && ref $options[-1] eq 'CODE' )
200             {
201 0           $on_change = pop @options;
202 0           pop @options;
203             }
204              
205 0 0 0       if ( $is_ssh || !$has_config_augeas ) {
206 0 0         my $position = ( exists $opts->{"before"} ? "before" : "after" );
207 0 0         unless ( exists $opts->{$position} ) {
208 0           Rex::Logger::info(
209             "Error inserting key. You have to specify before or after.");
210 0           return 0;
211             }
212              
213 0           my @commands = ("ins $label $position $file$opts->{$position}\n");
214 0           delete $opts->{$position};
215              
216 0           for ( my $i = 0 ; $i < @options ; $i += 2 ) {
217 0           my $key = $options[$i];
218 0           my $val = $options[ $i + 1 ];
219 0 0 0       next if ( $key eq "after" or $key eq "before" or $key eq "label" );
      0        
220              
221 0           my $_key = "$file/$label/$key";
222 0           Rex::Logger::debug("Setting $_key => $val");
223              
224 0           push @commands, qq(set $_key "$val"\n);
225             }
226 0           my $result = _run_augtool(@commands);
227 0           $ret = $result->{return};
228 0           $changed = $result->{changed};
229             }
230             else {
231 0 0         if ( exists $opts->{"before"} ) {
    0          
232 0           $aug->insert( $label, before => "$file" . $opts->{"before"} );
233 0           delete $opts->{"before"};
234             }
235             elsif ( exists $opts->{"after"} ) {
236 0           my $t = $aug->insert( $label, after => "$file" . $opts->{"after"} );
237 0           delete $opts->{"after"};
238             }
239             else {
240 0           Rex::Logger::info(
241             "Error inserting key. You have to specify before or after.");
242 0           return 0;
243             }
244              
245 0           for ( my $i = 0 ; $i < @options ; $i += 2 ) {
246 0           my $key = $options[$i];
247 0           my $val = $options[ $i + 1 ];
248              
249 0 0 0       next if ( $key eq "after" or $key eq "before" or $key eq "label" );
      0        
250              
251 0           my $_key = "$file/$label/$key";
252 0           Rex::Logger::debug("Setting $_key => $val");
253              
254 0           $aug->set( $_key, $val );
255             }
256              
257 0           $ret = $aug->save();
258 0 0 0       $changed = 1 if $ret && $aug->get('/augeas/events/saved'); # Any files changed?
259             }
260             }
261              
262             =item dump
263              
264             Dump the contents of a file to STDOUT.
265              
266             augeas dump => "/files/etc/hosts";
267              
268             =cut
269              
270             elsif ( $action eq "dump" ) {
271 0           my $file = shift @options;
272 0           my $aug_key = $file;
273              
274 0 0 0       if ( $is_ssh || !$has_config_augeas ) {
275 0           my @list = i_exec "augtool", "print", $aug_key;
276 0           print join( "\n", @list ) . "\n";
277             }
278             else {
279 0           $aug->print($aug_key);
280             }
281 0           $ret = 0;
282             }
283              
284             =item exists
285              
286             Check if an item exists.
287              
288             my $exists = augeas exists => "/files/etc/hosts/*/ipaddr" => "127.0.0.1";
289             if($exists) {
290             say "127.0.0.1 exists!";
291             }
292              
293             =cut
294              
295             elsif ( $action eq "exists" ) {
296 0           my $file = shift @options;
297              
298 0           my $aug_key = $file;
299 0   0       my $val = $options[0] || "";
300              
301 0 0 0       if ( $is_ssh || !$has_config_augeas ) {
302 0           my @paths;
303 0           my $result = _run_augtool("match $aug_key");
304 0           for my $line ( split "\n", $result->{return} ) {
305 0 0         $line =~ s/\s=[^=]+$// or next;
306 0           push @paths, $line;
307             }
308              
309 0 0         if ($val) {
310 0           for my $k (@paths) {
311 0           my @ret;
312 0           my $result = _run_augtool("get $k");
313 0           for my $line ( split "\n", $result->{return} ) {
314 0           $line =~ s/^[^=]+=\s//;
315 0           push @ret, $line;
316             }
317              
318 0 0         if ( $ret[0] eq $val ) {
319 0           return $k;
320             }
321             }
322             }
323             else {
324 0           return @paths;
325             }
326              
327 0           $ret = undef;
328             }
329             else {
330 0           my @paths = $aug->match($aug_key);
331              
332 0 0         if ($val) {
333 0           for my $k (@paths) {
334 0 0         if ( $aug->get($k) eq $val ) {
335 0           return $k;
336             }
337             }
338             }
339             else {
340 0           return @paths;
341             }
342              
343 0           $ret = undef;
344             }
345             }
346              
347             =item get
348              
349             Returns the value of the given item.
350              
351             my $val = augeas get => "/files/etc/hosts/1/ipaddr";
352              
353             =cut
354              
355             elsif ( $action eq "get" ) {
356 0           my $file = shift @options;
357              
358 0 0 0       if ( $is_ssh || !$has_config_augeas ) {
359 0           my @lines;
360 0           my $result = _run_augtool("get $file");
361 0           for my $line ( split "\n", $result->{return} ) {
362 0           $line =~ s/^[^=]+=\s//;
363 0           push @lines, $line;
364             }
365 0           return $lines[0];
366             }
367             else {
368 0           return $aug->get($file);
369             }
370             }
371              
372             else {
373 0           Rex::Logger::info("Unknown augeas action.");
374             }
375              
376 0 0 0       if ( $on_change && $changed ) {
377 0           Rex::Logger::debug("Calling on_change hook of augeas");
378 0           $on_change->();
379             }
380              
381 0 0         Rex::Logger::debug("Augeas Returned: $ret") if $ret;
382              
383 0           return $ret;
384             }
385              
386             =back
387              
388             =cut
389              
390             sub _run_augtool {
391 0     0     my (@commands) = @_;
392              
393 0 0         die "augtool is not installed or not executable in the path"
394             unless can_run "augtool";
395 0           my $rnd_file = get_tmp_file;
396 0           my $fh = Rex::Interface::File->create;
397 0           $fh->open( ">", $rnd_file );
398 0           $fh->write($_) foreach (@commands);
399 0           $fh->close;
400             my ( $return, $error ) = i_run "augtool --file $rnd_file --autosave",
401 0     0     sub { @_ }, fail_ok => 1;
  0            
402 0 0         my $ret = $? == 0 ? 1 : 0;
403              
404 0 0         if ($ret) {
405 0           Rex::Logger::debug("Augeas command return value: $ret");
406 0           Rex::Logger::debug("Augeas result: $return");
407             }
408             else {
409 0           Rex::Logger::info( "Augeas command failed: $error", 'warn' );
410             }
411 0 0         my $changed = "$return" =~ /Saved/ ? 1 : 0;
412 0           unlink $rnd_file;
413              
414             {
415 0   0       result => $ret,
416             return => $return || $error,
417             changed => $changed,
418             };
419             }
420              
421             1;