line
stmt
bran
cond
sub
pod
time
code
1
#
2
# (c) Jan Gehring
3
#
4
5
=head1 NAME
6
7
Rex::Commands::File - Transparent File Manipulation
8
9
=head1 DESCRIPTION
10
11
With this module you can manipulate files.
12
13
=head1 SYNOPSIS
14
15
task "read_passwd", "server01", sub {
16
my $fh = file_read "/etc/passwd";
17
for my $line ($fh->read_all) {
18
print $line;
19
}
20
$fh->close;
21
};
22
23
task "read_passwd2", "server01", sub {
24
say cat "/etc/passwd";
25
};
26
27
28
task "write_passwd", "server01", sub {
29
my $fh = file_write "/etc/passwd";
30
$fh->write("root:*:0:0:root user:/root:/bin/sh\n");
31
$fh->close;
32
};
33
34
delete_lines_matching "/var/log/auth.log", matching => "root";
35
delete_lines_matching "/var/log/auth.log", matching => qr{Failed};
36
delete_lines_matching "/var/log/auth.log",
37
matching => "root", qr{Failed}, "nobody";
38
39
file "/path/on/the/remote/machine",
40
source => "/path/on/local/machine";
41
42
file "/path/on/the/remote/machine",
43
content => "foo bar";
44
45
file "/path/on/the/remote/machine",
46
source => "/path/on/local/machine",
47
owner => "root",
48
group => "root",
49
mode => 400,
50
on_change => sub { say shift, " was changed."; },
51
on_no_change => sub { say shift, " wasn't changed."; };
52
53
54
=head1 EXPORTED FUNCTIONS
55
56
=cut
57
58
package Rex::Commands::File;
59
60
45
45
269103
use v5.12.5;
45
552
61
45
45
245
use warnings;
45
142
45
1295
62
45
45
222
use Fcntl;
45
98
45
11403
63
64
our $VERSION = '1.14.2.3'; # TRIAL VERSION
65
66
require Rex::Exporter;
67
45
45
2251
use Data::Dumper;
45
21019
45
2275
68
45
45
2432
use Rex::Config;
45
104
45
398
69
45
45
3235
use Rex::FS::File;
45
130
45
456
70
45
45
1594
use Rex::Commands::Upload;
45
222
45
362
71
45
45
299
use Rex::Commands::MD5;
45
110
45
251
72
45
45
514
use Rex::File::Parser::Data;
45
139
45
570
73
45
45
1377
use Rex::Helper::File::Spec;
45
97
45
263
74
45
45
1072
use Rex::Helper::System;
45
117
45
394
75
45
45
1238
use Rex::Helper::Path;
45
111
45
2603
76
45
45
261
use Rex::Hook;
45
89
45
1609
77
45
45
281
use Carp;
45
107
45
2310
78
79
45
45
276
use Rex::Interface::Exec;
45
116
45
589
80
45
45
1172
use Rex::Interface::File;
45
86
45
244
81
45
45
1110
use Rex::Interface::Fs;
45
115
45
293
82
require Rex::CMDB;
83
84
45
45
1981
use File::Basename qw(dirname basename);
45
93
45
2356
85
86
45
45
274
use vars qw(@EXPORT);
45
116
45
2022
87
45
45
307
use base qw(Rex::Exporter);
45
117
45
4815
88
89
@EXPORT = qw(file_write file_read file_append
90
cat sed
91
delete_lines_matching append_if_no_such_line delete_lines_according_to
92
file template append_or_amend_line
93
extract);
94
95
45
45
324
use vars qw(%file_handles);
45
131
45
180696
96
97
=head2 template($file [, %params])
98
99
Parse a template and return the content.
100
101
By default, it uses L. If any of the L or L<1.3|Rex#1.3> (or newer) feature flag is enabled, then L is used instead of this module (recommended).
102
103
For more advanced functionality, you may use your favorite template engine via the L configuration option.
104
105
Template variables may be passed either as hash or a hash reference. The following calls are equivalent:
106
107
template( $template, variable => value );
108
109
template( $template, { variable => value } );
110
111
=head3 List of exposed template variables
112
113
The following template variables are passed to the underlying templating engine, in order of precedence from low to high (variables of the same name are overridden by the next level aka "last one wins"):
114
115
=over 4
116
117
=item task parameters
118
119
All task parameters coming from the command line via C>, or from calling a task as a function, like C value } )>>.
120
121
=item resource parameters
122
123
All resource parameters as returned by Cget_current_resource()-Eget_all_parameters>, when called inside a resource.
124
125
=item explicit template variables
126
127
All manually specified, explicit template variables passed to C.
128
129
=item system information
130
131
The results from all available L modules as returned by Cget('All')>.
132
133
Pass C<__no_sys_info__ =E TRUE> as a template variable to disable including system information:
134
135
my $content = template( $template, __no_sys_info__ => TRUE );
136
137
=back
138
139
=head3 Embedded templates
140
141
Use C<__DATA__> to embed templates at the end of the file. Prefix embedded template names with C<@>. If embedding multiple templates, mark their end with C<@end>.
142
143
=head4 Single template
144
145
my $content = template( '@hello', name => 'world' ); # Hello, world!
146
__DATA__
147
@hello
148
Hello, <%= $name -%>!
149
150
=head4 Multiple templates
151
152
Use C<@end> to separate multiple templates inside C<__DATA__>.
153
154
my $content = template( '@hello', name => 'world' ); # Hello, world!
155
my $alternative = template( '@hi', name => 'world' ); # Hi, world!
156
157
__DATA__
158
@hello
159
Hello, <%= $name -%>!
160
@end
161
162
@hi
163
Hi, <%= $name -%>!
164
@end
165
166
=head3 File templates
167
168
my $content = template("/files/templates/vhosts.tpl",
169
name => "test.lan",
170
webmaster => 'webmaster@test.lan');
171
172
The file name specified is subject to "path_map" processing as documented
173
under the file() function to resolve to a physical file name.
174
175
In addition to the "path_map" processing, if the B<-E> command line switch
176
is used to specify an environment name, existence of a file ending with
177
'.' is checked and has precedence over the file without one, if it
178
exists. E.g. if rex is started as:
179
180
$ rex -E prod task1
181
182
then in task1 defined as:
183
184
task "task1", sub {
185
say template("files/etc/ntpd.conf");
186
};
187
188
will print the content of 'files/etc/ntpd.conf.prod' if it exists.
189
190
Note: the appended environment mechanism is always applied, after
191
the 'path_map' mechanism, if that is configured.
192
193
=cut
194
195
sub template {
196
19
19
1
6425
my ( $file, @params ) = @_;
197
19
82
my $param;
198
199
19
100
120
if ( ref $params[0] eq "HASH" ) {
200
3
10
$param = $params[0];
201
}
202
else {
203
16
96
$param = {@params};
204
}
205
206
19
50
128
if ( !exists $param->{server} ) {
207
19
159
$param->{server} = Rex::Commands::connection()->server;
208
}
209
210
19
67
my $content;
211
19
100
66
261
if ( ref $file && ref $file eq 'SCALAR' ) {
212
16
50
$content = ${$file};
16
82
213
}
214
else {
215
3
10
$file = resolv_path($file);
216
217
3
100
66
21
unless ( $file =~ m/^\// || $file =~ m/^\@/ ) {
218
219
# path is relative and no template
220
1
8
Rex::Logger::debug("Relativ path $file");
221
222
1
45
$file = Rex::Helper::Path::get_file_path( $file, caller() );
223
224
1
6
Rex::Logger::debug("New filename: $file");
225
}
226
227
# if there is a file called filename.environment then use this file
228
# ex:
229
# $content = template("files/hosts.tpl");
230
#
231
# rex -E live ...
232
# will first look if files/hosts.tpl.live is available, if not it will
233
# use files/hosts.tpl
234
3
50
19
if ( -f "$file." . Rex::Config->get_environment ) {
235
0
0
$file = "$file." . Rex::Config->get_environment;
236
}
237
238
3
100
61
if ( -f $file ) {
50
239
1
3
$content = eval { local ( @ARGV, $/ ) = ($file); <>; };
1
8
1
106
240
}
241
elsif ( $file =~ m/^\@/ ) {
242
2
21
my @caller = caller(0);
243
244
2
10
my $file_path = Rex::get_module_path( $caller[0] );
245
246
2
50
18
if ( !-f $file_path ) {
247
2
7
my ($mod_name) = ( $caller[0] =~ m/^.*::(.*?)$/ );
248
2
50
33
99
if ( $mod_name && -f "$file_path/$mod_name.pm" ) {
50
50
50
0
249
0
0
$file_path = "$file_path/$mod_name.pm";
250
}
251
elsif ( -f "$file_path/__module__.pm" ) {
252
0
0
$file_path = "$file_path/__module__.pm";
253
}
254
elsif ( -f "$file_path/Module.pm" ) {
255
0
0
$file_path = "$file_path/Module.pm";
256
}
257
elsif ( -f $caller[1] ) {
258
2
9
$file_path = $caller[1];
259
}
260
elsif ( $caller[1] =~ m|^/loader/[^/]+/__Rexfile__.pm$| ) {
261
0
0
$file_path = $INC{"__Rexfile__.pm"};
262
}
263
}
264
265
2
5
my $file_content = eval { local ( @ARGV, $/ ) = ($file_path); <>; };
2
11
2
133
266
2
20
my ($data) = ( $file_content =~ m/.*__DATA__(.*)/ms );
267
2
26
my $fp = Rex::File::Parser::Data->new( data => [ split( /\n/, $data ) ] );
268
2
5
my $snippet_to_read = substr( $file, 1 );
269
2
8
$content = $fp->read($snippet_to_read);
270
}
271
else {
272
0
0
die("$file not found");
273
}
274
}
275
276
19
70
my %template_vars;
277
19
100
86
if ( !exists $param->{__no_sys_info__} ) {
278
13
48
%template_vars = _get_std_template_vars($param);
279
}
280
else {
281
6
19
delete $param->{__no_sys_info__};
282
6
16
%template_vars = %{$param};
6
37
283
}
284
285
# configuration variables
286
19
276
my $config_values = Rex::Config->get_all;
287
19
45
for my $key ( keys %{$config_values} ) {
19
224
288
26
100
135
if ( !exists $template_vars{$key} ) {
289
25
166
$template_vars{$key} = $config_values->{$key};
290
}
291
}
292
293
19
100
66
204
if ( Rex::CMDB::cmdb_active() && Rex::Config->get_register_cmdb_template ) {
294
4
18
my $data = Rex::CMDB::cmdb();
295
4
26
for my $key ( keys %{ $data->{value} } ) {
4
38
296
28
100
61
if ( !exists $template_vars{$key} ) {
297
26
109
$template_vars{$key} = $data->{value}->{$key};
298
}
299
}
300
}
301
302
19
329
return Rex::Config->get_template_function()->( $content, \%template_vars );
303
}
304
305
sub _get_std_template_vars {
306
13
13
31
my ($param) = @_;
307
308
13
50
41
my %merge1 = %{ $param || {} };
13
78
309
13
31
my %merge2;
310
311
13
50
55
if ( Rex::get_cache()->valid("system_information_info") ) {
312
0
0
%merge2 = %{ Rex::get_cache()->get("system_information_info") };
0
0
313
}
314
else {
315
13
60
%merge2 = Rex::Helper::System::info();
316
13
137
Rex::get_cache()->set( "system_information_info", \%merge2 );
317
}
318
319
13
405
my %template_vars = ( %merge1, %merge2 );
320
321
13
491
return %template_vars;
322
}
323
324
=head2 file($file_name [, %options])
325
326
This function is the successor of I. Please use this function to upload files to your server.
327
328
task "prepare", "server1", "server2", sub {
329
file "/file/on/remote/machine",
330
source => "/file/on/local/machine";
331
332
file "/etc/hosts",
333
content => template("templates/etc/hosts.tpl"),
334
owner => "user",
335
group => "group",
336
mode => 700,
337
on_change => sub { say "Something was changed." },
338
on_no_change => sub { say "Nothing has changed." };
339
340
file "/etc/motd",
341
content => `fortune`;
342
343
file "/etc/named.conf",
344
content => template("templates/etc/named.conf.tpl"),
345
no_overwrite => TRUE; # this file will not be overwritten if already exists.
346
347
file "/etc/httpd/conf/httpd.conf",
348
source => "/files/etc/httpd/conf/httpd.conf",
349
on_change => sub { service httpd => "restart"; };
350
351
file "/etc/named.d",
352
ensure => "directory", # this will create a directory
353
owner => "root",
354
group => "root";
355
356
file "/etc/motd",
357
ensure => "absent"; # this will remove the file or directory
358
359
};
360
361
The first parameter is either a string or an array reference. In the latter case the
362
function is called for all strings in the array. Therefore, the following constructs
363
are equivalent:
364
365
file '/tmp/test1', ensure => 'directory';
366
file '/tmp/test2', ensure => 'directory';
367
368
file [ qw( /tmp/test1 /tmp/test2 ) ], ensure => 'directory'; # use array ref
369
370
file [ glob('/tmp/test{1,2}') ], ensure => 'directory'; # explicit glob call for array contents
371
372
Use the glob carefully as B (e.g. when using wildcards).
373
374
The I is subject to a path resolution algorithm. This algorithm
375
can be configured using the I function to set the value of the
376
I variable to a hash containing path prefixes as its keys.
377
The associated values are arrays listing the prefix replacements in order
378
of (decreasing) priority.
379
380
set "path_map", {
381
"files/" => [ "files/{environment}/{hostname}/_root_/",
382
"files/{environment}/_root_/" ]
383
};
384
385
With this configuration, the file "files/etc/ntpd.conf" will be probed for
386
in the following locations:
387
388
- files/{environment}/{hostname}/_root_/etc/ntpd.conf
389
- files/{environment}/_root_/etc/ntpd.conf
390
- files/etc/ntpd.conf
391
392
Furthermore, if a path prefix matches multiple prefix entries in 'path_map',
393
e.g. "files/etc/ntpd.conf" matching both "files/" and "files/etc/", the
394
longer matching prefix(es) have precedence over shorter ones. Note that
395
keys without a trailing slash (i.e. "files/etc") will be treated as having
396
a trailing slash when matching the prefix ("files/etc/").
397
398
If no file is found using the above procedure and I is relative,
399
it will search from the location of your I or the I<.pm> file if
400
you use Perl packages.
401
402
All the possible variables ('{environment}', '{hostname}', ...) are documented
403
in the CMDB YAML documentation.
404
405
=head3 Hooks
406
407
This function supports the following L:
408
409
=over 4
410
411
=item before
412
413
This gets executed before anything is done. All original parameters are passed to it, including the applied defaults (C 'present'>, resolved path for C).
414
415
The return value of this hook overwrites the original parameters of the function call.
416
417
=item before_change
418
419
This gets executed right before the new file is written. All original parameters are passed to it, including the applied defaults (C 'present'>, resolved path for C).
420
421
=item after_change
422
423
This gets executed right after the file is written. All original parameters, including the applied defaults (C 'present'>, resolved path for C), and any returned results are passed to it.
424
425
=item after
426
427
This gets executed right before the C function returns. All original parameters, including the applied defaults (C 'present'>, resolved path for C), and any returned results are passed to it.
428
429
=back
430
431
=cut
432
433
sub file {
434
53
53
1
68086
my ( $file, @options ) = @_;
435
436
53
50
419
if ( ref $file eq "ARRAY" ) {
437
0
0
my @ret;
438
439
# $file is an array, so iterate over these files
440
0
0
for my $f ( @{$file} ) {
0
0
441
0
0
push( @ret, file( $f, @options ) );
442
}
443
444
0
0
return \@ret;
445
}
446
447
53
572
my $option = {@options};
448
449
53
438
$file = resolv_path($file);
450
451
53
242
my ($is_directory);
452
53
100
100
536
if ( exists $option->{ensure} && $option->{ensure} eq "directory" ) {
453
2
13
$is_directory = 1;
454
}
455
456
53
100
66
325
if ( exists $option->{source} && !$is_directory ) {
457
3
28
$option->{source} = resolv_path( $option->{source} );
458
}
459
460
# default: ensure = present
461
53
100
1119
$option->{ensure} ||= "present";
462
463
53
1121
my $fs = Rex::Interface::Fs->create;
464
465
53
100
100
1315
if ( $option->{ensure} ne 'absent' && $fs->is_symlink($file) ) {
466
4
111
my $original_file = $file;
467
4
123
$file = resolve_symlink($file);
468
4
262
Rex::Logger::info(
469
"$original_file is a symlink, operating on $file instead", 'warn' );
470
}
471
472
#### check and run before hook
473
eval {
474
53
212
my @new_args = Rex::Hook::run_hook( file => "before", $file, %{$option} );
53
2062
475
53
100
416
if (@new_args) {
476
3
42
( $file, @options ) = @new_args;
477
3
21
$option = {@options};
478
}
479
53
273
1;
480
53
50
584
} or do {
481
0
0
die("Before hook failed. Cancelling file() action: $@");
482
};
483
##############################
484
485
Rex::get_current_connection()->{reporter}
486
53
262
->report_resource_start( type => "file", name => $file );
487
488
53
100
100
499
my $need_md5 = ( $option->{"on_change"} && !$is_directory ? 1 : 0 );
489
53
100
40
1519
my $on_change = $option->{"on_change"} || sub { };
490
53
50
12
884
my $on_no_change = $option->{"on_no_change"} || sub { };
491
492
53
296
my $__ret = { changed => 0 };
493
494
53
142
my ( $new_md5, $old_md5 );
495
496
53
50
33
1044
if ( exists $option->{no_overwrite}
100
0
100
66
497
&& $option->{no_overwrite}
498
&& $fs->is_file($file) )
499
{
500
0
0
Rex::Logger::debug(
501
"File already exists and no_overwrite option given. Doing nothing.");
502
0
0
$__ret = { changed => 0 };
503
504
Rex::get_current_connection()->{reporter}->report(
505
0
0
changed => 0,
506
message =>
507
"File already exists and no_overwrite option given. Doing nothing."
508
);
509
}
510
511
elsif ( ( exists $option->{content} || exists $option->{source} )
512
&& !$is_directory )
513
{
514
515
# first upload file to tmp location, to get md5 sum.
516
# than we can decide if we need to replace the current (old) file.
517
518
42
670
my $tmp_file_name = get_tmp_file_name($file);
519
520
42
100
282
if ( exists $option->{content} ) {
50
521
39
251
my $fh = file_write($tmp_file_name);
522
39
1443
my @lines = split( qr{$/}, $option->{"content"} );
523
39
422
for my $line (@lines) {
524
240
1321
$fh->write( $line . $/ );
525
}
526
39
433
$fh->close;
527
}
528
elsif ( exists $option->{source} ) {
529
$option->{source} =
530
3
93
Rex::Helper::Path::get_file_path( $option->{source}, caller );
531
532
3
47
upload $option->{source}, $tmp_file_name;
533
}
534
535
# now get md5 sums
536
42
206
eval { $old_md5 = md5($file); };
42
782
537
42
378
$new_md5 = md5($tmp_file_name);
538
539
42
100
66
1562
if ( $new_md5 && $old_md5 && $new_md5 eq $old_md5 ) {
100
540
4
161
Rex::Logger::debug(
541
"No need to overwrite existing file. Old and new files are the same. $old_md5 eq $new_md5."
542
);
543
544
# md5 sums are the same, delete tmp.
545
4
155
$fs->unlink($tmp_file_name);
546
4
33
$need_md5 = 0; # we don't need to execute on_change hook
547
548
Rex::get_current_connection()->{reporter}->report(
549
4
94
changed => 0,
550
message =>
551
"No need to overwrite existing file. Old and new files are the same. $old_md5 eq $new_md5."
552
);
553
}
554
else {
555
38
100
449
$old_md5 ||= "";
556
38
718
Rex::Logger::debug(
557
"Need to use the new file. md5 sums are different. <<$old_md5>> = <<$new_md5>>"
558
);
559
560
#### check and run before_change hook
561
38
189
Rex::Hook::run_hook( file => "before_change", $file, %{$option} );
38
1681
562
##############################
563
564
38
50
444
if (Rex::is_sudo) {
565
0
0
my $current_options =
566
Rex::get_current_connection_object()->get_current_sudo_options;
567
0
0
Rex::get_current_connection_object()->push_sudo_options( {} );
568
569
0
0
0
if ( exists $current_options->{user} ) {
570
0
0
$fs->chown( "$current_options->{user}:", $tmp_file_name );
571
}
572
}
573
574
38
1018
$fs->rename( $tmp_file_name, $file );
575
38
50
596
Rex::get_current_connection_object()->pop_sudo_options()
576
if (Rex::is_sudo);
577
578
38
1244
$__ret = { changed => 1 };
579
580
Rex::get_current_connection()->{reporter}->report(
581
38
446
changed => 1,
582
message => "File updated. old md5: $old_md5, new md5: $new_md5"
583
);
584
585
#### check and run after_change hook
586
38
240
Rex::Hook::run_hook( file => "after_change", $file, %{$option}, $__ret );
38
1471
587
##############################
588
589
}
590
591
}
592
593
53
50
325
if ( exists $option->{"ensure"} ) {
594
53
100
430
if ( $option->{ensure} eq "present" ) {
100
50
595
49
100
641
if ( !$fs->is_file($file) ) {
100
596
597
#### check and run before_change hook
598
1
7
Rex::Hook::run_hook( file => "before_change", $file, %{$option} );
1
10
599
##############################
600
601
1
16
my $fh = file_write($file);
602
1
11
$fh->write("");
603
1
10
$fh->close;
604
1
6
$__ret = { changed => 1 };
605
606
Rex::get_current_connection()->{reporter}->report(
607
1
5
changed => 1,
608
message => "file is now present, with no content",
609
);
610
611
#### check and run after_change hook
612
Rex::Hook::run_hook(
613
file => "after_change",
614
1
5
$file, %{$option}, $__ret
1
12
615
);
616
##############################
617
618
}
619
elsif ( !$__ret->{changed} ) {
620
10
149
$__ret = { changed => 0 };
621
10
173
Rex::get_current_connection()->{reporter}->report( changed => 0, );
622
}
623
}
624
elsif ( $option->{ensure} eq "absent" ) {
625
2
41
$need_md5 = 0;
626
627
#### check and run before_change hook
628
2
13
Rex::Hook::run_hook( file => "before_change", $file, %{$option} );
2
31
629
##############################
630
631
2
50
34
if ( $fs->is_file($file) ) {
0
632
2
185
$fs->unlink($file);
633
2
13
$__ret = { changed => 1 };
634
Rex::get_current_connection()->{reporter}->report(
635
2
24
changed => 1,
636
message => "File removed."
637
);
638
}
639
elsif ( $fs->is_dir($file) ) {
640
0
0
$fs->rmdir($file);
641
0
0
$__ret = { changed => 1 };
642
Rex::get_current_connection()->{reporter}->report(
643
0
0
changed => 1,
644
message => "Directory removed.",
645
);
646
}
647
else {
648
0
0
$__ret = { changed => 0 };
649
0
0
Rex::get_current_connection()->{reporter}->report( changed => 0, );
650
}
651
652
#### check and run after_change hook
653
2
10
Rex::Hook::run_hook( file => "after_change", $file, %{$option}, $__ret );
2
15
654
##############################
655
656
}
657
elsif ( $option->{ensure} eq "directory" ) {
658
2
66
Rex::Logger::debug("file() should be a directory");
659
2
15
my %dir_option;
660
2
50
23
if ( exists $option->{owner} ) {
661
0
0
$dir_option{owner} = $option->{owner};
662
}
663
2
50
25
if ( exists $option->{group} ) {
664
0
0
$dir_option{group} = $option->{group};
665
}
666
2
50
17
if ( exists $option->{mode} ) {
667
0
0
$dir_option{mode} = $option->{mode};
668
}
669
670
2
46
Rex::Commands::Fs::mkdir( $file, %dir_option, on_change => $on_change );
671
}
672
}
673
674
53
100
100
696
if ( !exists $option->{content}
100
675
&& !exists $option->{source}
676
&& $option->{ensure} ne "absent" )
677
{
678
679
# no content and no source, so just verify that the file is present
680
9
50
66
63
if ( !$fs->is_file($file) && !$is_directory ) {
681
682
#### check and run before_change hook
683
0
0
Rex::Hook::run_hook( file => "before_change", $file, %{$option} );
0
0
684
##############################
685
686
0
0
my $fh = file_write($file);
687
0
0
$fh->write("");
688
0
0
$fh->close;
689
690
0
0
my $f_type = "file is now present, with no content";
691
0
0
0
0
if ( exists $option->{ensure} && $option->{ensure} eq "directory" ) {
692
0
0
$f_type = "directory is now present";
693
}
694
695
Rex::get_current_connection()->{reporter}->report(
696
0
0
changed => 1,
697
message => $f_type,
698
);
699
700
#### check and run after_change hook
701
0
0
Rex::Hook::run_hook( file => "after_change", $file, %{$option}, $__ret );
0
0
702
##############################
703
704
}
705
}
706
707
53
100
439
if ( $option->{ensure} ne "absent" ) {
708
709
51
100
225
if ($need_md5) {
710
2
25
eval { $new_md5 = md5($file); };
2
51
711
}
712
51
453
my %stat_old = $fs->stat($file);
713
714
51
100
537
if ( exists $option->{"mode"} ) {
715
28
1044
$fs->chmod( $option->{"mode"}, $file );
716
}
717
718
51
100
919
if ( exists $option->{"group"} ) {
719
25
781
$fs->chgrp( $option->{"group"}, $file );
720
}
721
722
51
100
844
if ( exists $option->{"owner"} ) {
723
25
758
$fs->chown( $option->{"owner"}, $file );
724
}
725
726
51
1289
my %stat_new = $fs->stat($file);
727
728
51
100
33
1432
if ( %stat_old && %stat_new && $stat_old{mode} ne $stat_new{mode} ) {
66
729
Rex::get_current_connection()->{reporter}->report(
730
15
111
changed => 1,
731
message =>
732
"File-System permissions changed from $stat_old{mode} to $stat_new{mode}.",
733
);
734
}
735
736
51
50
33
1353
if ( %stat_old && %stat_new && $stat_old{uid} ne $stat_new{uid} ) {
33
737
Rex::get_current_connection()->{reporter}->report(
738
0
0
changed => 1,
739
message => "Owner changed from $stat_old{uid} to $stat_new{uid}.",
740
);
741
}
742
743
51
50
33
1500
if ( %stat_old && %stat_new && $stat_old{gid} ne $stat_new{gid} ) {
33
744
Rex::get_current_connection()->{reporter}->report(
745
0
0
changed => 1,
746
message => "Group changed from $stat_old{gid} to $stat_new{gid}.",
747
);
748
}
749
750
}
751
752
53
283
my $on_change_done = 0;
753
754
53
100
225
if ($need_md5) {
755
2
0
33
49
unless ( $old_md5 && $new_md5 && $old_md5 eq $new_md5 ) {
33
756
2
50
54
$old_md5 ||= "";
757
2
50
22
$new_md5 ||= "";
758
759
2
36
Rex::Logger::debug("File $file has been changed... Running on_change");
760
2
25
Rex::Logger::debug("old: $old_md5");
761
2
30
Rex::Logger::debug("new: $new_md5");
762
763
2
38
&$on_change($file);
764
765
2
31
$on_change_done = 1;
766
767
Rex::get_current_connection()->{reporter}->report(
768
2
28
changed => 1,
769
message => "Content changed.",
770
);
771
772
2
24
$__ret = { changed => 1 };
773
}
774
}
775
776
53
100
100
743
if ( $__ret->{changed} == 1 && $on_change_done == 0 ) {
100
777
39
429
&$on_change($file);
778
}
779
elsif ( $__ret->{changed} == 0 ) {
780
12
332
Rex::Logger::debug(
781
"File $file has not been changed... Running on_no_change");
782
12
155
&$on_no_change($file);
783
}
784
785
#### check and run after hook
786
53
201
Rex::Hook::run_hook( file => "after", $file, %{$option}, $__ret );
53
1376
787
##############################
788
789
Rex::get_current_connection()->{reporter}
790
53
264
->report_resource_end( type => "file", name => $file );
791
792
53
3378
return $__ret->{changed};
793
}
794
795
sub get_tmp_file_name {
796
44
44
0
2513
my $file = shift;
797
798
44
6254
my $dirname = dirname($file);
799
44
1989
my $filename = ".rex.tmp." . basename($file);
800
801
44
100
1958
my $tmp_file_name =
802
$dirname eq '.'
803
? $filename
804
: Rex::Helper::File::Spec->catfile( $dirname, $filename );
805
806
44
316
return $tmp_file_name;
807
}
808
809
=head2 file_write($file_name)
810
811
This function opens a file for writing (it will truncate the file if it already exists). It returns a Rex::FS::File object on success.
812
813
On failure it will die.
814
815
my $fh;
816
eval {
817
$fh = file_write("/etc/groups");
818
};
819
820
# catch an error
821
if($@) {
822
print "An error occurred. $@.\n";
823
}
824
825
# work with the filehandle
826
$fh->write("...");
827
$fh->close;
828
829
=cut
830
831
sub file_write {
832
42
42
1
3100
my ($file) = @_;
833
42
705
$file = resolv_path($file);
834
835
42
986
Rex::Logger::debug("Opening file: $file for writing.");
836
837
42
1182
my $fh = Rex::Interface::File->create;
838
42
50
449
if ( !$fh->open( ">", $file ) ) {
839
0
0
Rex::Logger::debug("Can't open $file for writing.");
840
0
0
die("Can't open $file for writing.");
841
}
842
843
42
968
return Rex::FS::File->new( fh => $fh );
844
}
845
846
=head2 file_append($file_name)
847
848
=cut
849
850
sub file_append {
851
1
1
1
2181
my ($file) = @_;
852
1
50
$file = resolv_path($file);
853
854
1
47
Rex::Logger::debug("Opening file: $file for appending.");
855
856
1
57
my $fh = Rex::Interface::File->create;
857
858
1
50
41
if ( !$fh->open( ">>", $file ) ) {
859
0
0
Rex::Logger::debug("Can't open $file for appending.");
860
0
0
die("Can't open $file for appending.");
861
}
862
863
1
51
return Rex::FS::File->new( fh => $fh );
864
}
865
866
=head2 file_read($file_name)
867
868
This function opens a file for reading. It returns a Rex::FS::File object on success.
869
870
On failure it will die.
871
872
my $fh;
873
eval {
874
$fh = file_read("/etc/groups");
875
};
876
877
# catch an error
878
if($@) {
879
print "An error occurred. $@.\n";
880
}
881
882
# work with the filehandle
883
my $content = $fh->read_all;
884
$fh->close;
885
886
=cut
887
888
sub file_read {
889
137
137
1
1262
my ($file) = @_;
890
137
1247
$file = resolv_path($file);
891
892
137
3015
Rex::Logger::debug("Opening file: $file for reading.");
893
894
137
4680
my $fh = Rex::Interface::File->create;
895
896
137
50
1188
if ( !$fh->open( "<", $file ) ) {
897
0
0
Rex::Logger::debug("Can't open $file for reading.");
898
0
0
die("Can't open $file for reading.");
899
}
900
901
137
3665
return Rex::FS::File->new( fh => $fh );
902
}
903
904
=head2 cat($file_name)
905
906
This function returns the complete content of $file_name as a string.
907
908
print cat "/etc/passwd";
909
910
=cut
911
912
sub cat {
913
86
86
1
14769
my ($file) = @_;
914
86
1119
$file = resolv_path($file);
915
916
86
939
my $fh = file_read($file);
917
86
50
367
unless ($fh) {
918
0
0
die("Can't open $file for reading");
919
}
920
86
757
my $content = $fh->read_all;
921
86
704
$fh->close;
922
923
86
944
return $content;
924
}
925
926
=head2 delete_lines_matching($file, $regexp)
927
928
Delete lines that match $regexp in $file.
929
930
task "clean-logs", sub {
931
delete_lines_matching "/var/log/auth.log" => "root";
932
};
933
934
=cut
935
936
sub delete_lines_matching {
937
1
1
1
1764
my ( $file, @m ) = @_;
938
1
31
$file = resolv_path($file);
939
940
Rex::get_current_connection()->{reporter}
941
1
24
->report_resource_start( type => "delete_lines_matching", name => $file );
942
943
1
12
for (@m) {
944
1
50
19
if ( ref($_) ne "Regexp" ) {
945
1
49
$_ = qr{\Q$_\E};
946
}
947
}
948
949
1
46
my $fs = Rex::Interface::Fs->create;
950
951
1
25
my %stat = $fs->stat($file);
952
953
1
50
26
if ( !$fs->is_file($file) ) {
954
0
0
Rex::Logger::info("File: $file not found.");
955
0
0
die("$file not found");
956
}
957
958
1
50
21
if ( !$fs->is_writable($file) ) {
959
0
0
Rex::Logger::info("File: $file not writable.");
960
0
0
die("$file not writable");
961
}
962
963
1
11
my $nl = $/;
964
1
36
my @content = split( /$nl/, cat($file) );
965
966
1
13
my $old_md5 = "";
967
1
7
eval { $old_md5 = md5($file); };
1
27
968
969
1
38
my @new_content;
970
971
OUT:
972
1
20
for my $line (@content) {
973
IN:
974
1
14
for my $match (@m) {
975
1
50
62
if ( $line =~ $match ) {
976
1
24
next OUT;
977
}
978
}
979
980
0
0
push @new_content, $line;
981
}
982
983
file $file,
984
content => join( $nl, @new_content ),
985
owner => $stat{uid},
986
group => $stat{gid},
987
1
55
mode => $stat{mode};
988
989
1
21
my $new_md5 = "";
990
1
20
eval { $new_md5 = md5($file); };
1
41
991
992
1
50
55
if ( $new_md5 ne $old_md5 ) {
993
Rex::get_current_connection()->{reporter}->report(
994
1
52
changed => 1,
995
message => "Content changed.",
996
);
997
}
998
else {
999
0
0
Rex::get_current_connection()->{reporter}->report( changed => 0, );
1000
}
1001
1002
Rex::get_current_connection()->{reporter}
1003
1
26
->report_resource_end( type => "delete_lines_matching", name => $file );
1004
}
1005
1006
=head2 delete_lines_according_to($search, $file [, @options])
1007
1008
This is the successor of the delete_lines_matching() function. This function also allows the usage of on_change and on_no_change hooks.
1009
1010
It will search for $search in $file and remove the found lines. If on_change hook is present it will execute this if the file was changed.
1011
1012
task "cleanup", "server1", sub {
1013
delete_lines_according_to qr{^foo:}, "/etc/passwd",
1014
on_change => sub {
1015
say "removed user foo.";
1016
};
1017
};
1018
1019
=cut
1020
1021
sub delete_lines_according_to {
1022
0
0
1
0
my ( $search, $file, @options ) = @_;
1023
0
0
$file = resolv_path($file);
1024
1025
0
0
my $option = {@options};
1026
0
0
0
my $on_change = $option->{on_change} || undef;
1027
0
0
0
my $on_no_change = $option->{on_no_change} || undef;
1028
1029
0
0
my ( $old_md5, $new_md5 );
1030
1031
0
0
0
if ($on_change) {
1032
0
0
$old_md5 = md5($file);
1033
}
1034
1035
0
0
delete_lines_matching( $file, $search );
1036
1037
0
0
0
0
if ( $on_change || $on_no_change ) {
1038
0
0
$new_md5 = md5($file);
1039
1040
0
0
0
if ( $old_md5 ne $new_md5 ) {
1041
0
0
0
&$on_change($file) if $on_change;
1042
}
1043
else {
1044
0
0
0
&$on_no_change($file) if $on_no_change;
1045
}
1046
}
1047
1048
}
1049
1050
=head2 append_if_no_such_line($file, $new_line [, @regexp])
1051
1052
Append $new_line to $file if none in @regexp is found. If no regexp is
1053
supplied, the line is appended unless there is already an identical line
1054
in $file.
1055
1056
task "add-group", sub {
1057
append_if_no_such_line "/etc/groups", "mygroup:*:100:myuser1,myuser2", on_change => sub { service sshd => "restart"; };
1058
};
1059
1060
Since 0.42 you can use named parameters as well
1061
1062
task "add-group", sub {
1063
append_if_no_such_line "/etc/groups",
1064
line => "mygroup:*:100:myuser1,myuser2",
1065
regexp => qr{^mygroup},
1066
on_change => sub {
1067
say "file was changed, do something.";
1068
};
1069
1070
append_if_no_such_line "/etc/groups",
1071
line => "mygroup:*:100:myuser1,myuser2",
1072
regexp => [qr{^mygroup:}, qr{^ourgroup:}]; # this is an OR
1073
};
1074
1075
=cut
1076
1077
sub append_if_no_such_line {
1078
14
14
1
29230
_append_or_update( 'append_if_no_such_line', @_ );
1079
}
1080
1081
=head2 append_or_amend_line($file, $line [, @regexp])
1082
1083
Similar to L, but if the line in the regexp is
1084
found, it will be updated. Otherwise, it will be appended.
1085
1086
task "update-group", sub {
1087
append_or_amend_line "/etc/groups",
1088
line => "mygroup:*:100:myuser3,myuser4",
1089
regexp => qr{^mygroup},
1090
on_change => sub {
1091
say "file was changed, do something.";
1092
},
1093
on_no_change => sub {
1094
say "file was not changed, do something.";
1095
};
1096
};
1097
1098
=cut
1099
1100
sub append_or_amend_line {
1101
5
5
1
11829
_append_or_update( 'append_or_amend_line', @_ );
1102
}
1103
1104
sub _append_or_update {
1105
19
19
182
my $action = shift;
1106
19
69
my $file = shift;
1107
1108
19
118
$file = resolv_path($file);
1109
19
124
my ( $new_line, @m );
1110
1111
# check if parameters are in key => value format
1112
19
0
my ( $option, $on_change, $on_no_change );
1113
1114
Rex::get_current_connection()->{reporter}
1115
19
103
->report_resource_start( type => $action, name => $file );
1116
1117
eval {
1118
45
45
489
no warnings;
45
119
45
74446
1119
19
207
$option = {@_};
1120
1121
# if there is no line parameter, it is the old parameter format
1122
# so go dieing
1123
19
100
130
if ( !exists $option->{line} ) {
1124
4
73
die;
1125
}
1126
15
67
$new_line = $option->{line};
1127
15
100
100
357
if ( exists $option->{regexp} && ref $option->{regexp} eq "Regexp" ) {
100
1128
10
57
@m = ( $option->{regexp} );
1129
}
1130
elsif ( ref $option->{regexp} eq "ARRAY" ) {
1131
2
9
@m = @{ $option->{regexp} };
2
20
1132
}
1133
15
50
196
$on_change = $option->{on_change} || undef;
1134
15
50
135
$on_no_change = $option->{on_no_change} || undef;
1135
15
226
1;
1136
19
100
90
} or do {
1137
4
30
( $new_line, @m ) = @_;
1138
1139
# check if something in @m (the regexpes) is named on_change or on_no_change
1140
4
62
for my $option ( [ on_change => \$on_change ],
1141
[ on_no_change => \$on_no_change ] )
1142
{
1143
8
33
for ( my $i = 0 ; $i < $#m ; $i++ ) {
1144
8
100
66
70
if ( $m[$i] eq $option->[0] && ref( $m[ $i + 1 ] ) eq "CODE" ) {
1145
5
11
${ $option->[1] } = $m[ $i + 1 ];
5
10
1146
5
11
splice( @m, $i, 2 );
1147
5
17
last;
1148
}
1149
}
1150
}
1151
};
1152
1153
19
50
101
unless ( defined $new_line ) {
1154
0
0
my ( undef, undef, undef, $subroutine ) = caller(1);
1155
0
0
$subroutine =~ s/^.*:://;
1156
0
0
die "Undefined new line while trying to run $subroutine on $file";
1157
}
1158
1159
19
558
my $fs = Rex::Interface::Fs->create;
1160
1161
19
136
my %stat = $fs->stat($file);
1162
1163
19
96
my ( $old_md5, $ret );
1164
19
127
$old_md5 = md5($file);
1165
1166
# slow but secure way
1167
19
213
my $content;
1168
eval {
1169
19
466
$content = [ split( /\n/, cat($file) ) ];
1170
19
173
1;
1171
19
50
259
} or do {
1172
0
0
$ret = 1;
1173
};
1174
1175
19
100
180
if ( !@m ) {
1176
5
315
push @m, qr{\Q$new_line\E};
1177
}
1178
1179
19
88
my $found;
1180
19
68
for my $line ( 0 .. $#{$content} ) {
19
250
1181
129
321
for my $match (@m) {
1182
144
50
497
if ( ref($match) ne "Regexp" ) {
1183
0
0
$match = qr{$match};
1184
}
1185
144
100
1122
if ( $content->[$line] =~ $match ) {
1186
9
94
$found = 1;
1187
9
100
125
last if $action eq 'append_if_no_such_line';
1188
3
40
$content->[$line] = "$new_line";
1189
}
1190
}
1191
}
1192
1193
19
78
my $new_md5;
1194
19
100
100
355
if ( $action eq 'append_if_no_such_line' && $found ) {
1195
6
69
$new_md5 = $old_md5;
1196
}
1197
else {
1198
13
100
169
push @$content, "$new_line" unless $found;
1199
1200
file $file,
1201
content => join( "\n", @$content ),
1202
owner => $stat{uid},
1203
group => $stat{gid},
1204
13
437
mode => $stat{mode};
1205
13
389
$new_md5 = md5($file);
1206
}
1207
1208
19
100
66
768
if ( $on_change || $on_no_change ) {
1209
3
100
33
223
if ( $old_md5 && $new_md5 && $old_md5 ne $new_md5 ) {
50
66
1210
1
50
32
if ($on_change) {
1211
1
50
38
$old_md5 ||= "";
1212
1
50
29
$new_md5 ||= "";
1213
1214
1
98
Rex::Logger::debug("File $file has been changed... Running on_change");
1215
1
34
Rex::Logger::debug("old: $old_md5");
1216
1
26
Rex::Logger::debug("new: $new_md5");
1217
1
64
&$on_change($file);
1218
}
1219
}
1220
elsif ($on_no_change) {
1221
2
50
31
$new_md5 ||= "";
1222
1223
2
29
Rex::Logger::debug(
1224
"File $file has not been changed (md5 $new_md5)... Running on_no_change"
1225
);
1226
2
33
&$on_no_change($file);
1227
}
1228
}
1229
1230
19
100
33
729
if ( $old_md5 && $new_md5 && $old_md5 ne $new_md5 ) {
66
1231
Rex::get_current_connection()->{reporter}->report(
1232
13
138
changed => 1,
1233
message => "Content changed.",
1234
);
1235
}
1236
else {
1237
6
60
Rex::get_current_connection()->{reporter}->report( changed => 0, );
1238
}
1239
1240
Rex::get_current_connection()->{reporter}
1241
19
184
->report_resource_end( type => $action, name => $file );
1242
}
1243
1244
=head2 extract($file [, %options])
1245
1246
This function extracts a file. The target directory optionally specified with the `to` option will be created automatically.
1247
1248
Supported formats are .box, .tar, .tar.gz, .tgz, .tar.Z, .tar.bz2, .tbz2, .zip, .gz, .bz2, .war, .jar.
1249
1250
task prepare => sub {
1251
extract "/tmp/myfile.tar.gz",
1252
owner => "root",
1253
group => "root",
1254
to => "/etc";
1255
1256
extract "/tmp/foo.tgz",
1257
type => "tgz",
1258
mode => "g+rwX";
1259
};
1260
1261
Can use the type=> option if the file suffix has been changed. (types are tar, tgz, tbz, zip, gz, bz2)
1262
1263
=cut
1264
1265
sub extract {
1266
0
0
1
0
my ( $file, %option ) = @_;
1267
0
0
$file = resolv_path($file);
1268
1269
0
0
my $pre_cmd = "";
1270
0
0
my $to = ".";
1271
0
0
my $type = "";
1272
1273
0
0
0
if ( $option{chdir} ) {
1274
0
0
$to = $option{chdir};
1275
}
1276
1277
0
0
0
if ( $option{to} ) {
1278
0
0
$to = $option{to};
1279
}
1280
0
0
$to = resolv_path($to);
1281
1282
0
0
0
if ( $option{type} ) {
1283
0
0
$type = $option{type};
1284
}
1285
1286
0
0
Rex::Commands::Fs::mkdir($to);
1287
0
0
$pre_cmd = "cd $to; ";
1288
1289
0
0
my $exec = Rex::Interface::Exec->create;
1290
0
0
my $cmd = "";
1291
1292
0
0
0
0
if ( $type eq 'tgz'
0
0
0
0
0
0
0
0
0
0
0
0
0
1293
|| $file =~ m/\.tar\.gz$/
1294
|| $file =~ m/\.tgz$/
1295
|| $file =~ m/\.tar\.Z$/ )
1296
{
1297
0
0
$cmd = "${pre_cmd}gunzip -c $file | tar -xf -";
1298
}
1299
elsif ( $type eq 'tbz' || $file =~ m/\.tar\.bz2/ || $file =~ m/\.tbz2/ ) {
1300
0
0
$cmd = "${pre_cmd}bunzip2 -c $file | tar -xf -";
1301
}
1302
elsif ( $type eq 'tar' || $file =~ m/\.(tar|box)/ ) {
1303
0
0
$cmd = "${pre_cmd}tar -xf $file";
1304
}
1305
elsif ( $type eq 'zip' || $file =~ m/\.(zip|war|jar)$/ ) {
1306
0
0
$cmd = "${pre_cmd}unzip -o $file";
1307
}
1308
elsif ( $type eq 'gz' || $file =~ m/\.gz$/ ) {
1309
0
0
$cmd = "${pre_cmd}gunzip -f $file";
1310
}
1311
elsif ( $type eq 'bz2' || $file =~ m/\.bz2$/ ) {
1312
0
0
$cmd = "${pre_cmd}bunzip2 -f $file";
1313
}
1314
else {
1315
0
0
Rex::Logger::info("File not supported.");
1316
0
0
die("File ($file) not supported.");
1317
}
1318
1319
0
0
$exec->exec($cmd);
1320
1321
0
0
my $fs = Rex::Interface::Fs->create;
1322
0
0
0
if ( $option{owner} ) {
1323
0
0
$fs->chown( $option{owner}, $to, recursive => 1 );
1324
}
1325
1326
0
0
0
if ( $option{group} ) {
1327
0
0
$fs->chgrp( $option{group}, $to, recursive => 1 );
1328
}
1329
1330
0
0
0
if ( $option{mode} ) {
1331
0
0
$fs->chmod( $option{mode}, $to, recursive => 1 );
1332
}
1333
1334
}
1335
1336
=head2 sed($search, $replace, $file [, %options])
1337
1338
Search some string in a file and replace it.
1339
1340
task sar => sub {
1341
# this will work line by line
1342
sed qr{search}, "replace", "/var/log/auth.log";
1343
1344
# to use it in a multiline way
1345
sed qr{search}, "replace", "/var/log/auth.log",
1346
multiline => TRUE;
1347
};
1348
1349
Like similar file management commands, it also supports C and C hooks.
1350
1351
=cut
1352
1353
sub sed {
1354
11
11
1
17806
my ( $search, $replace, $file, @option ) = @_;
1355
11
185
$file = resolv_path($file);
1356
11
50
my $options = {};
1357
1358
Rex::get_current_connection()->{reporter}
1359
11
74
->report_resource_start( type => "sed", name => $file );
1360
1361
11
50
75
if ( ref( $option[0] ) ) {
1362
0
0
$options = $option[0];
1363
}
1364
else {
1365
11
59
$options = {@option};
1366
}
1367
1368
11
50
175
my $on_change = $options->{"on_change"} || undef;
1369
11
50
134
my $on_no_change = $options->{"on_no_change"} || undef;
1370
1371
11
44
my @content;
1372
1373
11
100
54
if ( exists $options->{multiline} ) {
1374
1
31
$content[0] = cat($file);
1375
1
33
$content[0] =~ s/$search/$replace/gms;
1376
}
1377
else {
1378
10
81
@content = split( /\n/, cat($file) );
1379
10
64
for (@content) {
1380
109
481
s/$search/$replace/;
1381
}
1382
}
1383
1384
11
297
my $fs = Rex::Interface::Fs->create;
1385
11
66
my %stat = $fs->stat($file);
1386
1387
my $ret = file(
1388
$file,
1389
content => join( "\n", @content ),
1390
on_change => $on_change,
1391
on_no_change => $on_no_change,
1392
owner => $stat{uid},
1393
group => $stat{gid},
1394
mode => $stat{mode}
1395
11
180
);
1396
1397
Rex::get_current_connection()->{reporter}
1398
11
209
->report_resource_end( type => "sed", name => $file );
1399
1400
11
680
return $ret;
1401
}
1402
1403
1;