Add new System Logs Viewer (logviewer) module

This commit is contained in:
Ilia
2022-05-30 00:41:12 +03:00
parent 8f9e1cbfc5
commit c3ba4a9557
15 changed files with 600 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
---- Changes since 1.000 ----
Add support for viewing all system logs files, including systemd-journald
+46
View File
@@ -0,0 +1,46 @@
require 'logviewer-lib.pl';
# acl_security_form(&options)
# Output HTML for editing security options for the syslog module
sub acl_security_form
{
# Can enter arbitrary filename
print &ui_table_row($text{'acl_any'},
&ui_yesno_radio("any", int($_[0]->{'any'})));
# Can view syslog logs and logs from other modules
print &ui_table_row($text{'acl_syslog'},
&ui_yesno_radio("syslog", int($_[0]->{'syslog'})));
print &ui_table_row($text{'acl_others'},
&ui_yesno_radio("others", int($_[0]->{'others'})));
# Allowed directories
print &ui_table_row($text{'acl_logs'},
&ui_radio("logs_def", $_[0]->{'logs'} ? 0 : 1,
[ [ 1, $text{'acl_all'} ], [ 0, $text{'acl_sel'} ] ]).
"<br>\n".
&ui_textarea("logs", join("\n", split(/\t+/, $_[0]->{'logs'})),
5, 50), 3);
# Extra per-user log files
print &ui_table_row($text{'acl_extra'},
&ui_textarea("extras", join("\n", split(/\t+/, $_[0]->{'extras'})),
5, 50), 3);
}
# acl_security_save(&options)
# Parse the form for security options for the syslog module
sub acl_security_save
{
$_[0]->{'any'} = $in{'any'};
$_[0]->{'syslog'} = $in{'syslog'};
$_[0]->{'others'} = $in{'others'};
$in{'logs'} =~ s/\r//g;
$_[0]->{'logs'} = $in{'logs_def'} ? undef :
join("\t", split(/\n/, $in{'logs'}));
$_[0]->{'extras'} = join("\t", split(/\n/, $in{'extras'}));
}
+40
View File
@@ -0,0 +1,40 @@
do 'logviewer-lib.pl';
# backup_config_files()
# Returns files and directories that can be backed up
sub backup_config_files
{
return undef
}
# pre_backup(&files)
# Called before the files are actually read
sub pre_backup
{
return undef;
}
# post_backup(&files)
# Called after the files are actually read
sub post_backup
{
return undef;
}
# pre_restore(&files)
# Called before the files are restored from a backup
sub pre_restore
{
return undef;
}
# post_restore(&files)
# Called after the files are restored from a backup
sub post_restore
{
return undef;
}
1;
+3
View File
@@ -0,0 +1,3 @@
lines=100
others=1
reverse=1
+6
View File
@@ -0,0 +1,6 @@
line1=Configurable options,11
lines=Default number of lines to display,0,6
refresh=Seconds between log view refreshes,3,Never
others=Show logs from other modules?,1,1-Yes,0-No
extras=Extra log files to show,9,50,4,\t
reverse=Log display order,1,1-Newest lines at top,0-Newest lines at bottom
+3
View File
@@ -0,0 +1,3 @@
any=1
syslog=1
others=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

