#! /usr/bin/perl -w
# /usr/sbin/dphys-setup.pl - install/upgrade packages
# authors dsbg and franklin, last modification 2004.02.12
#
# called by  cron  daily at 06:xx and by  init  at boot time
#   as first parameter ($ARGV[0]) is "cron" or "init" depending on caller
#
# controlled by /var/lib/dphys-admin/pkglist
#   # blah blah: comment, don't do anything, same for empty line
#   ? blah blah: write this text out to user
#   | cmd param param: execute this command by an shell
#   - pack1 pack2: delete packages
#   * pack1 pack2: delete packages with --purge
#   + pack1 pack2: install packages
#   ! pack1 pack2: install packages even if already installed (early upgrade)
#   < input pack1 pack2: install packages with  yes '$input'
#   pack1 pack2: install packages (old syntax for +, deprecated)
#
# we originally used an site package with an large Depends: list
#   unfortunately apt-get loads stuff in an unpredictaly row
#   this lead to conflicts where packages dif not find stuff they need
#   Predepends: was not reliable either, when we need something configured


# --- sanitise this place, else ssh rsh-style non-logins fail in dpkg

$ENV{PATH} = "/sbin:/bin:/usr/sbin:/usr/bin";


# --- parameters

# by who are we being called, cron or init
my $called_by = $ARGV[0];


# --- variables for filenames

# our local configs server, you may want to change this
#   this variable name and path are also used in endfirstrun
my $conf_baseurl = "http://debian.ethz.ch/pub/debian-local/share";
my $conf_hosturl = "$conf_baseurl/hostconfig/" . `hostname`;
chomp $conf_hosturl;
my $pkgpathdir = "/var/lib/dphys-admin";

# file with our pre main (host dependant) package list
my $prepkgfile = "host.pre.pkglist";
my $prepkgurl = "$conf_hosturl/$prepkgfile";
my $prepkgpath = "$pkgpathdir/$prepkgfile";
# file with our main (host independant) package list
my $mainpkgfile = "main.pkglist";
my $mainpkgurl = "$conf_baseurl/$mainpkgfile";
my $mainpkgpath = "$pkgpathdir/$mainpkgfile";
# file with our post main (host dependant) package list
my $postpkgfile = "host.post.pkglist";
my $postpkgurl = "$conf_hosturl/$postpkgfile";
my $postpkgpath = "$pkgpathdir/$postpkgfile";

# for merging the 3 above files
my $pkgfile = "merged.pkglist";
my $pkgpath = "$pkgpathdir/$pkgfile";

# logfile for voluminous output, to also analyse it later
my $logdir = "/var/log/";
my $logfile = "dphys-admin.log";
my $logpath = $logdir . $logfile;
# logfile backup, as the daily cron overwrites it
my $logfileb = "dphys-admin.log.bak";
my $logpathb = $logdir . $logfileb;

# flag file, any script can  touch  and so demand an reboot
#   reboot must be done after  apt-get  and  postinst  else we are screwed
my $need_reboot = "/root/NEED-REBOOT";
my $need_reboot_mail = "/root/NEED-REBOOT-MAIL";
# flag file, any script can  cat >>  and so demand an script
#   script run is intended for stuff after  apt-get  and  postinst
#   usually because this stuff wants to use the debconf database itsself
my $need_script = "/root/NEED-SCRIPT";


# --- variable for sending mails

my $mail_address = "bb\@phys.ethz.ch";


# --- logging

# begin output, better for processing and recognizing trouble
print("\ndphys-setup.pl is updating the software collection ...\n");

if ($called_by eq "cron") {
  # as we run by cron, log to an separate file and mail it to root
  #   keep old logfile content forone generation/day, as backup file
  rename $logpath, $logpathb; }
else {
  # OTOH reboot/init can happen arbitrariliy often, so no mails
  #   to not lise information, here allways add to old file, no 1 gen backup
  # 2 empty lines so there is still an separator, even if last \n was missing
  open(LOGPATH, ">>$logpath");
  print LOGPATH "\n"; print LOGPATH "\n";
  close(LOGPATH); }

# and now announce the start of this run
open(LOGPATH, ">>$logpath");
print LOGPATH "@@@ dphys-setup.pl $called_by started: " . `/bin/date -R` ."\n";
close(LOGPATH);


