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
247306
use v5.12.5;
45
437
61
45
45
240
use warnings;
45
94
45
1242
62
45
45
222
use Fcntl;
45
115
45
11327
63
64
our $VERSION = '1.14.3'; # VERSION
65
66
require Rex::Exporter;
67
45
45
2173
use Data::Dumper;
45
19846
45
2223
68
45
45
2369
use Rex::Config;
45
93
45
1534
69
45
45
3259
use Rex::FS::File;
45
112
45
412
70
45
45
1544
use Rex::Commands::Upload;
45
106
45
318
71
45
45
277
use Rex::Commands::MD5;
45
112
45
261
72
45
45
468
use Rex::File::Parser::Data;
45
122
45
564
73
45
45
1324
use Rex::Helper::File::Spec;
45
110
45
264
74
45
45
1050
use Rex::Helper::System;
45
102
45
396
75
45
45
1156
use Rex::Helper::Path;
45
95
45
3340
76
45
45
250
use Rex::Hook;
45
93
45
1595
77
45
45
245
use Carp;
45
88
45
2180
78
79
45
45
263
use Rex::Interface::Exec;
45
121
45
511
80
45
45
1056
use Rex::Interface::File;
45
90
45
321
81
45
45
1187
use Rex::Interface::Fs;
45
93
45
258
82
require Rex::CMDB;
83
84
45
45
1918
use File::Basename qw(dirname basename);
45
104
45
2395
85
86
45
45
264
use vars qw(@EXPORT);
45
98
45
2009
87
45
45
272
use base qw(Rex::Exporter);
45
104
45
4595
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
328
use vars qw(%file_handles);
45
114
45
174382
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
7472
my ( $file, @params ) = @_;
197
19
47
my $param;
198
199
19
100
108
if ( ref $params[0] eq "HASH" ) {
200
3
12
$param = $params[0];
201
}
202
else {
203
16
61
$param = {@params};
204
}
205
206
19
50
111
if ( !exists $param->{server} ) {
207
19
146
$param->{server} = Rex::Commands::connection()->server;
208
}
209
210
19
51
my $content;
211
19
100
66
236
if ( ref $file && ref $file eq 'SCALAR' ) {
212
16
42
$content = ${$file};
16
90
213
}
214
else {
215
3
11
$file = resolv_path($file);
216
217
3
100
66
19
unless ( $file =~ m/^\// || $file =~ m/^\@/ ) {
218
219
# path is relative and no template
220
1
7
Rex::Logger::debug("Relativ path $file");
221
222
1
7
$file = Rex::Helper::Path::get_file_path( $file, caller() );
223
224
1
7
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
56
if ( -f $file ) {
50
239
1
3
$content = eval { local ( @ARGV, $/ ) = ($file); <>; };
1
7
1
127
240
}
241
elsif ( $file =~ m/^\@/ ) {
242
2
21
my @caller = caller(0);
243
244
2
11
my $file_path = Rex::get_module_path( $caller[0] );
245
246
2
50
18
if ( !-f $file_path ) {
247
2
8
my ($mod_name) = ( $caller[0] =~ m/^.*::(.*?)$/ );
248
2
50
33
115
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
8
$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
4
my $file_content = eval { local ( @ARGV, $/ ) = ($file_path); <>; };
2
13
2
142
266
2
20
my ($data) = ( $file_content =~ m/.*__DATA__(.*)/ms );
267
2
24
my $fp = Rex::File::Parser::Data->new( data => [ split( /\n/, $data ) ] );
268
2
6
my $snippet_to_read = substr( $file, 1 );
269
2
7
$content = $fp->read($snippet_to_read);
270
}
271
else {
272
0
0
die("$file not found");
273
}
274
}
275
276
19
52
my %template_vars;
277
19
100
89
if ( !exists $param->{__no_sys_info__} ) {
278
13
47
%template_vars = _get_std_template_vars($param);
279
}
280
else {
281
6
30
delete $param->{__no_sys_info__};
282
6
19
%template_vars = %{$param};
6
33
283
}
284
285
# configuration variables
286
19
294
my $config_values = Rex::Config->get_all;
287
19
81
for my $key ( keys %{$config_values} ) {
19
224
288
26
100
116
if ( !exists $template_vars{$key} ) {
289
25
183
$template_vars{$key} = $config_values->{$key};
290
}
291
}
292
293
19
100
66
234
if ( Rex::CMDB::cmdb_active() && Rex::Config->get_register_cmdb_template ) {
294
4
22
my $data = Rex::CMDB::cmdb();
295
4
25
for my $key ( keys %{ $data->{value} } ) {
4
38
296
28
100
70
if ( !exists $template_vars{$key} ) {
297
26
73
$template_vars{$key} = $data->{value}->{$key};
298
}
299
}
300
}
301
302
19
292
return Rex::Config->get_template_function()->( $content, \%template_vars );
303
}
304
305
sub _get_std_template_vars {
306
13
13
32
my ($param) = @_;
307
308
13
50
29
my %merge1 = %{ $param || {} };
13
125
309
13
44
my %merge2;
310
311
13
50
53
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
377
my %template_vars = ( %merge1, %merge2 );
320
321
13
584
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
62450
my ( $file, @options ) = @_;
435
436
53
50
380
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
545
my $option = {@options};
448
449
53
411
$file = resolv_path($file);
450
451
53
237
my ($is_directory);
452
53
100
100
568
if ( exists $option->{ensure} && $option->{ensure} eq "directory" ) {
453
2
14
$is_directory = 1;
454
}
455
456
53
100
66
423
if ( exists $option->{source} && !$is_directory ) {
457
3
32
$option->{source} = resolv_path( $option->{source} );
458
}
459
460
# default: ensure = present
461
53
100
893
$option->{ensure} ||= "present";
462
463
53
1011
my $fs = Rex::Interface::Fs->create;
464
465
53
100
100
981
if ( $option->{ensure} ne 'absent' && $fs->is_symlink($file) ) {
466
4
71
my $original_file = $file;
467
4
112
$file = resolve_symlink($file);
468
4
167
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
270
my @new_args = Rex::Hook::run_hook( file => "before", $file, %{$option} );
53
1852
475
53
100
279
if (@new_args) {
476
3
36
( $file, @options ) = @new_args;
477
3
23
$option = {@options};
478
}
479
53
262
1;
480
53
50
468
} or do {
481
0
0
die("Before hook failed. Cancelling file() action: $@");
482
};
483
##############################
484
485
Rex::get_current_connection()->{reporter}
486
53
215
->report_resource_start( type => "file", name => $file );
487
488
53
100
100
555
my $need_md5 = ( $option->{"on_change"} && !$is_directory ? 1 : 0 );
489
53
100
40
1494
my $on_change = $option->{"on_change"} || sub { };
490
53
50
12
891
my $on_no_change = $option->{"on_no_change"} || sub { };
491
492
53
267
my $__ret = { changed => 0 };
493
494
53
244
my ( $new_md5, $old_md5 );
495
496
53
50
33
1004
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
582
my $tmp_file_name = get_tmp_file_name($file);
519
520
42
100
259
if ( exists $option->{content} ) {
50
521
39
197
my $fh = file_write($tmp_file_name);
522
39
1317
my @lines = split( qr{$/}, $option->{"content"} );
523
39
366
for my $line (@lines) {
524
240
1259
$fh->write( $line . $/ );
525
}
526
39
337
$fh->close;
527
}
528
elsif ( exists $option->{source} ) {
529
$option->{source} =
530
3
108
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
187
eval { $old_md5 = md5($file); };
42
690
537
42
363
$new_md5 = md5($tmp_file_name);
538
539
42
100
66
1355
if ( $new_md5 && $old_md5 && $new_md5 eq $old_md5 ) {
100
540
4
149
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
128
$fs->unlink($tmp_file_name);
546
4
27
$need_md5 = 0; # we don't need to execute on_change hook
547
548
Rex::get_current_connection()->{reporter}->report(
549
4
73
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
385
$old_md5 ||= "";
556
38
369
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
160
Rex::Hook::run_hook( file => "before_change", $file, %{$option} );
38
1515
562
##############################
563
564
38
50
360
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
822
$fs->rename( $tmp_file_name, $file );
575
38
50
539
Rex::get_current_connection_object()->pop_sudo_options()
576
if (Rex::is_sudo);
577
578
38
987
$__ret = { changed => 1 };
579
580
Rex::get_current_connection()->{reporter}->report(
581
38
428
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
197
Rex::Hook::run_hook( file => "after_change", $file, %{$option}, $__ret );
38
1349
587
##############################
588
589
}
590
591
}
592
593
53
50
488
if ( exists $option->{"ensure"} ) {
594
53
100
361
if ( $option->{ensure} eq "present" ) {
100
50
595
49
100
571
if ( !$fs->is_file($file) ) {
100
596
597
#### check and run before_change hook
598
1
6
Rex::Hook::run_hook( file => "before_change", $file, %{$option} );
1
14
599
##############################
600
601
1
20
my $fh = file_write($file);
602
1
10
$fh->write("");
603
1
11
$fh->close;
604
1
4
$__ret = { changed => 1 };
605
606
Rex::get_current_connection()->{reporter}->report(
607
1
9
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
3
$file, %{$option}, $__ret
1
10
615
);
616
##############################
617
618
}
619
elsif ( !$__ret->{changed} ) {
620
10
156
$__ret = { changed => 0 };
621
10
122
Rex::get_current_connection()->{reporter}->report( changed => 0, );
622
}
623
}
624
elsif ( $option->{ensure} eq "absent" ) {
625
2
15
$need_md5 = 0;
626
627
#### check and run before_change hook
628
2
16
Rex::Hook::run_hook( file => "before_change", $file, %{$option} );
2
32
629
##############################
630
631
2
50
27
if ( $fs->is_file($file) ) {
0
632
2
42
$fs->unlink($file);
633
2
10
$__ret = { changed => 1 };
634
Rex::get_current_connection()->{reporter}->report(
635
2
18
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
14
Rex::Hook::run_hook( file => "after_change", $file, %{$option}, $__ret );
2
43
654
##############################
655
656
}
657
elsif ( $option->{ensure} eq "directory" ) {
658
2
62
Rex::Logger::debug("file() should be a directory");
659
2
14
my %dir_option;
660
2
50
21
if ( exists $option->{owner} ) {
661
0
0
$dir_option{owner} = $option->{owner};
662
}
663
2
50
23
if ( exists $option->{group} ) {
664
0
0
$dir_option{group} = $option->{group};
665
}
666
2
50
14
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
738
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
81
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
401
if ( $option->{ensure} ne "absent" ) {
708
709
51
100
204
if ($need_md5) {
710
2
29
eval { $new_md5 = md5($file); };
2
66
711
}
712
51
424
my %stat_old = $fs->stat($file);
713
714
51
100
505
if ( exists $option->{"mode"} ) {
715
28
701
$fs->chmod( $option->{"mode"}, $file );
716
}
717
718
51
100
769
if ( exists $option->{"group"} ) {
719
25
618
$fs->chgrp( $option->{"group"}, $file );
720
}
721
722
51
100
682
if ( exists $option->{"owner"} ) {
723
25
546
$fs->chown( $option->{"owner"}, $file );
724
}
725
726
51
994
my %stat_new = $fs->stat($file);
727
728
51
100
33
1404
if ( %stat_old && %stat_new && $stat_old{mode} ne $stat_new{mode} ) {
66
729
Rex::get_current_connection()->{reporter}->report(
730
15
116
changed => 1,
731
message =>
732
"File-System permissions changed from $stat_old{mode} to $stat_new{mode}.",
733
);
734
}
735
736
51
50
33
1193
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
1308
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
413
my $on_change_done = 0;
753
754
53
100
260
if ($need_md5) {
755
2
0
33
42
unless ( $old_md5 && $new_md5 && $old_md5 eq $new_md5 ) {
33
756
2
50
45
$old_md5 ||= "";
757
2
50
35
$new_md5 ||= "";
758
759
2
48
Rex::Logger::debug("File $file has been changed... Running on_change");
760
2
22
Rex::Logger::debug("old: $old_md5");
761
2
43
Rex::Logger::debug("new: $new_md5");
762
763
2
56
&$on_change($file);
764
765
2
38
$on_change_done = 1;
766
767
Rex::get_current_connection()->{reporter}->report(
768
2
25
changed => 1,
769
message => "Content changed.",
770
);
771
772
2
15
$__ret = { changed => 1 };
773
}
774
}
775
776
53
100
100
607
if ( $__ret->{changed} == 1 && $on_change_done == 0 ) {
100
777
39
463
&$on_change($file);
778
}
779
elsif ( $__ret->{changed} == 0 ) {
780
12
432
Rex::Logger::debug(
781
"File $file has not been changed... Running on_no_change");
782
12
158
&$on_no_change($file);
783
}
784
785
#### check and run after hook
786
53
166
Rex::Hook::run_hook( file => "after", $file, %{$option}, $__ret );
53
1127
787
##############################
788
789
Rex::get_current_connection()->{reporter}
790
53
264
->report_resource_end( type => "file", name => $file );
791
792
53
3305
return $__ret->{changed};
793
}
794
795
sub get_tmp_file_name {
796
44
44
0
2764
my $file = shift;
797
798
44
5371
my $dirname = dirname($file);
799
44
1607
my $filename = ".rex.tmp." . basename($file);
800
801
44
100
1699
my $tmp_file_name =
802
$dirname eq '.'
803
? $filename
804
: Rex::Helper::File::Spec->catfile( $dirname, $filename );
805
806
44
232
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
1868
my ($file) = @_;
833
42
686
$file = resolv_path($file);
834
835
42
859
Rex::Logger::debug("Opening file: $file for writing.");
836
837
42
971
my $fh = Rex::Interface::File->create;
838
42
50
251
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
1174
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
2432
my ($file) = @_;
852
1
55
$file = resolv_path($file);
853
854
1
52
Rex::Logger::debug("Opening file: $file for appending.");
855
856
1
67
my $fh = Rex::Interface::File->create;
857
858
1
50
29
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
85
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
987
my ($file) = @_;
890
137
1104
$file = resolv_path($file);
891
892
137
2508
Rex::Logger::debug("Opening file: $file for reading.");
893
894
137
4071
my $fh = Rex::Interface::File->create;
895
896
137
50
1019
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
3337
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
12668
my ($file) = @_;
914
86
1049
$file = resolv_path($file);
915
916
86
766
my $fh = file_read($file);
917
86
50
320
unless ($fh) {
918
0
0
die("Can't open $file for reading");
919
}
920
86
656
my $content = $fh->read_all;
921
86
568
$fh->close;
922
923
86
724
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
1696
my ( $file, @m ) = @_;
938
1
34
$file = resolv_path($file);
939
940
Rex::get_current_connection()->{reporter}
941
1
27
->report_resource_start( type => "delete_lines_matching", name => $file );
942
943
1
14
for (@m) {
944
1
50
25
if ( ref($_) ne "Regexp" ) {
945
1
58
$_ = qr{\Q$_\E};
946
}
947
}
948
949
1
41
my $fs = Rex::Interface::Fs->create;
950
951
1
26
my %stat = $fs->stat($file);
952
953
1
50
20
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
16
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
15
my $nl = $/;
964
1
40
my @content = split( /$nl/, cat($file) );
965
966
1
9
my $old_md5 = "";
967
1
7
eval { $old_md5 = md5($file); };
1
31
968
969
1
22
my @new_content;
970
971
OUT:
972
1
18
for my $line (@content) {
973
IN:
974
1
20
for my $match (@m) {
975
1
50
35
if ( $line =~ $match ) {
976
1
13
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
49
mode => $stat{mode};
988
989
1
21
my $new_md5 = "";
990
1
16
eval { $new_md5 = md5($file); };
1
46
991
992
1
50
34
if ( $new_md5 ne $old_md5 ) {
993
Rex::get_current_connection()->{reporter}->report(
994
1
30
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
16
->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
21294
_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
9785
_append_or_update( 'append_or_amend_line', @_ );
1102
}
1103
1104
sub _append_or_update {
1105
19
19
165
my $action = shift;
1106
19
90
my $file = shift;
1107
1108
19
95
$file = resolv_path($file);
1109
19
108
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
70
->report_resource_start( type => $action, name => $file );
1116
1117
eval {
1118
45
45
450
no warnings;
45
107
45
71652
1119
19
155
$option = {@_};
1120
1121
# if there is no line parameter, it is the old parameter format
1122
# so go dieing
1123
19
100
159
if ( !exists $option->{line} ) {
1124
4
49
die;
1125
}
1126
15
53
$new_line = $option->{line};
1127
15
100
100
307
if ( exists $option->{regexp} && ref $option->{regexp} eq "Regexp" ) {
100
1128
10
58
@m = ( $option->{regexp} );
1129
}
1130
elsif ( ref $option->{regexp} eq "ARRAY" ) {
1131
2
15
@m = @{ $option->{regexp} };
2
9
1132
}
1133
15
50
171
$on_change = $option->{on_change} || undef;
1134
15
50
104
$on_no_change = $option->{on_no_change} || undef;
1135
15
99
1;
1136
19
100
68
} or do {
1137
4
26
( $new_line, @m ) = @_;
1138
1139
# check if something in @m (the regexpes) is named on_change or on_no_change
1140
4
64
for my $option ( [ on_change => \$on_change ],
1141
[ on_no_change => \$on_no_change ] )
1142
{
1143
8
38
for ( my $i = 0 ; $i < $#m ; $i++ ) {
1144
8
100
66
52
if ( $m[$i] eq $option->[0] && ref( $m[ $i + 1 ] ) eq "CODE" ) {
1145
5
13
${ $option->[1] } = $m[ $i + 1 ];
5
14
1146
5
21
splice( @m, $i, 2 );
1147
5
13
last;
1148
}
1149
}
1150
}
1151
};
1152
1153
19
50
102
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
350
my $fs = Rex::Interface::Fs->create;
1160
1161
19
224
my %stat = $fs->stat($file);
1162
1163
19
119
my ( $old_md5, $ret );
1164
19
115
$old_md5 = md5($file);
1165
1166
# slow but secure way
1167
19
174
my $content;
1168
eval {
1169
19
456
$content = [ split( /\n/, cat($file) ) ];
1170
19
275
1;
1171
19
50
171
} or do {
1172
0
0
$ret = 1;
1173
};
1174
1175
19
100
133
if ( !@m ) {
1176
5
237
push @m, qr{\Q$new_line\E};
1177
}
1178
1179
19
87
my $found;
1180
19
63
for my $line ( 0 .. $#{$content} ) {
19
187
1181
129
251
for my $match (@m) {
1182
144
50
384
if ( ref($match) ne "Regexp" ) {
1183
0
0
$match = qr{$match};
1184
}
1185
144
100
810
if ( $content->[$line] =~ $match ) {
1186
9
70
$found = 1;
1187
9
100
107
last if $action eq 'append_if_no_such_line';
1188
3
57
$content->[$line] = "$new_line";
1189
}
1190
}
1191
}
1192
1193
19
132
my $new_md5;
1194
19
100
100
306
if ( $action eq 'append_if_no_such_line' && $found ) {
1195
6
31
$new_md5 = $old_md5;
1196
}
1197
else {
1198
13
100
134
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
293
mode => $stat{mode};
1205
13
422
$new_md5 = md5($file);
1206
}
1207
1208
19
100
66
552
if ( $on_change || $on_no_change ) {
1209
3
100
33
129
if ( $old_md5 && $new_md5 && $old_md5 ne $new_md5 ) {
50
66
1210
1
50
19
if ($on_change) {
1211
1
50
13
$old_md5 ||= "";
1212
1
50
20
$new_md5 ||= "";
1213
1214
1
28
Rex::Logger::debug("File $file has been changed... Running on_change");
1215
1
31
Rex::Logger::debug("old: $old_md5");
1216
1
19
Rex::Logger::debug("new: $new_md5");
1217
1
20
&$on_change($file);
1218
}
1219
}
1220
elsif ($on_no_change) {
1221
2
50
19
$new_md5 ||= "";
1222
1223
2
31
Rex::Logger::debug(
1224
"File $file has not been changed (md5 $new_md5)... Running on_no_change"
1225
);
1226
2
29
&$on_no_change($file);
1227
}
1228
}
1229
1230
19
100
33
473
if ( $old_md5 && $new_md5 && $old_md5 ne $new_md5 ) {
66
1231
Rex::get_current_connection()->{reporter}->report(
1232
13
87
changed => 1,
1233
message => "Content changed.",
1234
);
1235
}
1236
else {
1237
6
43
Rex::get_current_connection()->{reporter}->report( changed => 0, );
1238
}
1239
1240
Rex::get_current_connection()->{reporter}
1241
19
126
->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
15986
my ( $search, $replace, $file, @option ) = @_;
1355
11
169
$file = resolv_path($file);
1356
11
76
my $options = {};
1357
1358
Rex::get_current_connection()->{reporter}
1359
11
72
->report_resource_start( type => "sed", name => $file );
1360
1361
11
50
93
if ( ref( $option[0] ) ) {
1362
0
0
$options = $option[0];
1363
}
1364
else {
1365
11
60
$options = {@option};
1366
}
1367
1368
11
50
208
my $on_change = $options->{"on_change"} || undef;
1369
11
50
124
my $on_no_change = $options->{"on_no_change"} || undef;
1370
1371
11
42
my @content;
1372
1373
11
100
68
if ( exists $options->{multiline} ) {
1374
1
45
$content[0] = cat($file);
1375
1
31
$content[0] =~ s/$search/$replace/gms;
1376
}
1377
else {
1378
10
70
@content = split( /\n/, cat($file) );
1379
10
67
for (@content) {
1380
109
385
s/$search/$replace/;
1381
}
1382
}
1383
1384
11
241
my $fs = Rex::Interface::Fs->create;
1385
11
54
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
199
);
1396
1397
Rex::get_current_connection()->{reporter}
1398
11
226
->report_resource_end( type => "sed", name => $file );
1399
1400
11
627
return $ret;
1401
}
1402
1403
1;