diff --git a/aii-ks/src/main/pan/quattor/aii/ks/schema.pan b/aii-ks/src/main/pan/quattor/aii/ks/schema.pan index e572004a..b3c2b0dc 100644 --- a/aii-ks/src/main/pan/quattor/aii/ks/schema.pan +++ b/aii-ks/src/main/pan/quattor/aii/ks/schema.pan @@ -32,10 +32,22 @@ type structure_ks_ksfirewall = { }; # Information needed for logging into syslog +# Anaconda syslog uses UDP type structure_ks_logging = { "host" : type_hostname "port" : type_port = 514 "level" ? string with match(SELF, "^(debug|warning|error|critical|info)$") + + "console" : boolean = true # redirect AII ks logfile to console + + # send AII ks logfile to host/port + "send_aiilogs" : boolean = false + + # use legacy defaults + # via bash or netcat + "method" : string = 'netcat' with match(SELF, '^(bash|netcat)$') + # via tcp or udp + "protocol" : string = 'udp' with match(SELF, '^(tcp|udp)$') }; # Information needed for creating the Kickstart file diff --git a/aii-ks/src/main/perl/ks.pm b/aii-ks/src/main/perl/ks.pm index 981eacdd..b14e7c2d 100755 --- a/aii-ks/src/main/perl/ks.pm +++ b/aii-ks/src/main/perl/ks.pm @@ -77,6 +77,15 @@ use constant { KS => "/system/aii/osinstall/ks", use constant MODULEBASE => "AII::"; use constant USEMODULE => "use " . MODULEBASE; +# use this syslogheader for remote AII scripts logging: +# 190 = local7.info +use constant LOG_ACTION_SYSLOGHEADER => '<190>AII: '; +# awk command to prefix LOG_ACTION_SYSLOGHEADER and +# to insert sleep (usleep by initscripts), throtlles to 40 lines per sec +use constant LOG_ACTION_AWK => + "awk '{print \"".LOG_ACTION_SYSLOGHEADER."\"\$0; fflush(); system(\"usleep 25000 >& /dev/null\");}'"; + + # Configuration variable for the osinstall directory. use constant KSDIROPT => 'osinstalldir'; @@ -217,7 +226,8 @@ sub kscommands my $config = shift; my $tree = $config->getElement(KS)->getTree; - + my @packages = @{$tree->{packages}}; + my $installtype = $tree->{installtype}; if ($installtype =~ /http/) { my ($proxyhost, $proxyport, $proxytype) = proxy($config); @@ -252,6 +262,11 @@ EOF "--port=$tree->{logging}->{port}"; print " --level=$tree->{logging}->{level}" if $tree->{logging}->{level}; print "\n"; + if(exists($tree->{logging}->{send_aiilogs}) && $tree->{logging}->{send_aiilogs}) { + # requirement for usleep + push(@packages, 'initscripts'); + push(@packages, 'nc') if ($tree->{logging}->{method} eq 'netcat'); + } } print "bootloader --location=$tree->{bootloader_location}"; print " --driveorder=", join(',', @{$tree->{bootdisk_order}}) @@ -312,7 +327,7 @@ EOF print "services ", join (' ', @services), "\n" if (@services); print "%packages ", join(" ",@{$tree->{packages_args}}), "\n", - join ("\n", @{$tree->{packages}}), "\n", + join ("\n", @packages), "\n", $config->getElement(END_SCRIPT_FIELD)->getValue(), "\n"; } @@ -397,12 +412,69 @@ EOF kscommands ($config); } +# Create the action to be taken on the log files +# logfile is the path to the log file +sub log_action { + my ($config, $logfile, $wait_for_network) = @_; + + my $tree = $config->getElement(KS)->getTree; + my @logactions; + push(@logactions, "exec >$logfile 2>&1"); + + my $consolelogging = 1; # default behaviour + if (exists($tree->{logging})) { + $consolelogging = $tree->{logging}->{console} if(exists($tree->{logging}->{console})); + + if (exists($tree->{logging}->{send_aiilogs}) && $tree->{logging}->{send_aiilogs}) { + # network must be functional + # (not needed in %pre and %post; we can rely on anaconda for that) + push(@logactions, "wait_for_network $tree->{logging}->{host}") + if ($wait_for_network); + + my $method = $tree->{logging}->{method}; + my $protocol = $tree->{logging}->{protocol}; + + my $actioncmd; + if ($method eq 'netcat') { + push(@logactions,'# Send messages to $protocol syslog server via netcat'); + # use netcat to log to syslog port + my $nccmd = 'nc'; + $nccmd .= ' -u' if ($protocol eq 'udp'); + + $actioncmd = "| $nccmd $tree->{logging}->{host} $tree->{logging}->{port}"; + } elsif ($method eq 'bash') { + push(@logactions,"# Send messages to $protocol syslog server via bash /dev/$protocol"); + # use netcat to log to UDP syslog port + # this assumes that the %pre, %post and post-reboot are bash + $actioncmd = "> /dev/$protocol/$tree->{logging}->{host}/$tree->{logging}->{port}"; + } + + my $action = "(tail -f $logfile | ".LOG_ACTION_AWK." $actioncmd) &"; + push(@logactions, $action); + + # insert extra sleep to get all started before any output is send + push(@logactions, 'sleep 1'); + } + } + + if ($consolelogging) { + push(@logactions, '# Make sure messages show up on the serial console', + "tail -f $logfile > /dev/console &"); + } + + push(@logactions,''); # add trailing newline + return join("\n", @logactions) +} + # Takes care of the pre-install script, in which the sub pre_install_script { my ($self, $config) = @_; - print <<'EOF'; + my $logfile = '/tmp/pre-log.log'; + my $logaction = log_action($config, $logfile); + + print </tmp/pre-log.log 2>&1 -tail -f /tmp/pre-log.log > /dev/console & +$logaction +echo 'Begin of pre section' set -x +EOF + + print <<'EOF'; + # Hack for RHEL 6: force re-reading the partition table # # fdisk often fails to re-read the partition table on RHEL 6, so we have to do @@ -490,8 +565,11 @@ EOF # De-activate logical volumes. Needed on RHEL6, see: # https://bugzilla.redhat.com/show_bug.cgi?id=652417 lvm vgchange -an +echo 'End of pre section' $end + EOF + } # Prints the code needed for removing and creating partitions, block @@ -602,25 +680,35 @@ sub kspostreboot_header { my $config = shift; + # TODO is it ok to rename this logfile? + my $logfile = '/root/ks-post-reboot.log'; + my $logaction = log_action($config, $logfile, 1); + $logaction =~ s/\$/\\\$/g; + my $hostname = $config->getElement (HOSTNAME)->getValue; my $domain = $config->getElement (DOMAINNAME)->getValue; + my $fqdn = "$hostname.$domain"; + my $rootmail = $config->getElement (ROOTMAIL)->getValue; + print </dev/null|xargs tail /var/log/spma.log\\` @@ -632,37 +720,44 @@ End_of_sendmail # Function to be called if the installation succeeds. It sends an # e-mail to $rootmail alerting about the installation success. success() { + echo "Quattor installation on $fqdn succeeded" sendmail -t < /dev/null + do + sleep 1 + let i=\\\$i+1 + if [ \\\$i -gt 120 ] + then + fail "Network does not come up (nslookup \\\$1)" + fi + done +} + # Ensure that the log file doesn't exist. -[ -e /root/ks-post-install.log ] && \\ - fail "Last installation went wrong. Aborting. See logfile" +[ -e $logfile ] && \\ + fail "Last installation went wrong. Aborting. See logfile $logfile." -exec &> /root/ks-post-install.log -tail -f /root/ks-post-install.log &>/dev/console & +$logaction +echo 'Begin of ks-post-reboot' set -x -# Wait up to 2 minutes until the network comes up -i=0 -while ! nslookup \\`hostname\\` > /dev/null -do - sleep 1 - let i = \\\$i+1 - if [ \\\$i -gt 120 ] - then - fail "Network does not come up" - fi -done +wait_for_network $fqdn EOF + } sub ksquattor_config @@ -746,6 +841,8 @@ sub kspostreboot_tail print </tmp/post-log.log -tail -f /tmp/post-log.log > /dev/console & EOF @@ -1061,7 +1162,12 @@ EOF ksuserhooks ($config, PREREBOOTHOOK); my $end = $config->getElement(END_SCRIPT_FIELD)->getValue(); - print "$end\n"; + print < and C<%post> and C<%packages> section +(usually either empty or "%end"). Early Anaconda versions (< 6) did not have such a closing +tag. Later the use of %end became optional, and recent Anaconda releases made it mandatory. =item * part_label: boolean @@ -185,6 +179,10 @@ See below See below +=item * logging + +See below + =back =head3 Firewall configuration @@ -265,6 +263,64 @@ Default color depth, in bits. Defaults to 24. =back +All paths are relative to C + +=head3 Logging configuration + +Two logging paths exist: the Anaconda logging via an Anaconda +syslog and one specific to the logs produced by the AII scripts +in C<%pre>, C<%post> and post-reboot. + +For the Anaconda syslog, one can set the remote host and port, and +optional loglevel, but not the used protocol. For SL versions < 7, UDP +is implied, for versions >= 7 TCP is used. + +The logs produced by AII scripts can be send to the console and to +the same remote syslog server as used by the Anaconda syslog. +For the remote logging of the AII logs, +the protocol can be chosen (and should probably match the one used +by the Anaconda syslog). + +=over 4 + +=item * host : string + +The remote syslog server name used by Anaconda syslog (and optional +AII remote scripts logging). + +=item * port : integer + +The portnumber of the remote syslog server used by Anaconda syslog (and optional +AII remote scripts logging). + +=item * level : string + +The Anaconda syslog level that send to the remote server. Level can be one of +C, C, C, C or C. + +=item * console : boolean + +Redirect the AII script logs to the console. (This is the default if this boolean +if not defined). + +=item * send_aiilogs : boolean + +Send or do not send the AII script logs to the remote host + +=item * method : string + +Method that is used to send the AII script logs to the remote server. Has to be either +C or C. For SL 7>= bash should be used, for lower versions, netcat should be used +(although bash might work too). + +=item * protocol : string + +Protocol that is used to send the AII script logs to the remote server. Has to be either +C or C. Ideally this is equal to the the protocol that Anaconda syslog uses: +for SL 7>= tcp should be used, for lower versions, udp should be used. + +=back + =head2 Variables =over diff --git a/aii-ks/src/test/perl/kickstart_logging.t b/aii-ks/src/test/perl/kickstart_logging.t new file mode 100644 index 00000000..0f430c79 --- /dev/null +++ b/aii-ks/src/test/perl/kickstart_logging.t @@ -0,0 +1,44 @@ +use strict; +use warnings; +use Test::More; +use Test::Quattor qw(kickstart_logging); +use NCM::Component::ks; +use CAF::FileWriter; +use CAF::Object; + +=pod + +=head1 SYNOPSIS + +Tests for the C method with emphasis on logging. + +=cut + +$CAF::Object::NoAction = 1; + +my $fh = CAF::FileWriter->new("target/test/ks"); +# This module simply prints to the default filehandle. +select($fh); + +my $ks = NCM::Component::ks->new('ks'); +my $cfg = get_config_for_profile('kickstart_logging'); + +NCM::Component::ks::kscommands($cfg); + +like($fh, qr{^logging\s--host=logserver\s--port=514\s--level=debug}m, 'logging present'); + +# netcat enabled, add nc and initscripts to packages that are required in %post and postreboot +like($fh, qr{^%packages\s--ignoremissing\s--resolvedeps\n^package\n^package2\n^initscripts\n^nc\n^EENNDD\n}m, 'installtype present'); + +# logaction tests +my $logaction = NCM::Component::ks::log_action($cfg, 'mylogfile', 1); +like($logaction, qr{^exec\s>mylogfile\s2>&1}, 'start with exec redirection'); # no multiline search! +like($logaction, qr{^tail\s-f\smylogfile\s>\s/dev/console\s&}m, 'console logging enabled'); + +like($logaction, qr{^wait_for_network\slogserver}m, 'Insert sleep to make sure network is up'); +like($logaction, qr{^\(tail\s-f\smylogfile.*?usleep.*?\snc\s-u\slogserver\s514\)\s&$}m, 'netcat udp logsending'); +like($logaction, qr{^sleep\s\d+$}m, 'sleep inserted to allow start'); + + + +done_testing(); diff --git a/aii-ks/src/test/resources/kickstart_logging.pan b/aii-ks/src/test/resources/kickstart_logging.pan new file mode 100644 index 00000000..193c271f --- /dev/null +++ b/aii-ks/src/test/resources/kickstart_logging.pan @@ -0,0 +1,16 @@ +@{ +Profile to ensure that the kickstart commands and packages section are generated +@} +object template kickstart_logging; + +include 'kickstart'; + +prefix "/system/aii/osinstall/ks/logging"; +"host" = "logserver"; +"port" = 514; +"level" = "debug"; +"console" = true; + +"send_aiilogs" = true; +"method" = 'netcat'; +"protocol" = 'udp'; diff --git a/aii-pxelinux/src/main/perl/pxelinux.pm b/aii-pxelinux/src/main/perl/pxelinux.pm index fcaa7a5f..6f522b31 100755 --- a/aii-pxelinux/src/main/perl/pxelinux.pm +++ b/aii-pxelinux/src/main/perl/pxelinux.pm @@ -36,6 +36,8 @@ use constant REMOVE_HOOK_PATH => '/system/aii/hooks/remove'; use constant BOOT_HOOK_PATH => '/system/aii/hooks/boot'; use constant FIRMWARE_HOOK_PATH => '/system/aii/hooks/firmware'; use constant LIVECD_HOOK_PATH => '/system/aii/hooks/livecd'; +# Kickstart constants (trying to use same name as in ks.pm from aii-ks) +use constant KS => "/system/aii/osinstall/ks"; our @ISA = qw (NCM::Component); our $EC = LC::Exception::Context->new->will_store_all; @@ -80,26 +82,55 @@ sub link_filepath return undef; } - -# Writes the PXE configuration file. -sub pxeprint +# create a list with all append options +sub pxe_append { my $cfg = shift; + my $t = $cfg->getElement (PXEROOT)->getTree; + + my $kst = {}; # empty hashref in case no kickstart is defined + $kst = $cfg->getElement (KS)->getTree if $cfg->elementExists(KS); + my $ksloc = $t->{kslocation}; my $server = hostname(); $ksloc =~ s{LOCALHOST}{$server}; + + my @append; + push(@append, + "ramdisk=32768", + "initrd=$t->{initrd}", + "ks=$ksloc", + "ksdevice=$t->{ksdevice}" + ); + + if (exists($kst->{logging})) { + push(@append, "syslog=$kst->{logging}->{host}:$kst->{logging}->{port}"); + push(@append, "loglevel=$kst->{logging}->{level}") if $kst->{logging}->{level}; + } + + push(@append, $t->{append}) if exists $t->{append}; + + return @append; +} + +# Prints the PXE configuration file. +sub pxeprint +{ + my $cfg = shift; + my $t = $cfg->getElement (PXEROOT)->getTree; my $fh = CAF::FileWriter->open (filepath ($cfg), - log => $this_app, - mode => 0644); - $t->{append} = "" unless exists $t->{append}; + log => $this_app, mode => 0644); + + my $appendtxt = join(" ", pxe_append($cfg)); + $fh->print (<{label} label $t->{label} kernel $t->{kernel} - append ramdisk=32768 initrd=$t->{initrd} ks=$ksloc ksdevice=$t->{ksdevice} $t->{append} + append $appendtxt EOF ); diff --git a/aii-pxelinux/src/test/perl/pxelinux_block.t b/aii-pxelinux/src/test/perl/pxelinux_block.t index a5eded83..6526e4a5 100644 --- a/aii-pxelinux/src/test/perl/pxelinux_block.t +++ b/aii-pxelinux/src/test/perl/pxelinux_block.t @@ -31,7 +31,7 @@ my $fh = get_file($fp); like($fh, qr{^default\skernel\slabel}m, 'default kernel'); like($fh, qr{^\s{4}label\skernel\slabel}m, 'label default kernel'); like($fh, qr{^\s{4}kernel\smykernel}m, 'kernel mykernel'); -like($fh, qr{^\s{4}append\sramdisk=32768\sinitrd=path/to/initrd\sks=http://server/ks\sksdevice=eth0}m, 'append line'); +like($fh, qr{^\s{4}append\sramdisk=32768\sinitrd=path/to/initrd\sks=http://server/ks\sksdevice=eth0(\s|$)}m, 'append line'); done_testing(); diff --git a/aii-pxelinux/src/test/perl/pxelinux_logging.t b/aii-pxelinux/src/test/perl/pxelinux_logging.t new file mode 100644 index 00000000..93714de4 --- /dev/null +++ b/aii-pxelinux/src/test/perl/pxelinux_logging.t @@ -0,0 +1,34 @@ +use strict; +use warnings; +use Test::More; +use Test::Quattor qw(pxelinux_logging); +use NCM::Component::pxelinux; +use CAF::FileWriter; +use CAF::Object; + +=pod + +=head1 SYNOPSIS + +Tests for the C method. + +=cut + +$CAF::Object::NoAction = 1; + +# mock filepath, it has this_app->option +my $fp = "target/test/pxelinux"; +my $mockpxe = Test::MockModule->new('NCM::Component::pxelinux'); +$mockpxe->mock('filepath', $fp); + +my $ks = NCM::Component::pxelinux->new('pxelinux'); +my $cfg = get_config_for_profile('pxelinux_logging'); + +NCM::Component::pxelinux::pxeprint($cfg); + +my $fh = get_file($fp); + +like($fh, qr{^\s{4}append\s.*?\ssyslog=logserver:514\sloglevel=debug(\s|$)}m, 'append line'); + + +done_testing(); diff --git a/aii-pxelinux/src/test/resources/pxelinux_logging.pan b/aii-pxelinux/src/test/resources/pxelinux_logging.pan new file mode 100644 index 00000000..0be3b05d --- /dev/null +++ b/aii-pxelinux/src/test/resources/pxelinux_logging.pan @@ -0,0 +1,8 @@ +object template pxelinux_logging; + +include 'pxelinux'; + +prefix "/system/aii/osinstall/ks/logging"; +"host" = "logserver"; +"port" = 514; +"level" = "debug";