+91
View File
@@ -0,0 +1,91 @@
#!/usr/local/bin/perl
# index.cgi
# Display syslog rules
require './logviewer-lib.pl';
&ui_print_header($text{'index_subtitle'}, $text{'index_title'}, "", undef, 1, 1, 0,
&help_search_link("systemd-journal journalctl", "man", "doc"));
if (!&has_command('journalctl')) {
# Not installed
&ui_print_endpage(&text('index_econf', "<tt>$config{'syslog_conf'}</tt>", "../config.cgi?$module_name"));
}
# Display syslog rules
my @col1;
my @col2;
my @col3;
if ($access{'syslog'}) {
my @systemctl_cmds = &get_systemctl_cmds();
foreach $o (@systemctl_cmds) {
local @cols;
push(@cols, &text('index_cmd', "<tt>".$o->{'cmd'}."</tt>"));
push(@cols, $o->{'desc'});
push(@cols, &ui_link("view_log.cgi?idx=$o->{'id'}&view=1", $text{'index_view'}) );
push(@col1, \@cols);
}
}
# Display logs from other modules
if ($config{'others'} && $access{'others'}) {
@others = &get_other_module_logs();
if (@others) {
foreach $o (@others) {
local @cols;
if ($o->{'file'}) {
push(@cols, &text('index_file',"<tt>$o->{'file'}</tt>"));
}
else {
push(@cols, &text('index_cmd', "<tt>".$o->{'cmd'}."</tt>"));
}
push(@cols, $o->{'desc'});
push(@cols, &ui_link("view_log.cgi?oidx=$o->{'mindex'}".
"&omod=$o->{'mod'}&view=1", $text{'index_view'}) );
push(@col2, \@cols);
}
}
}
# Display extra log files
foreach $e (&extra_log_files()) {
local @cols;
push(@cols, &text('index_file', $e->{'file'}));
push(@cols, $e->{'desc'});
push(@cols, &ui_link("view_log.cgi?extra=$e->{'file'}&view=1", $text{'index_view'}) );
push(@col3, \@cols);
}
# Print sorted table with logs files and commands
my @acols = (@col1, @col2, @col3);
if (@acols) {
print &ui_columns_start([
$text{'index_to'},
$text{'index_rule'}, "" ], 100);
@acols = sort { $a->[0] cmp $b->[0] } @acols;
foreach my $col (@acols) {
print &ui_columns_row($col);
}
print &ui_columns_end();
print "<p>\n";
}
else {
print &ui_columns_start([ ], 100);
print &ui_columns_row([$text{'index_elogs'}], [" style='text-align: center'"]);
print &ui_columns_end();
print "<p>\n";
}
if ($access{'any'}) {
# Can view any log (under allowed dirs)
print &ui_form_start("view_log.cgi");
print &ui_hidden("view", 1),"\n";
print "$text{'index_viewfile'}&nbsp;&nbsp;\n",
&ui_textbox("file", undef, 50),"\n",
&file_chooser_button("file", 0, 1),"\n",
&ui_submit($text{'index_viewok'}),"\n";
print &ui_form_end();
}
&ui_print_footer("/", $text{'index'});
+13
View File
@@ -0,0 +1,13 @@
# install_check.pl
do 'logviewer-lib.pl';
# is_installed(mode)
# For mode 1, returns 2 if the server is installed and configured for use by
# Webmin, 1 if installed but not configured, or 0 otherwise.
# For mode 0, returns 1 if installed, 0 if not
sub is_installed
{
return $_[0] ? 2 : 1;
}
+39
View File
@@ -0,0 +1,39 @@
index_title=System Logs Viewer
index_elogs=No system logs were found to display
index_to=Log destination
index_rule=Messages selected
index_file=File $1
index_cmd=Output from $1
index_return=system logs viewer
index_view=View..
index_viewfile=View log file:
index_viewok=View
journal_journalctl=All system logs
journal_expla_journalctl=All system logs with message explanations
journal_journalctl_dmesg=All system kernel messages
journal_journalctl_debug_info=All system info and debug messages
journal_journalctl_notice_warning=All system notice and warning messages
journal_journalctl_err_emerg=All system error, critical, alert and emergency messages
view_title=View Logfile
view_header=Last $1 lines of $2 file
view_empty=Log file is empty
view_refresh=Refresh
view_filter=Filter lines with text $1
save_efile='$1' is not a valid filename : $2
save_ecannot2=You are not allowed to view this log
save_ecannot6=You are not allowed to view arbitrary logs
save_ecannot7=You are not allowed to view this extra log
save_emissing=Missing log file to view
acl_any=Can view any file as a log?
acl_logs=Can view and configure log files
acl_all=All logs
acl_sel=Only listed files and those under listed directories ..
acl_extra=Extra log files for this user
acl_syslog=Can view logs from syslog?
acl_others=Can view logs from other modules?
__norefs=1
+133
View File
@@ -0,0 +1,133 @@
# logviewer-lib.pl
# Functions for the syslog module
BEGIN { push(@INC, ".."); };
use WebminCore;
&init_config();
%access = &get_module_acl();
# can_edit_log(&log|file)
# Returns 1 if some log can be viewed/edited, 0 if not
sub can_edit_log
{
return 1 if (!$access{'logs'});
local @files = split(/\s+/, $access{'logs'});
local $lf;
if (ref($_[0])) {
$lf = $_[0]->{'file'} || $_[0]->{'pipe'} || $_[0]->{'host'} ||
$_[0]->{'socket'} || $_[0]->{'cmd'} ||
($_[0]->{'all'} ? "*" : "users");
}
else {
$lf = $_[0];
}
foreach $f (@files) {
return 1 if ($f eq $lf || &is_under_directory($f, $lf));
}
return 0;
}
#
# Returns standard
sub get_systemctl_cmds
{
my $lines = $config{'lines'} || 1000;
return !&has_command('journalctl') ? () : (
{ 'cmd' => "journalctl --lines $lines -p 0..3",
'desc' => $text{'journal_journalctl_err_emerg'},
'id' => 1, },
{ 'cmd' => "journalctl --lines $lines -p 4..5",
'desc' => $text{'journal_journalctl_notice_warning'},
'id' => 2, },
{ 'cmd' => "journalctl --lines $lines -p 6..7",
'desc' => $text{'journal_journalctl_debug_info'},
'id' => 3, },
{ 'cmd' => "journalctl --lines $lines -k ",
'desc' => $text{'journal_journalctl_dmesg'},
'id' => 4, },
{ 'cmd' => "journalctl --lines $lines",
'desc' => $text{'journal_journalctl'},
'id' => 5, },
{ 'cmd' => "journalctl --lines $lines -x ",
'desc' => $text{'journal_expla_journalctl'},
'id' => 6, } );
}
# all_log_files(file)
# Given a filename, returns all rotated versions, ordered by oldest first
sub all_log_files
{
$_[0] =~ /^(.*)\/([^\/]+)$/;
local $dir = $1;
local $base = $2;
local ($f, @rv);
opendir(DIR, &translate_filename($dir));
foreach $f (readdir(DIR)) {
local $trans = &translate_filename("$dir/$f");
if ($f =~ /^\Q$base\E/ && -f $trans && $f !~ /\.offset$/) {
push(@rv, "$dir/$f");
$mtime{"$dir/$f"} = [ stat($trans) ];
}
}
closedir(DIR);
return sort { $mtime{$a}->[9] <=> $mtime{$b}->[9] } @rv;
}
# get_other_module_logs([module])
# Returns a list of logs supplied by other modules
sub get_other_module_logs
{
local ($mod) = @_;
local @rv;
local %done;
foreach my $minfo (&get_all_module_infos()) {
next if ($mod && $minfo->{'dir'} ne $mod);
next if (!$minfo->{'syslog'});
next if ($minfo->{'dir'} =~ /^(init|proc)$/);
next if (!&foreign_installed($minfo->{'dir'}));
local $mdir = &module_root_directory($minfo->{'dir'});
next if (!-r "$mdir/syslog_logs.pl");
&foreign_require($minfo->{'dir'}, "syslog_logs.pl");
local $j = 0;
foreach my $l (&foreign_call($minfo->{'dir'}, "syslog_getlogs")) {
local $fc = $l->{'file'} || $l->{'cmd'};
next if ($done{$fc}++);
$l->{'minfo'} = $minfo;
$l->{'mod'} = $minfo->{'dir'};
$l->{'mindex'} = $j++;
push(@rv, $l);
}
}
@rv = sort { $a->{'minfo'}->{'desc'} cmp $b->{'minfo'}->{'desc'} } @rv;
local $i = 0;
foreach my $l (@rv) {
$l->{'index'} = $i++;
}
return @rv;
}
# extra_log_files()
# Returns a list of extra log files available to the current Webmin user. No filtering
# based on allowed directory is done though!
sub extra_log_files
{
local @rv;
foreach my $fd (split(/\t+/, $config{'extras'}), split(/\t+/, $access{'extras'})) {
if ($fd =~ /^"(\S+)"\s+"(\S.*)"$/) {
push(@rv, { 'file' => $1, 'desc' => $2 });
}
elsif ($fd =~ /^"(\S+)"$/) {
push(@rv, { 'file' => $1 });
}
elsif ($fd =~ /^(\S+)\s+(\S.*)$/) {
push(@rv, { 'file' => $1, 'desc' => $2 });
}
else {
push(@rv, { 'file' => $fd });
}
}
return @rv;
}
1;
+7
View File
@@ -0,0 +1,7 @@
name=logviewer
category=system
os_support=*-linux
desc=System Logs Viewer
depends=proc
longdesc=View and search all logs available on system
readonly=1
+1
View File
@@ -0,0 +1 @@
allowed=lines,refresh_def,refresh,others,reverse
+3
View File
@@ -0,0 +1,3 @@
any=0
syslog=1
others=1
+213
View File
@@ -0,0 +1,213 @@
#!/usr/local/bin/perl
# view_log.cgi
# Save, create, delete or view a log
require './logviewer-lib.pl';
&ReadParse();
&foreign_require("proc", "proc-lib.pl");
if ($in{'view'}) {
# Viewing a log file
@extras = &extra_log_files();
if ($in{'idx'} =~ /^\//) {
# The drop-down selector on this page has chosen a file
if (&indexof($in{'idx'}, (map { $_->{'file'} } @extras)) >= 0) {
$in{'extra'} = $in{'idx'};
delete($in{'file'});
}
else {
$in{'file'} = $in{'idx'};
delete($in{'extra'});
}
delete($in{'idx'});
delete($in{'oidx'});
}
if ($in{'idx'} ne '') {
# From systemctl commands
my @systemctl_cmds = &get_systemctl_cmds();
my ($log) = grep { $_->{'id'} eq $in{'idx'} } @systemctl_cmds;
&can_edit_log($log) && $access{'syslog'} ||
&error($text{'save_ecannot2'});
$cmd = $log->{'cmd'};
}
elsif ($in{'oidx'} ne '') {
# From another module
@others = &get_other_module_logs($in{'omod'});
($other) = grep { $_->{'mindex'} == $in{'oidx'} } @others;
&can_edit_log($other) && $access{'others'} ||
&error($text{'save_ecannot2'});
if ($other->{'file'}) {
$file = $other->{'file'};
}
else {
$cmd = $other->{'cmd'};
}
}
elsif ($in{'extra'}) {
# Extra log file
($extra) = grep { $_->{'file'} eq $in{'extra'} } @extras;
$extra || &error($text{'save_ecannot7'});
&can_edit_log($extra) || &error($text{'save_ecannot2'});
$file = $extra->{'file'};
}
elsif ($in{'file'}) {
# Explicitly named file
$access{'any'} || &error($text{'save_ecannot6'});
$file = $in{'file'};
&can_edit_log($file) || &error($text{'save_ecannot2'});
}
else {
&error($text{'save_emissing'});
}
print "Refresh: $config{'refresh'}\r\n"
if ($config{'refresh'});
&ui_print_header("<tt>".&html_escape($file || $cmd)."</tt>",
$in{'linktitle'} || $text{'view_title'}, "", undef, undef, $in{'nonavlinks'});
$lines = $in{'lines'} ? int($in{'lines'}) : int($config{'lines'});
$filter = $in{'filter'} ? quotemeta($in{'filter'}) : "";
&filter_form();
$| = 1;
print "<pre>";
local $tailcmd = $config{'tail_cmd'} || "tail -n LINES";
$tailcmd =~ s/LINES/$lines/g;
if ($filter ne "") {
# Are we supposed to filter anything? Then use grep.
local @cats;
if ($cmd) {
# Getting output from a command
push(@cats, $cmd);
}
elsif ($config{'compressed'}) {
# All compressed versions
foreach $l (&all_log_files($file)) {
$c = &catter_command($l);
push(@cats, $c) if ($c);
}
}
else {
# Just the one log
@cats = ( "cat ".quotemeta($file) );
}
$cat = "(".join(" ; ", @cats).")";
if ($config{'reverse'}) {
$tailcmd .= " | tac";
}
$eflag = $gconfig{'os_type'} =~ /-linux/ ? "-E" : "";
$dashflag = $gconfig{'os_type'} =~ /-linux/ ? "--" : "";
if (@cats) {
$got = &proc::safe_process_exec(
"$cat | grep -i -a $eflag $dashflag $filter ".
"| $tailcmd",
0, 0, STDOUT, undef, 1, 0, undef, 1);
}
else {
$got = undef;
}
} else {
# Not filtering .. so cat the most recent non-empty file
if ($cmd) {
# Getting output from a command
$fullcmd = $cmd." | ".$tailcmd;
}
elsif ($config{'compressed'}) {
# Cat all compressed files
local @cats;
$total = 0;
foreach $l (reverse(&all_log_files($file))) {
next if (!-s $l);
$c = &catter_command($l);
if ($c) {
$len = int(&backquote_command(
"$c | wc -l"));
$total += $len;
push(@cats, $c);
last if ($total > $in{'lines'});
}
}
if (@cats) {
$cat = "(".join(" ; ", reverse(@cats)).")";
$fullcmd = $cat." | ".$tailcmd;
}
else {
$fullcmd = undef;
}
}
else {
# Just run tail on the file
$fullcmd = $tailcmd." ".quotemeta($file);
}
if ($config{'reverse'} && $fullcmd) {
$fullcmd .= " | tac";
}
if ($fullcmd) {
$got = &proc::safe_process_exec(
$fullcmd, 0, 0, STDOUT, undef, 1, 0, undef, 1);
}
else {
$got = undef;
}
}
print "<i>$text{'view_empty'}</i>\n" if (!$got);
print "</pre>\n";
&filter_form();
$in{'nonavlinks'} ? &ui_print_footer() :
&ui_print_footer("", $text{'index_return'});
exit;
}
sub filter_form
{
print &ui_form_start("view_log.cgi");
print &ui_hidden("nonavlinks", $in{'nonavlinks'} ? 1 : 0),"\n";
print &ui_hidden("linktitle", $in{'linktitle'}),"\n";
print &ui_hidden("oidx", $in{'oidx'}),"\n";
print &ui_hidden("omod", $in{'omod'}),"\n";
print &ui_hidden("file", $in{'file'}),"\n";
print &ui_hidden("extra", $in{'extra'}),"\n";
print &ui_hidden("view", 1),"\n";
# Create list of logs and selector
my @logfiles;
my $found = 0;
if ($access{'syslog'}) {
# Logs from syslog
my @systemctl_cmds = &get_systemctl_cmds();
foreach $c (@systemctl_cmds) {
next if (!&can_edit_log($c));
push(@logfiles, [ $c->{'id'}, "$c->{'cmd'}" ]);
$found++ if ($c->{'id'} eq $in{'idx'});
}
}
if ($config{'others'} && $access{'others'}) {
foreach my $o (&get_other_module_logs()) {
next if (!&can_edit_log($o));
next if (!$o->{'file'});
push(@logfiles, [ $o->{'file'} ]);
$found++ if ($o->{'file'} eq $file);
}
}
foreach $e (&extra_log_files()) {
next if (!&can_edit_log($e));
push(@logfiles, [ $e->{'file'} ]);
$found++ if ($e->{'file'} eq $file);
}
if (@logfiles && $found) {
$sel = &ui_select("idx", $in{'idx'} eq '' ? $file : $in{'idx'},
[ @logfiles ], undef, undef, undef, undef, "onChange='form.submit()'");
}
else {
$sel = "<tt>".&html_escape($in{'file'})."</tt>";
print &ui_hidden("idx", $in{'idx'}),"\n";
}
print &text('view_header', "&nbsp;" . &ui_textbox("lines", $lines, 3), "&nbsp;$sel"),"\n";
print "&nbsp;&nbsp;&nbsp;&nbsp;\n";
print &text('view_filter', "&nbsp;" . &ui_textbox("filter", $in{'filter'}, 25)),"\n";
print "&nbsp;&nbsp;\n";
print &ui_submit($text{'view_refresh'});
print &ui_form_end(),"<br>\n";
}