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