# --- package database update

# update package database first, so that we also can see new packages
print("updating package database ...\n");
open(LOGPATH, ">>$logpath");
print LOGPATH "@@@ package database update: " . `/bin/date -R` . "\n";
close(LOGPATH);
system("/usr/bin/nice /usr/bin/yes ''" .
  " | /usr/bin/apt-get update >>$logpath 2>>$logpath\n");
system("/usr/bin/apt-cache dumpavail > /var/cache/apt/available\n");
system("/usr/bin/dpkg --update-avail /var/cache/apt/available" .
  " >>$logpath 2>>$logpath\n");
unlink "/var/cache/apt/available";


# --- package list update

# fetch package list
print("fetching package list ...\n");
open(LOGPATH, ">>$logpath");
print LOGPATH "@@@ wget pkgfile: " . `/bin/date -R` . "\n";
close(LOGPATH);

# CVS keeps on deleting our empty directory, so create it from here if
if (! -d "$pkgpathdir") {
  if (-e "$pkgpathdir") {
    unlink "$pkgpathdir"; }
  mkdir "$pkgpathdir", 0700; }

# fetch the possibly updated package lists
if (-x "/usr/bin/wget") {
  system("/usr/bin/wget -O $prepkgpath  $prepkgurl " .
    " >>$logpath 2>>$logpath\n");
  system("/usr/bin/wget -O $mainpkgpath $mainpkgurl" .
    " >>$logpath 2>>$logpath\n");
  system("/usr/bin/wget -O $postpkgpath $postpkgurl" .
    " >>$logpath 2>>$logpath\n"); }

# merge pre|post package lists into main list
if (-f $prepkgpath || -f $mainpkgpath || -f $postpkgpath) {
  open(PKGPATH, ">$pkgpath");
  if (-f $prepkgpath) {
    open(PREPKGPATH, "<$prepkgpath");
    print PKGPATH while <PREPKGPATH>;
    close(PREPKGPATH); }
  if (-f $mainpkgpath) {
    open(MAINPKGPATH, "<$mainpkgpath");
    print PKGPATH while <MAINPKGPATH>;
    close(MAINPKGPATH); }
  if (-f $postpkgpath) {
    open(POSTPKGPATH, "<$postpkgpath");
    print PKGPATH while <POSTPKGPATH>;
    close(POSTPKGPATH); }
  close(PKGPATH); }

if (! -f $pkgpath || -z $pkgpath) {
  # package list not (and so never) fetched, or empty, so we can not work
  print("no or empty $pkgpath packge list, bailing out ...\n");
  # log this error so that we know it
  open(LOGPATH, ">>$logpath");
  print LOGPATH "@@@ ERROR: missing or empty pkgfile\n";
  close(LOGPATH);
  # warn the admin if we can, and then send the abortive logfile
  my $mail_subject = "dphys-admin $called_by ERROR no pkglist on " .
    `hostname`;
  open(MAIL, "|/usr/bin/mail -s \"$mail_subject\" $mail_address");
  print MAIL "dphys-setup.pl found no (or empty) $pkgpath\n";
  close(MAIL);

  # and bail out
  exit (1); }


# --- collect already installed stuff, to later eliminate it from installing

my %installed;
open(DPKGGETSEL, "dpkg --get-selections|");

while (<DPKGGETSEL>) {

  my $package;
  my $state;
  chomp;
  ($package, $state) = split;
  $installed{$package} = $state; }

close(DPKGGETSEL);


# --- the actual work

# evaluate package list and load/eliminate packages
print("installing or removing packages in $pkgpath ...\n");
open(LOGPATH, ">>$logpath");
print LOGPATH "@@@ install/remove: " . `/bin/date -R` . "\n";
close(LOGPATH);
open(PKGLIST, "<$pkgpath") or die "Can't open $pkgpath";

