#!/usr/bin/perl -w
#
# Copyright (c) 2000-2019 University of Utah and the Flux Group.
# 
# {{{EMULAB-LICENSE
# 
# This file is part of the Emulab network testbed software.
# 
# This file is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
# 
# This file is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public
# License for more details.
# 
# You should have received a copy of the GNU Affero General Public License
# along with this file.  If not, see <http://www.gnu.org/licenses/>.
# 
# }}}
#
use English;
use Getopt::Std;

#
# Prepare the node for cutting a new image. Should be run just prior, on
# each of the FreeBSD and Linux partitions.
#
my $DUMPDATES   = "/etc/dumpdates";
my @DBFILES     = ();
my @SYSVARDIRS  = ("backups", "lib/dhcp", "lib/dhclient", "lib/dhcp3",
		   "spool/clientmqueue",
		   "log/audit", "log/account", "log/mysql", "log/ntpstats");
my @VARDIRS	= ("logs", "db", "jails", "boot", "lock", "vms");
my @SOCKETS	= ("/dev/log", "/var/run/acpid.socket");
my $FSTAB       = "/etc/fstab";
my $LOGDIR      = "/var/log";
my $RUNDIR      = "/var/run";
my $HISTORY     = ".bash_history";
my $MAILDIR     = "/var/spool/mail";
my $NTPDRIFT1   = "/etc/ntp/drift";
my $NTPDRIFT2   = "/var/lib/ntp/drift";
my $NTPDRIFT3   = "/var/lib/ntp/ntp.drift";
my $SFSUSERS    = "/etc/sfs/sfs_users";
my $SFSHOSTKEY  = "/etc/sfs/sfs_host_key";
my $SSHDCONFIG  = "/etc/ssh/sshd_config";
my $VARACCTDIR  = "/var/account";
my $VARACCTSDIR = "/var/log/sa";
my $IFTAB       = "/etc/iftab";
my $ANACRON     = "/usr/sbin/anacron";
my $GRUBDEF	= "/etc/default/grub";
my $ROOTSSHDIR	= "/root/.ssh";
my $ROOTSSLDIR	= "/root/.ssl";
my $PUBSUBD     = "/usr/local/libexec/pubsubd";
my $PUBSUBCONF  = "/usr/local/etc/pubsubd.conf";
my $PUBSUBEXPR  = "/usr/local/etc/pubsubd.expr";
#
# If $PUBSUBD is not in /usr/local and is in /usr, swap config file
# locations.
#
if (! -e $PUBSUBD && -e "/usr/libexec/pubsubd") {
    $PUBSUBD     = "/usr/libexec/pubsubd";
    $PUBSUBCONF  = "/etc/pubsubd.conf";
    $PUBSUBEXPR  = "/etc/pubsubd.expr";
}

#
# Dead wood in $BINDIR
# If you remove/move a script, list it here so that it goes away when the
# image is remade.
#
my @DEADFILES	= ("rc.agents", "rc.delayagent", "rc.delta", "rc.healthd",
		   "rc.injail", "rc.ipod", "rc.mfs", "rc.progagent",
		   "rc.setup", "rc.slothd", "rc.testbed",
		   "batchcmddone", "bootsetup", "install-tarfile",
		   "jailsetup", "update_delays",
		   # replaced by sethostname.dhclient
		   "sethostname",
		   # now in /usr/local/bin
		   "frisbee", "frisupload"
		   );

# Drag in path stuff so we can find emulab stuff.
BEGIN { require "/etc/emulab/paths.pm"; import emulabpaths; }

#
# Turn off line buffering on output
#
$| = 1;

#
# Load the OS independent support library. It will load the OS dependent
# library and initialize itself. 
# 
use libsetup;
use liblocsetup;

