# logrotate-lib.pl # Common functions for parsing the logrotate configuration file. BEGIN { push(@INC, ".."); }; use WebminCore; &init_config(); if (open(VERSION, "<$module_config_directory/version")) { chop($logrotate_version = ); close(VERSION); } # Use sample config if it exists but real config doesn't yet if (!-r $config{'logrotate_conf'} && -r $config{'sample_conf'}) { ©_source_dest($config{'sample_conf'}, $config{'logrotate_conf'}); } # get_config_parent() # Returns the parsed global config while keeping the writable local file as # its save target. Callers must materialize that file before global writes. sub get_config_parent { if (!$get_config_parent_cache) { local ($conf, $lines) = &get_config(); # Even when members came from the vendor config, never make /usr the # destination for newly-added global directives. $get_config_parent_cache = { 'members' => $conf, 'file' => $config{'logrotate_conf'}, 'line' => 0, 'eline' => $lines, 'global' => 1 }; } return $get_config_parent_cache; } # get_main_config_file() # Returns the local main config, or the vendor default if no local one exists sub get_main_config_file { return $config{'logrotate_conf'} if (-e $config{'logrotate_conf'}); return $config{'vendor_logrotate_conf'} if ($config{'vendor_logrotate_conf'}); return $config{'logrotate_conf'}; } # is_vendor_main_config(file) # Returns 1 if a file is the vendor-provided main config sub is_vendor_main_config { my ($file) = @_; return $config{'vendor_logrotate_conf'} && &same_file($file, $config{'vendor_logrotate_conf'}); } # relative_config_path(file, directory) # Returns a file's path relative to a config directory sub relative_config_path { my ($file, $dir) = @_; return undef if (!$file || !$dir); $dir =~ s/\/+$//; $dir .= '/'; return $file =~ /^\Q$dir\E(.+)$/ ? $1 : undef; } # is_vendor_config_file(file) # Returns 1 if a drop-in comes from the vendor directory sub is_vendor_config_file { my ($file) = @_; return defined(&relative_config_path( $file, $config{'vendor_add_file'})); } # get_local_override_file(vendor-file) # Returns the local path that overrides a vendor drop-in sub get_local_override_file { my ($file) = @_; my $rel = &relative_config_path($file, $config{'vendor_add_file'}); return undef if (!defined($rel) || !$config{'add_file'}); return $config{'add_file'}.'/'.$rel; } # get_vendor_config_file(local-file) # Returns the vendor file shadowed by a local drop-in, if any sub get_vendor_config_file { my ($file) = @_; my $rel = &relative_config_path($file, $config{'add_file'}); return undef if (!defined($rel) || !$config{'vendor_add_file'}); my $vendor = $config{'vendor_add_file'}.'/'.$rel; return -f $vendor ? $vendor : undef; } # flush_logrotate_config_cache() # Clears parsed config state after creating a local override sub flush_logrotate_config_cache { %get_config_cache = ( ); %get_config_lnum_cache = ( ); %get_config_files_cache = ( ); $get_config_parent_cache = undef; } # copy_vendor_config(source, destination) # Copies a vendor config to the writable local tree sub copy_vendor_config { my ($source, $dest) = @_; # An existing independent regular destination is already a usable override. # Refuse links to the vendor file, symlinks, and other non-regular file types # so the local path cannot redirect writes back into the read-only tree. if (-e $dest || -l $dest) { if (-f $dest && !-l $dest) { if (&same_file($source, $dest)) { &error(&text('save_evendorwrite', "". &html_escape($source)."")); } else { &flush_logrotate_config_cache(); return $dest; } } &error(&text('save_eoverride', "". &html_escape($dest)."")); } # Create missing subdirectories before copying the complete vendor file. # Following a source symlink produces an editable snapshot, not another link. my $dir = $dest; $dir =~ s/\/[^\/]+$//; &make_dir_recursive($dir, 0755) if (!-d $dir); my ($ok, $err) = ©_source_dest($source, $dest, 1); # Do not leave a partial override behind after a copy or chmod failure, since # even an incomplete local file would hide the valid vendor configuration. if (!$ok || !&set_ownership_permissions(undef, undef, 0644, $dest)) { $err ||= $!; &unlink_file($dest) if (-e $dest || -l $dest); &error(&text('save_ecopy', "".&html_escape($dest)."", &html_escape($err))); } # Force the next read to select and parse the newly-created local file. &flush_logrotate_config_cache(); return $dest; } # ensure_local_main_config() # Creates a writable local main config when only the vendor default exists sub ensure_local_main_config { my $main = &get_main_config_file(); return $config{'logrotate_conf'} if (!&is_vendor_main_config($main)); return ©_vendor_config($main, $config{'logrotate_conf'}); } # ensure_local_config_override(vendor-file) # Creates a writable local copy that shadows a vendor drop-in sub ensure_local_config_override { my ($file) = @_; my $local = &get_local_override_file($file); return $file if (!$local); return ©_vendor_config($file, $local); } # ensure_writable_config_file(file) # Makes a config file safe to edit, by creating the local override that # shadows it when it belongs to the vendor tree. Callers must use this before # saving directives, as save_directive refuses to write to a vendor file. sub ensure_writable_config_file { my ($file) = @_; if (&is_vendor_main_config($file)) { return &ensure_local_main_config(); } elsif (&is_vendor_config_file($file)) { return &ensure_local_config_override($file); } elsif (my $vendor = &get_vendor_config_file($file)) { # A local path that shadows a vendor file, but does not exist yet return &ensure_local_config_override($vendor); } elsif ($file eq &get_config_parent()->{'file'} && &is_vendor_main_config(&get_main_config_file())) { # New directives go to the main config, which is still the vendor one return &ensure_local_main_config(); } return $file; } # list_config_dir_files(directory, [relative-subdirectory]) # Returns relative and absolute paths for regular files below a directory sub list_config_dir_files { my ($dir, $subdir) = @_; my $path = $subdir ? $dir.'/'.$subdir : $dir; opendir(my $dh, $path) || return ( ); my @names = sort { $a cmp $b } readdir($dh); closedir($dh); my @rv; foreach my $name (@names) { next if ($name eq '.' || $name eq '..'); my $rel = $subdir ? $subdir.'/'.$name : $name; my $file = $dir.'/'.$rel; # Match find without -L: ignore symlinks, recurse into real directories, # and return only regular files with paths relative to the scanned root. next if (-l $file); if (-d $file) { push(@rv, &list_config_dir_files($dir, $rel)); } elsif (-f $file) { push(@rv, [ $rel, $file ]); } } return @rv; } # get_add_file_configs([&already-loaded-files]) # Returns the effective vendor and local configs loaded by logrotate-all sub get_add_file_configs { my ($files) = @_; return ( ) if (!$config{'scan_add_file'}); # Collect the same relative names produced by the wrapper's recursive find. # Processing the local tree last records its regular files directly. my %effective; foreach my $dir ($config{'vendor_add_file'}, $config{'add_file'}) { next if (!$dir || !-d $dir); foreach my $entry (&list_config_dir_files($dir)) { $effective{$entry->[0]} = $entry->[1]; } } # Match the wrapper's stable lexical order and omit files already reached by # an explicit include in the main configuration. The existence check also # honors a local non-regular counterpart exactly as the wrapper does. my @rv; foreach my $name (sort { $a cmp $b } keys %effective) { my $local = $config{'add_file'} ? $config{'add_file'}.'/'.$name : undef; my $f = $local && -e $local ? $local : $effective{$name}; next if ($files && grep { &same_file($_, $f) } @$files); push(@rv, $f); } return @rv; } # get_scheduled_logrotate_command() # Returns the distro wrapper, or a command for the effective config files sub get_scheduled_logrotate_command { # The distro wrapper discovers the effective drop-in set on every run, so it # remains correct when packages or administrators add files later. if ($config{'logrotate_all'} && -x $config{'logrotate_all'}) { return "e_path($config{'logrotate_all'}); } # Preserve the historical command exactly on systems that do not opt into # external or vendor configuration discovery. if (!$config{'vendor_logrotate_conf'} && !$config{'vendor_add_file'} && !$config{'scan_add_file'}) { return &has_command($config{'logrotate'})." ". $config{'logrotate_conf'}; } # If the configured wrapper is unavailable, build a usable command from the # effective main config and the drop-ins visible at schedule creation time. my $main = &get_main_config_file(); my (undef, undef, $files) = &get_config($main); my @configs = ($main, &get_add_file_configs($files)); my $program = &has_command($config{'logrotate'}) || $config{'logrotate'}; return "e_path($program).' '. join(' ', map { "e_path($_) } @configs); } # get_config([file]) # Returns a list of logrotate config file entries sub get_config { my ($argfile) = @_; my $file = $argfile || &get_main_config_file(); if (!$argfile && $get_config_cache{$file}) { return wantarray ? ( $get_config_cache{$file}, $get_config_lnum_cache{$file}, $get_config_files_cache{$file} ) : $get_config_cache{$file}; } my @files = ( $file ); my @rv; my $addto = \@rv; my $section; my $lnum = 0; my $fh = "FILE".$file_count++; open($fh, "<".$file); while(<$fh>) { s/\r|\n//g; s/#.*$//; if (/^\s*(.*)\{\s*$/) { # Start of a section push(@name, &split_quoted_string($1)); $section = { 'name' => [ @name ], 'members' => [ ], 'index' => scalar(@$addto), 'line' => defined($namestart) ? $namestart : $lnum, 'eline' => $lnum, 'file' => $file }; push(@$addto, $section); $addto = $section->{'members'}; @name = ( ); $namestart = undef; } elsif ((/^\s*\// || /^\s*"\//) && !$section) { # A path before a section $namestart = $lnum if (!@name); push(@name, &split_quoted_string($_)); } elsif (/^\s*}\s*$/) { # End of a section $addto = \@rv; $section->{'eline'} = $lnum; $section = undef; } elsif (/^\s*include\s+(.*)$/i) { # Including other directives files my $incfile = $1; if (-d $incfile) { # Multiple files! my $f; opendir(DIR, $incfile); my @dirs = sort { $a cmp $b } readdir(DIR); closedir(DIR); foreach $f (@dirs) { next if ($f =~ /^\./ || $f =~ /\.rpm(save|orig|new)$/ || $f =~ /\~$/ || $f =~ /,v$/ || $f =~ /\.swp$/ || $f =~ /\.lock$/); my ($inc, $ilnum, $ifiles) = &get_config("$incfile/$f"); push(@files, @$ifiles); map { $_->{'index'} += @$addto } @$inc; push(@$addto, @$inc); } } else { # A single file my ($inc, $ilnum, $ifiles) = &get_config($incfile); push(@files, @$ifiles); map { $_->{'index'} += @$addto } @$inc; push(@$addto, @$inc); } } elsif (/^\s*(\S+)\s*(.*)$/) { # Single directive my $dir = { 'name' => $1, 'value' => $2, 'index' => scalar(@$addto), 'line' => $lnum, 'eline' => $lnum, 'file' => $file }; push(@$addto, $dir); if ($1 eq 'postrotate' || $1 eq 'prerotate') { # Followed by a multi-line script! while(<$fh>) { $lnum++; s/\r|\n//g; last if (/^\s*(endscript|endrotate)\s*$/); s/^\s+//; $dir->{'script'} .= $_."\n"; } $dir->{'eline'} = $lnum; } } $lnum++; } close($fh); if (!$argfile) { foreach my $f (&get_add_file_configs(\@files)) { my ($inc, undef, $ifiles) = &get_config($f); map { $_->{'index'} += @rv } @$inc; push(@rv, @$inc); push(@files, @$ifiles); } $get_config_cache{$file} = \@rv; $get_config_lnum_cache{$file} = $lnum; $get_config_files_cache{$file} = \@files; } return wantarray ? (\@rv, $lnum, \@files) : \@rv; } # join_words(word, ...) # Joins an array of words into a string, with quotes if needed sub join_words { return join(" ", map { /\s/ ? "\"$_\"" : $_ } @_); } # find(name, &config) # Returns an object or objects from the config with some name sub find { my ($name, $conf) = @_; my @rv = grep { lc($_->{'name'}) eq lc($name) } @$conf; return wantarray ? @rv : $rv[0]; } # find_value(name, &config) # Returns a value or values from the config with some name sub find_value { my ($name, $conf) = @_; my @rv = map { defined($_->{'script'}) ? $_->{'script'} : $_->{'value'} } grep { lc($_->{'name'}) eq lc($name) } @$conf; return wantarray ? @rv : $rv[0]; } # get_logrotate_version([&out]) # Returns the version number, and saves the full -v output to the out param sub get_logrotate_version { my ($rv) = @_; my $out = &backquote_command("$config{'logrotate'} -v 2>&1", 1); $$rv = $out if ($rv); return $out =~ /logrotate\s+([0-9\.]+)\s/ || $out =~ /logrotate\-([0-9\.]+)\s/ ? $1 : undef; } # get_period(&conf) # Returns the rotation time period set in the config sub get_period { my ($conf) = @_; foreach my $p ("daily", "weekly", "monthly") { my $ex = &find($p, $conf); return $p if ($ex); } return undef; } # save_directive(&parent, &old|name, &new, [indent]) # Updates one entry identified by either its name or parsed object sub save_directive { my ($parent, $oldv, $newv, $indent) = @_; my $conf = $parent->{'members'}; my $old = !defined($oldv) ? undef : ref($oldv) ? $oldv : &find($oldv, $conf); my $new = !defined($newv) ? undef : ref($newv) ? $newv : { 'name' => $old ? $old->{'name'} : $oldv, 'value' => $newv }; # Deleting an entry that is already absent is a true no-op. In particular, # do not put a missing local main config into the writable line cache. return if (!$old && !$new); # Find the file behind this write. Existing directives use their own file, # new sections may name a separate file, and other additions use the parent # section or effective main config. my $vendor_file; my $write_file = $parent->{'file'}; if ($old) { $write_file = $old->{'file'}; } elsif ($new && $new->{'file'} && !($parent->{'global'} && !$new->{'members'})) { $write_file = $new->{'file'}; } if ($write_file) { my $shadowed_vendor = &get_vendor_config_file($write_file); if (&is_vendor_main_config($write_file) || &is_vendor_config_file($write_file)) { $vendor_file = $write_file; } elsif ($shadowed_vendor && (!-f $write_file || -l $write_file || &same_file($write_file, $shadowed_vendor))) { $vendor_file = $shadowed_vendor; } } if (!$vendor_file && !$old && $parent->{'global'} && &same_file($write_file, $parent->{'file'}) && &is_vendor_main_config(&get_main_config_file())) { $vendor_file = &get_main_config_file(); } # Copying changes which file owns the parsed objects, so callers must create # and reload a local override before editing. Never write through a stale # object that still points at the vendor tree. &error(&text('save_evendorwrite', "".&html_escape($vendor_file)."")) if ($vendor_file); my $lref = &read_file_lines($write_file); my @lines = &directive_lines($new, $indent) if ($new); my $gparent = &get_config_parent(); if ($old && $new) { # Update my $oldlines = $old->{'eline'} - $old->{'line'} + 1; splice(@$lref, $old->{'line'}, $oldlines, @lines); $new->{'line'} = $old->{'line'}; $new->{'index'} = $old->{'index'}; $new->{'file'} = $old->{'file'}; $new->{'eline'} = $new->{'line'} + scalar(@lines) - 1; %$old = %{$new}; &renumber($gparent, $old->{'file'}, $old->{'eline'}, scalar(@lines) - $oldlines, $old); } elsif ($old && !$new) { # Delete my $oldlines = $old->{'eline'} - $old->{'line'} + 1; splice(@$lref, $old->{'line'}, $old->{'eline'} - $old->{'line'} + 1); splice(@$conf, $old->{'index'}, 1); &renumber($gparent, $old->{'file'}, $old->{'line'}, -$oldlines); } elsif (!$old && $new && $parent->{'global'} && !$new->{'members'}) { # Add at the start of the file if (defined($parent->{'line'})) { splice(@$lref, 0, 0, @lines); $new->{'line'} = 0; $new->{'eline'} = $new->{'line'} + scalar(@lines) - 1; $new->{'file'} = $parent->{'file'}; &renumber($gparent, $new->{'file'}, $new->{'line'}-1, scalar(@lines)); } $new->{'index'} = 0; splice(@$conf, 0, 0, $new); } elsif (!$old && $new) { # Add (to end of section) if (defined($parent->{'line'})) { if (!$new->{'file'} || $parent->{'file'} eq $new->{'file'}) { # Adding to parent file splice(@$lref, $parent->{'eline'}, 0, @lines); $new->{'line'} = $parent->{'eline'}; $new->{'eline'} = $new->{'line'} + scalar(@lines) - 1; $new->{'file'} = $parent->{'file'}; &renumber($gparent, $new->{'file'}, $new->{'line'}-1, scalar(@lines)); } else { # Adding to another file my $lref2 = &read_file_lines($new->{'file'}); $new->{'line'} = scalar(@$lref2); $new->{'eline'} = $new->{'line'} + scalar(@lines) - 1; push(@$lref2, @lines); } } $new->{'index'} = scalar(@$conf); push(@$conf, $new); } } # renumber(&object, file, startline, count, [&skip]) # Update line numbers in the config that are in some file and after # some line sub renumber { my ($conf, $file, $start, $count, $skip) = @_; return if (!$count); if ($conf->{'file'} eq $file && $conf ne $skip) { $conf->{'line'} += $count if ($conf->{'line'} > $start); $conf->{'eline'} += $count if ($conf->{'eline'} > $start); } if ($conf->{'members'}) { foreach my $c (@{$conf->{'members'}}) { &renumber($c, $file, $start, $count, $skip); } } } # directive_lines(&dir, indent) # Returns an array of lines to add to the config file for some directive sub directive_lines { my ($dir, $indent) = @_; my @rv; if ($dir->{'members'}) { push(@rv, $indent.&join_words(@{$dir->{'name'}})." {"); foreach my $m (@{$dir->{'members'}}) { push(@rv, &directive_lines($m, $indent."\t")); } push(@rv, $indent."}"); } elsif ($dir->{'script'}) { push(@rv, $indent.$dir->{'name'}); foreach my $s (split(/\n/, $dir->{'script'})) { push(@rv, $indent.$s); } push(@rv, $indent."endscript"); } else { push(@rv, $indent.$dir->{'name'}. ($dir->{'value'} eq "" ? "" : " ".$dir->{'value'})); } return @rv; } # delete_if_empty(file) # Removes a file if it has no more parsed entries, unless it is a local # override whose continued existence is needed to hide a vendor file sub delete_if_empty { my ($file) = @_; return if (&get_vendor_config_file($file)); my $conf = &get_config(); my %files = map { $_, 1 } &unique(map { $_->{'file'} } @$conf); &unlink_file($file) if (!$files{$file}); } %global_default = ( "nocompress" => "", "compress" => undef, "nodelaycompress" => "", "delaycompress" => undef, "ifempty" => "", "notifempty" => undef, "nocopytruncate" => "", "copytruncate" => undef, "nomissingok" => "", "missingok" => undef, "rotate" => 0, "create" => "", "nocreate" => undef, "noolddir" => "", "olddir" => undef, "ext" => undef, "mail" => undef, "nomail" => "", "maillast" => "", "mailfirst" => undef, "errors" => undef, "postrotate" => undef, "prerotate" => undef, "errors" => undef, ); # rotate_log_now(&log) # Call logrotate on a config fragment file to rotate just one set of logs # immediately. sub rotate_log_now { my ($dir) = @_; my $conf = &get_config(); my $temp = &transname(); open(TEMP, ">$temp"); foreach my $c (@$conf) { if (!$c->{'members'}) { print TEMP map { "$_\n" } &directive_lines($c); } } print TEMP map { "$_\n" } &directive_lines($dir); close(TEMP); &set_ownership_permissions(undef, undef, 0644, $temp); my $out = &backquote_logged("$config{'logrotate'} -f $temp 2>&1"); return ($?, $out); } # get_add_file([filename]) # Returns the file to which new logrotate sections should be added sub get_add_file { my ($filename) = @_; $filename =~ s/\*/ALL/g; if ($config{'add_file'} && -d $config{'add_file'} && $filename) { # Adding to a new file in a directory return "$config{'add_file'}/$filename.conf"; } elsif ($config{'add_file'} && !-d $config{'add_file'}) { # Make sure file is valid my ($conf, $lnum, $files) = &get_config(); if (&indexof($config{'add_file'}, @$files) >= 0) { return $config{'add_file'}; } } return $config{'logrotate_conf'}; } 1;