while (<PKGLIST>) {

  chomp;


  # eliminate blanks at begin and end of line
  s/^\s+(.*)s+$/$1/;

  # empty line, ignore
  if (/^$/) {
    next; }


  # get first character, which says what to do with this line
  m/^(.)(.*)$/;


  #  #  = comment line, ignore
  if ($1 eq "\043") {
    next; }


  #  ?  = tell user what is happening, print it out
  elsif ($1 eq "?") {

    # first newline to terminate line of progress log lines
    print "?\n$2\n"; }


  #  |  = execute (pipe) of any random command
  elsif ($1 eq "|") {

    # first newline to terminate line of progress log lines
    print "|\n";
    system("$2\n"); }


  #  -  = uninstall (subtract) some packages
  #  *  = destroy ("splat", squashed insect) some packages
  elsif ($1 eq "-" || $1 eq "*") {

    my $operation;
    my $packages;
    if ($1 eq "-") {
      $operation = "--remove"; }
    else {
      $operation = "--purge"; }
    $packages = $2;

    # do not remove stuff that is not installed, to save time an error messages
    # split list of $packages, one per line ...
    my @packages;
    @packages = split(/\s+/, $packages);
    # ... elliminate the not installed ones ...
    foreach $package (@packages) {
      if (! exists $installed{$package}) {
        $package = ""; } }
    # ... and put the remaining back together
    $packages = join (" ", @packages);
    # elliminate leading spaces (= all spaces if nothing left, -> empty)
    $packages =~ s/^\s+//;
    # if no packages now empty string -> nothing to remove
    if ($packages eq "") {
      next; }

    system("/usr/bin/nice /usr/bin/yes ''" .
      " | /usr/bin/dpkg $operation $packages >>$logpath 2>>$logpath\n");
    print $1; }


  #  +  = install (add) some packages
  #  !  = install ("yes", "really"), even if already installed (early upgrade)
  #  <  = input ("stdin") install some packages with stdin input provided
  #     = also normal install, old syntax

  else {

    my $onlynew;
    my $input;
    my $packages;
    if ($1 eq "+") {
      $onlynew = 1; $input = ""; $packages = $2; }
    elsif ($1 eq "!") {
      $onlynew = 0; $input = ""; $packages = $2; }
    elsif ($1 eq "<") {
      # split into first character, and first word, and rest of line
      m/^(.) ([^ ]*) (.*)$/;
      $onlynew = 1; $input = "$2"; $packages = $3; }
    else {
      $onlynew = 1; $input = ""; $packages = $_; }

    # do not install stuff that is already installed, to save time
    #   unless forced install for forcing early upgrade, before rest upgraded
    if ($onlynew == 1) {
      # split list of $packages, one per line ...
      my @packages;
      @packages = split(/\s+/, $packages);
      # ... elliminate the already installed ones ...
      foreach $package (@packages) {
        if (exists $installed{$package}) {
          $package = ""; } }
      # ... and put the remaining back together
      $packages = join (" ", @packages);
      # elliminate leading spaces (= all spaces if nothing left, -> empty)
      $packages =~ s/^\s+//;
      # if no packages now empty string -> nothing to install
      if ($packages eq "") {
        next; } }

    system("/usr/bin/nice /usr/bin/yes '$input'" .
      " | /usr/bin/apt-get install $packages >>$logpath 2>>$logpath\n");
    print "+"; }

  if (-f $need_script) {
    # one or more of the installed packages wants a script run
    open(LOGPATH, ">>$logpath");
    print LOGPATH "@@@ install need script(s): " . `/bin/date -R` . "\n";
    close(LOGPATH);

    # first newline to terminate line of progress log lines
    print "\n";

    open(NEEDSCRIPT, "<$need_script") or die "Can't open $need_script";
    while ($script = <NEEDSCRIPT>) {
      system($script); }
    close(NEEDSCRIPT);

    unlink $need_script; }

  if (-f $need_reboot) {
    # one or more of the installed packages wants a reboot

    if ($called_by eq "init") {
      # if User is booting anyway, and so not in the middle of working
      #   we can reboot immediately without any question
      # if nightly cron update, and so perhaps a user is working
      #   we are not allowed to reboot, as this would interrupt work
      #   so no reboot, and file not deleted, test and mail later
      open(LOGPATH, ">>$logpath");
      print LOGPATH "@@@ install need reboot: " . `/bin/date -R` . "\n";
      close(LOGPATH);

      unlink $need_reboot;
      system("/sbin/reboot\n");
      # as /sbin/reboot only sends signal, then returns
      #   prevent further action before reboot, resulting in partial install
      exit (0); }

    else {
      # for now just log that it would have happend
      open(LOGPATH, ">>$logpath");
      print LOGPATH "@@@ inst need reboot *ignored*: " . `/bin/date -R` . "\n";
      close(LOGPATH);
      # warned about ignore, now we need the mail
      rename $need_reboot, $need_reboot_mail; } } }