#
# XXX Elabinelab hack: option to not unmount NFS filesystems.
#     The setup script assumes that /share is mounted after this runs.
#
my $noumount = 0;
my $updatemasterpasswdfiles = 0;
my %options = ();
if (getopts("NM", \%options)) {
    if (defined($options{"N"})) {
	$noumount = 1;
    }
    if (defined($options{"M"})) {
	$updatemasterpasswdfiles = 1;
    }
}
#
# Allow the updatemasterpasswdfiles flag to be passed along indirectly.
# This file gets deleted below. We can touch this file from boss before
# starting an image capture (before prepare runs) and not mess up old disk
# images.
#
if (-e "$RUNDIR/updatemasterpasswdfiles") {
    $updatemasterpasswdfiles = 1;
}
my $isvm = ((-e "$ETCDIR/genvmtype") ? 1 : 0);

#
# Really first, look for user prepare prehooks and run them.
#
my @PHOOKSDIRS = ("$DYNRUNDIR/prepare.pre.d","$BINDIR/prepare.pre.d");
for my $pdir (@PHOOKSDIRS) {
    my @sfiles = ();
    my $rc = sortedlistallfilesindir($pdir,\@sfiles,1);
    if (defined($rc) && $rc == 0) {
	for my $file (@sfiles) {
	    print "Running prepare hook $file ...\n";
	    system("$file");
	}
    }
}

#
# First clean up the node as it would be if free.
#
cleanup_node(1);

print "Running $BINDIR/rc/rc.config to clean up ...\n";
my $rcconfig_args = "";
if ($updatemasterpasswdfiles) {
    $rcconfig_args = "-M";
}
system("$BINDIR/rc/rc.config $rcconfig_args reset");

print "Removing old DB files ...\n";
foreach my $dbfile (@DBFILES) {
    if (-e $dbfile) {
	unlink($dbfile) or
	    die("Could not unlink $dbfile");
    }
}

#
# Attempt to unmount all NFS filesystem if not already unmounted.
# This allows us to clean up /users and /proj.  We only do the cleanup if
# if unmount succeeds and even then, we do it in a safe way.
#
if (!$noumount) {
    print "Unmounting NFS filesystems ...\n";
    if (!system("umount -at nfs")) {
	foreach my $dir ("/users", "/proj", "/groups", "/scratch") {
	    # Yes, this can happen.
	    next
		if (! -e $dir);
	    
	    if (!os_islocaldir("$dir")) {
		die("$dir did not unmount; aborting cleanup");
	    }
	    opendir(DIR, $dir) or
		die("Could not open directory $dir: $!");

	    while ($dirent = readdir(DIR)) {
		next
		    if ($dirent eq "." || $dirent eq "..");
		
		my $file = $dir . "/" . $dirent;
		if (-d $file) {
		    if (!os_islocaldir("$file")) {
			die("$dir did not unmount; aborting cleanup");
		    }
		    print "Removing local dir: $file\n";
		    system("/bin/rm -rf $file\n");
		    if ($?) {
			die("Could not rm -rf $dir/*");
		    }
		}
	    }
	}
    } else {
	print "Could not determine if NFS filesystems are unmounted, ".
	      "NOT cleaning up /proj and /users\n";
    }
}

#
# Run anacron to bring everything up to date. It never runs again.
#
# XXX anacron, at least later versions, insists on sending mail and there
# is no way to disable it.  Since FC6 doesn't run sendmail, anacrons attempts
# to send mail will hang and eventually timeout.  This makes things take a
# lot longer, so we hack and temporarily move sendmail while we run anacron!
#
if (-x "$ANACRON") {
    if (-x "/usr/sbin/sendmail") {
	my $didrename = 0;
	print "Running cron jobs ".
	    "(might see failed attempts to mail output).\n";
	if (rename("/usr/sbin/sendmail", "/usr/sbin/_sendmail")) {
	    $didrename = 1;
	}
	system("$ANACRON -d -f -s -n");
	if ($didrename) {
	    rename("/usr/sbin/_sendmail", "/usr/sbin/sendmail");
	}
    } else {
	print "Running cron jobs.\n";
	system("$ANACRON -d -f -s -n");
    }
}

#
# Update man-db.
#
if (-x "/usr/bin/mandb") {
    print "Updating man db ...\n";
    system("/usr/bin/mandb -q");
}

