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