# finish off all the progress log lines
print "\n";

close(PKGLIST);


# --- apt-get upgrade the already installed packages

# is after install/remove/purge, so we do not upgrade "vanishing" packages
print("dist upgrading existing packages ...\n");
open(LOGPATH, ">>$logpath");
print LOGPATH "@@@ apt-get dist-upgrade: " . `/bin/date -R` . "\n";
close(LOGPATH);
system("/usr/bin/nice /usr/bin/yes ''" .
  " | /usr/bin/apt-get dist-upgrade >>$logpath 2>>$logpath\n");

if (-f $need_script) {
  # one or more of the upgraded packages wants a script run
  open(LOGPATH, ">>$logpath");
  print LOGPATH "@@@ update need script(s): " . `/bin/date -R` . "\n";
  close(LOGPATH);

  open(NEEDSCRIPT, "<$need_script") or die "Can't open $need_script";
  while ($script = <NEEDSCRIPT>) {
    system($script); }
  close(NEEDSCRIPT);

  unlink $need_script; }

if (-f $need_reboot) {
  # one or more of the upgraded packages wants a reboot

  if ($called_by eq "init") {
    open(LOGPATH, ">>$logpath");
    print LOGPATH "@@@ update need reboot: " . `/bin/date -R` . "\n";
    close(LOGPATH);

    unlink $need_reboot;
    system("/sbin/reboot\n");
    # as /sbin/reboot only sends signal, then returns
    #   prevent further action before reboot, resulting in partial install
    exit (0); }

  else {
    # for now just log that it would have happend
    open(LOGPATH, ">>$logpath");
    print LOGPATH "@@@ update need reboot *ignored*: " . `/bin/date -R` . "\n";
    close(LOGPATH);
    # warned about ignore, now we need the mail
    rename $need_reboot, $need_reboot_mail; } }


# --- warn admin if some package wanted reboot and was ignored

# if reboot was not done (and flag file renamed instead of deleted)
if (-f $need_reboot_mail) {
  # this will catch this case and mail the admin for help
  open(LOGPATH, ">>$logpath");
  print LOGPATH "@@@ need reboot *mailed*: " . `/bin/date -R` . "\n";
  close(LOGPATH);

  my $mail_subject = "dphys-admin $called_by WARNING needreboot on " .
    `hostname`;
  open(MAIL, "|/usr/bin/mail -s \"$mail_subject\" $mail_address");
  print MAIL "phys-setup.pl has made an $need_reboot file, not rebooted\n";
  close(MAIL);
  # sent the mail, so we now again need reboot (and init.d deletes after that)
  rename $need_reboot_mail, $need_reboot; }


# --- apt-get clean to free disk space, unlikely to re-install, and we have LAN

# is after upgrade, so nothing added after cleaning
print("cleaning apt cache ...\n");
open(LOGPATH, ">>$logpath");
print LOGPATH "@@@ apt-get clean: " . `/bin/date -R` . "\n";
close(LOGPATH);
system("/usr/bin/nice /usr/bin/yes ''" .
  " | /usr/bin/apt-get clean >>$logpath 2>>$logpath\n");


# --- final logging

# tell waiting user that we are finished
print("\ndphys-setup.pl is done with upgrading software collection\n");

# and log to file that we are finished
open(LOGPATH, ">>$logpath");
print LOGPATH "@@@ dphys-setup.pl $called_by ended: " . `/bin/date -R` . "\n";
close(LOGPATH);

# and also mail log to admin that we are finished
my $mail_subject = "dphys-admin $called_by INFO has run on " . `hostname`;
open(MAIL, "|/usr/bin/mail -s \"$mail_subject\" $mail_address");
open(LOGPATH, "<$logpath");
print MAIL while <LOGPATH>;
close(LOGPATH);
close(MAIL);