#
# Update pci ids file.
#
if (-x "/usr/sbin/update-pciids") {
    print "Updating PCI IDs file ...\n";
    system("/usr/sbin/update-pciids");
}

#
# Create locate and makewhatis DBs, which are no longer in weekly cron.
#
if (-x "/etc/cron.pend/makewhatis.cron") {
    print "Building makewhatis DB. This could take a few minutes ...\n";
    system("/etc/cron.pend/makewhatis.cron");
}
if (-x "/etc/cron.pend/slocate.cron") {
    print "Building locate DB. This could take a few minutes ...\n";
    system("/etc/cron.pend/slocate.cron");
}

#
# Remove old swap lines from /etc/fstab
#
if (-e $FSTAB) {
    print "Removing auto-generated $FSTAB swap line ...\n";
    system("sed -i -e '/^# the following swap devices added by /,+1d' $FSTAB");
}

#
# Generate a dumpdates file. Use fstab to find out what filesystems
# need entries, and then put in entries for each filesystem using
# the current date.
#
if (-e $FSTAB) {
    print "Removing old $DUMPDATES file ...\n";
    if (-e $DUMPDATES) {
	unlink($DUMPDATES) or
	    die("Could not unlink $DUMPDATES");
    }

    print "Creating stub /etc/dumpdates file ...\n";
    open(TAB, "$FSTAB") or
	die("Could not open $FSTAB: $!");
    open(DMP, ">$DUMPDATES") or
	die("Could not open $DUMPDATES: $!");

    while (<TAB>) {
	if ($_ =~ /^([\w\/]+).*(\d).*\d$/) {
	    if ($2 != 0) {
		my $t = localtime(time);
		print DMP "$1                          0 $t\n";
	    }
	}
    }
    close(DMP);
    close(TAB);
}

print "Cleaning logfiles ...\n";
opendir(LOG,$LOGDIR) or
    die("Could not open directory $LOGDIR: $!");

while ($dirent = readdir(LOG)) {
    my $file = $LOGDIR . "/" . $dirent;
    if (-f $file) {
        if ($file =~ /\.\d$/ || $file =~ /\.\d\.gz$/ || $file =~ /-\d{8}$/) {
            # Remove archived logfiles
            unlink($file) or
                die("Could not unlink $file: $!");
        } elsif (-s $file) {
            # Truncate other logfiles - many do not get re-created
            truncate($file,0) or
                die("Could not truncate $file: $!");
        }
    }
}
closedir(LOG);

#
# Clear out systemd journals.
#
if (-d "/var/log/journal") {
    print "Cleaning systemd journal files...\n";
    system("journalctl --vacuum-time=1s");
    # Vacuum does not kill off the active journals, so truncate them.
    # They might reappear.
    system("truncate -s 0 /var/log/journal/*/*.journal");
}

my $XLOGDIR = "$LOGDIR/xen";
if (-d $XLOGDIR) {
    print "Cleaning Xen logfiles ...\n";
    opendir(LOG,$XLOGDIR) or
	die("Could not open directory $XLOGDIR: $!");

    while ($dirent = readdir(LOG)) {
	my $file = $XLOGDIR . "/" . $dirent;
	if (-f $file) {
	    if ($file =~ /\.\d$/ || $file =~ /\.\d\.gz$/) {
		# Remove archived logfiles
		unlink($file) or
		    die("Could not unlink $file: $!");
	    } elsif (-s $file) {
		# Truncate other logfiles - many do not get re-created
		truncate($file,0) or
		    die("Could not truncate $file: $!");
	    }
	}
    }
    closedir(LOG);
}

# Other log subdirs that we totally clean out
foreach my $subdir ("fsck", "installer", "news", "upstart") {
    if (-d "$LOGDIR/$subdir") {
	print "Cleaning $subdir logfiles ...\n";
	system("rm -rf $LOGDIR/$subdir/*");
    }
}

print "Removing accounting files ...\n";
if ( -f "$LOGDIR/pacct" ) {
    unlink("$LOGDIR/pacct");
}

if (-d $VARACCTDIR) {
    opendir(VARACCT,$VARACCTDIR) or
	die("Could not open directory $VARACCTDIR: $!");

    while ($dirent = readdir(VARACCT)) {
	my $file = $VARACCTDIR . "/" . $dirent;
	if (-f $file) {
	    if ($file =~ /acct/) {
		unlink($file) or
		    die ("Could not unlink $file: $!");
	    }
	}
    }
    closedir(VARACCT);
}

if (-d $VARACCTSDIR) {
    print "Removing accounting summary files ...\n";
    system("rm -f $VARACCTSDIR/*");
}

print "Removing root's history ...\n";
if (-f "/$HISTORY") {
    unlink("/$HISTORY") or
        die("Could not unlink /$HISTORY: $!");
}
if (-f "/root/$HISTORY") {
    unlink("/root/$HISTORY") or
        die("Could not unlink /root/$HISTORY: $!");
}

#
# Remove /root/.ssh and .ssl and then regenerate empty directories.
# We don't want any Utah specific keys tainting the images.
#
print "Cleaning root's .ssh/ssl directories ...\n";
if (system("rm -rf $ROOTSSHDIR") ||
    system("mkdir -p -m 700 $ROOTSSHDIR") ||
    system("chown root:root $ROOTSSHDIR")) {
    die("Could not clean root .ssh directory");
}
if (system("rm -rf $ROOTSSLDIR") ||
    system("mkdir -p -m 700 $ROOTSSLDIR") ||
    system("chown root:root $ROOTSSLDIR")) {
    die("Could not clean root .ssl directory");
}

#
# XXX unlike the root .ssh directory above, the proper host keys will
# not be restored when the node reboots following taking the image.
# Thus the node will wind up with new unique host keys after reboot and
# will likely sow confusion for anyone ssh'ing in after that. Arguably,
# the host keys should be configured on every boot like the root
# authorized_keys file is (in rc.localize).
#
#print "Removing SSH host keys ...\n";
#system("rm -rf /etc/ssh/ssh_host_*key /etc/ssh/ssh_host_*key.pub");

print "Cleaning mail spool files ...\n";
system("rm -rf $MAILDIR/*");

#
# Remove the iftab file (maps ifaces to mac addrs and renames ifaces according
# to the map -- screws us up).
#
if ( -f $IFTAB ) {
    unlink($IFTAB);
    print "Removed $IFTAB.\n";
}

#
# Just removing the drift file results in no drift being tracked,
# so set it to zero instead.  Not ideal, but at least the clock will
# stabilize eventually this way.
#
print "Resetting NTP drift ...\n";
my $driftfile = $NTPDRIFT1;
if (! -f $driftfile) {
    $driftfile = $NTPDRIFT2;
    if (! -f $driftfile) {
	$driftfile = $NTPDRIFT3;
    }
}
if (-f $driftfile) {
    open(DRIFT, ">$driftfile") or
	die("Could not open $driftfile: $!");
    print DRIFT "0\n";
    close(DRIFT);
}
print "Resetting chrony drift ...\n";
for my $fp ("/var/lib/chrony/drift","/var/lib/chrony/chrony.drift") {
    if (-f $fp) {
	unlink($fp);
    }
}

print "Clearing out $RUNDIR ...\n";
system("rm -rf $RUNDIR/*.pid $RUNDIR/sudo/* $RUNDIR/pump.sock");

print "Clearing out /tmp ...\n";
system("rm -rf /tmp/* /tmp/.??*");

my $GITREPO = "/local/repository";
if (-e $GITREPO) {
    print "Removing $GITREPO ...\n";
    system("rm -rf $GITREPO");
}

print "Cleaning out /local/logs ...\n";
system("rm -rf /local/logs/* /local/logs/.??*");

print "Removing SFS files ...\n";
if (-f $SFSUSERS) {
    system("rm -f $SFSUSERS ${SFSUSERS}.pub");
    system("rm -f $SFSUSERS.old ${SFSUSERS}.pub.old");
}
if (-f $SFSHOSTKEY) {
    system("rm -f $SFSHOSTKEY");
}

print "Clearing out directories in /var ...\n";
foreach my $dir (@SYSVARDIRS) {
    if (-d "/var/$dir") {
	system("rm -rf /var/$dir/*");
    }
}

# and clean out /var/tmp...hope there isn't anything we should be leaving!
print "Cleaning up /var/tmp ....\n";
system("rm -rf /var/tmp/*");

# take these out since they are large and RHL regens them anyway
system("rm -f /var/lib/rpm/__db*");

print "Clearing out directories in $VARDIR ...\n";
foreach my $dir (@VARDIRS) {
    # If it is a symlink, make it a real directory
    # XXX really just for vnode dirs ("vms")
    if (-l "$VARDIR/$dir") {
	unlink("$VARDIR/$dir");
	mkdir("$VARDIR/$dir", 0755);
    }
    elsif (-d "$VARDIR/$dir") {
	system("rm -rf $VARDIR/$dir/*");
    }
}

print "Clearing out old Emulab scripts and binaries in $BINDIR ...\n";
foreach my $file (@DEADFILES) {
    if (-f "$BINDIR/$file") {
	unlink("$BINDIR/$file") or
	    warn("*** could not remove $BINDIR/$file\n");
    }
}
system("rm -f /etc/pump.conf");

print "Removing backup files in /etc\n";

opendir(ETC,"/etc") or
    die ("Couldn't open /etc: $!");

while ($dirent = readdir(ETC)) {
    my $file = "/etc/" . $dirent;
    if (-f $file) {
	if (($file =~ /~$/) || ($file =~ /-$/)) {
	    unlink($file) or
		die ("Couldn't unlink $file: $!");
	}
    }
}
closedir(ETC);

print "Removing assorted unix-domain socket files\n";
foreach my $file (@SOCKETS) {
    if (-e "$file" && ! -f "$file") {
	unlink("$file") or
	    warn("*** could not remove $file\n");
    }
}

print "Removing extra Ports from $SSHDCONFIG\n";
if (-e $SSHDCONFIG) {
    # Kill off old sshd ports.
    system("sed -i.bak -e '/^# EmulabJail/,\$d' $SSHDCONFIG");
    warn("*** Could not remove Port lines\n")
	if ($?);
}

print "Removing pubsubd clustering config file\n";
if (-e $PUBSUBCONF) {
    unlink($PUBSUBCONF) or
	warn("*** could not remove $PUBSUBCONF\n");
}
if (-e $PUBSUBEXPR) {
    unlink($PUBSUBEXPR) or
	warn("*** could not remove $PUBSUBEXPR\n");
}

#
# Really finally, look for user prepare posthooks and run them.
#
@PHOOKSDIRS = ("$DYNRUNDIR/prepare.post.d","$BINDIR/prepare.post.d");
for my $pdir (@PHOOKSDIRS) {
    my @sfiles = ();
    my $rc = sortedlistallfilesindir($pdir,\@sfiles,1);
    if (defined($rc) && $rc == 0) {
	for my $file (@sfiles) {
	    print "Running prepare hook $file ...\n";
	    system("$file");
	}
    }
}

#
# Get set up to force a regeneration of the system machine-id.  This
# should be done last; it can make the system unstable; see man
# dbus-uuidgen.  We only do this if we've got a dbus-uuidgen.  NB:
# systems are theoretically supposed to regen /etc/machine-id if it's
# not present, but this doesn't happen on Ubuntu 18, for instance (see
# also https://bugzilla.redhat.com/show_bug.cgi?id=1379800).
# Fortunately, if the file is present and empty, it does get
# regenerated.  So we empty it, and ensure that /var/lib/dbus/machine-id
# is a symlink to /etc/machine-id.
#
if (-e "/etc/machine-id" && -e "/var/lib/dbus/machine-id") {
    open(FD,">/etc/machine-id");
    close(FD);
    system("ln -sf /etc/machine-id /var/lib/dbus/machine-id");
    print "Forcing regeneration of machine-id on next boot.\n";
}

# Leave this print statement here; create_image depends on it.
print "prepare ran successfully!\n";
exit 0;

