356 lines
9.0 KiB
Perl
356 lines
9.0 KiB
Perl
|
#!/usr/bin/env perl
|
||
|
|
||
|
use strict;
|
||
|
use warnings;
|
||
|
use YAML;
|
||
|
use File::Basename qw(fileparse);
|
||
|
use List::MoreUtils qw(uniq);
|
||
|
use Getopt::Std;
|
||
|
use Data::Dumper;
|
||
|
use File::HomeDir;
|
||
|
use File::Path qw(make_path);
|
||
|
use Sys::Syslog qw(:standard :macros);
|
||
|
use 5.010;
|
||
|
|
||
|
my $isOpenBSD = ( $^O eq 'openbsd' ) ? 1 : 0;
|
||
|
|
||
|
my $verbose = 0;
|
||
|
|
||
|
# display usage and quit
|
||
|
sub usage
|
||
|
{
|
||
|
say "usage: $0 [-v] -d directory -u user (start|stop|restart|status)";
|
||
|
say " $0 -t conf";
|
||
|
exit 1;
|
||
|
}
|
||
|
|
||
|
# display a warning unconditionally
|
||
|
sub saywarning
|
||
|
{
|
||
|
my $msg = shift;
|
||
|
say STDERR "WARNING: $msg";
|
||
|
syslog( LOG_WARNING, "$msg" );
|
||
|
}
|
||
|
|
||
|
# display a message only if verbose flag is used
|
||
|
sub saydebug
|
||
|
{
|
||
|
my $msg = shift;
|
||
|
say STDERR "DEBUG: $msg" if $verbose;
|
||
|
syslog( LOG_INFO, "$msg" );
|
||
|
}
|
||
|
|
||
|
# display a message before exiting with status 2
|
||
|
sub trap_error
|
||
|
{
|
||
|
my $msg = shift;
|
||
|
say STDERR "FATAL: $msg";
|
||
|
syslog( LOG_ERR, "$msg" );
|
||
|
exit 2;
|
||
|
}
|
||
|
|
||
|
# return a perl structure by reading a YAML file
|
||
|
sub read_yml_file_to_struct
|
||
|
{
|
||
|
my $file = shift;
|
||
|
open my $fh, '<', $file or die;
|
||
|
$/ = undef;
|
||
|
my $data = <$fh>;
|
||
|
close $fh;
|
||
|
return Load($data);
|
||
|
}
|
||
|
|
||
|
# some files or directories may be included in a listed directory
|
||
|
# remove those from the list and print a warning
|
||
|
sub remove_transclusion
|
||
|
{
|
||
|
my $data = shift;
|
||
|
my @files = uniq @{ $data->{"files"} };
|
||
|
my @directories = @{ $data->{"directories"} };
|
||
|
|
||
|
foreach (@directories) {
|
||
|
if ( $_ =~ m|/$| ) {
|
||
|
saywarning("removing trailing slash in $_");
|
||
|
$_ =~ s|/$||;
|
||
|
}
|
||
|
}
|
||
|
@directories = uniq @directories;
|
||
|
|
||
|
for ( my $i = 0 ; $i <= $#directories ; $i++ ) {
|
||
|
my $dir = $directories[$i];
|
||
|
next if ( $dir eq "" );
|
||
|
|
||
|
# check if a file is contained by a directory
|
||
|
foreach my $file (@files) {
|
||
|
my ( $filename, $dirs, $suffix ) = fileparse($file);
|
||
|
if ( $dirs =~ m/^$dir/ ) {
|
||
|
saywarning("WARNING: $dir contains $file");
|
||
|
$file = "";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# check if a directory is contained by another directory
|
||
|
# if so, mark them for deletion later
|
||
|
# we can't remove directly from the array because we
|
||
|
# are iterating in it in the main loop
|
||
|
for ( my $j = 0 ; $j <= $#directories ; $j++ ) {
|
||
|
if ( $j != $i ) {
|
||
|
my $dir2 = $directories[$j];
|
||
|
next if ( $dir2 eq "" );
|
||
|
if ( $dir =~ m/\/$dir2/ ) {
|
||
|
saywarning("$dir2 contains $dir");
|
||
|
$directories[$i] = "";
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# remove empty entries
|
||
|
@directories = sort grep { /^./ } @directories;
|
||
|
@files = sort grep { /^./ } @files;
|
||
|
$data->{"files"} = \@files;
|
||
|
$data->{"directories"} = \@directories;
|
||
|
|
||
|
return $data;
|
||
|
}
|
||
|
|
||
|
# create all the links in the ramdisk
|
||
|
sub populate_ramdisk
|
||
|
{
|
||
|
my ( $data, $persist_home, $impermanence_home, $user ) = @_;
|
||
|
saydebug("create the symlinks for files set");
|
||
|
create_links( $data->{"files"}, $persist_home, $impermanence_home, $user );
|
||
|
|
||
|
saydebug("create the symlinks for directories set");
|
||
|
create_links( $data->{"directories"},
|
||
|
$persist_home, $impermanence_home, $user );
|
||
|
|
||
|
saydebug("create the skeleton directories set");
|
||
|
create_skeleton( $data->{"skeleton"}, $impermanence_home, $user );
|
||
|
|
||
|
saydebug("call chezmoi to create config files");
|
||
|
call_chezmoi($user);
|
||
|
}
|
||
|
|
||
|
# check if the mountpoint is already mounted with mfs
|
||
|
sub is_mounted
|
||
|
{
|
||
|
my $impermanence_home = shift;
|
||
|
my $mounted = 0;
|
||
|
my @output;
|
||
|
|
||
|
# is this already mounted?
|
||
|
@output = split( "\n", `/bin/df $impermanence_home` );
|
||
|
@output = split( " ", $output[$#output] );
|
||
|
if ( $output[5] eq $impermanence_home && $output[0] =~ m/^(mfs|tmpfs)/ ) {
|
||
|
$mounted = 1;
|
||
|
}
|
||
|
return $mounted;
|
||
|
}
|
||
|
|
||
|
# OpenBSD: mount the destination with a ramdisk only if not currently mounted
|
||
|
sub mount_mfs
|
||
|
{
|
||
|
my ( $user, $impermanence_home, $data ) = @_;
|
||
|
my $ret;
|
||
|
my $filesystem;
|
||
|
|
||
|
saydebug("finding a ffs mountpoint to use for mfs");
|
||
|
if ( is_mounted($impermanence_home) ) {
|
||
|
trap_error("ERROR: $impermanence_home is already mounted with MFS");
|
||
|
}
|
||
|
my @fs = split( "\n", `/sbin/swapctl` );
|
||
|
@fs = split( " ", $fs[$#fs] );
|
||
|
$filesystem = $fs[0];
|
||
|
|
||
|
if ( $filesystem !~ m|^/dev/| ) {
|
||
|
trap_error("found swap device $filesystem doesn't start with /dev");
|
||
|
}
|
||
|
|
||
|
saydebug("mount the destination using mount_mfs from $filesystem");
|
||
|
$ret = system( "/sbin/mount_mfs", "-s", $data->{size}, $filesystem,
|
||
|
$impermanence_home );
|
||
|
if ( $ret != 0 ) {
|
||
|
trap_error("ERROR: mounting the mfs filesystem errored with error $ret");
|
||
|
} else {
|
||
|
saydebug("mount_mfs done on $impermanence_home");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# Linux: mount the destination with a ramdisk only if not currently mounted
|
||
|
sub mount_tmpfs
|
||
|
{
|
||
|
my ( $user, $impermanence_home, $data ) = @_;
|
||
|
my $ret;
|
||
|
|
||
|
saydebug("mount the destination tmpfs");
|
||
|
$ret =
|
||
|
system( "/bin/mount", "-t", "tmpfs", "tmpfs", "-o", "size=$data->{size}",
|
||
|
$impermanence_home );
|
||
|
if ( $ret != 0 ) {
|
||
|
trap_error("ERROR: mounting the tmpfs filesystem errored with error $ret");
|
||
|
} else {
|
||
|
saydebug("tmpfs done on $impermanence_home");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
# create the symbolic links listed in the yml file into the ramdisk destination
|
||
|
sub create_links
|
||
|
{
|
||
|
my $list = shift;
|
||
|
my ( $persist_home, $impermanence_home, $user ) = @_;
|
||
|
foreach ( @{$list} ) {
|
||
|
my $old_file = $persist_home . "/" . $user . "/" . $_;
|
||
|
my $new_file = $impermanence_home . "/" . $_;
|
||
|
|
||
|
my ( $filename, $dirs, $suffix ) = fileparse($new_file);
|
||
|
|
||
|
# recursively create missing directories to hold files
|
||
|
if ( !-e $dirs ) {
|
||
|
make_path( $dirs, { chmod => 0750, owner => $user } );
|
||
|
}
|
||
|
|
||
|
if ( !-e $old_file ) {
|
||
|
saywarning("$old_file doesn't exist");
|
||
|
}
|
||
|
|
||
|
if ( symlink( $old_file, $new_file ) == 0 ) {
|
||
|
trap_error("symlink $old_file to $new_file");
|
||
|
} else {
|
||
|
saydebug("ln -s $old_file $new_file");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
sub create_skeleton
|
||
|
{
|
||
|
my $list = shift;
|
||
|
my ( $impermanence_home, $user ) = @_;
|
||
|
foreach ( @{$list} ) {
|
||
|
my $dir = $impermanence_home . "/" . $_;
|
||
|
|
||
|
saydebug("Create directory $dir");
|
||
|
make_path( $dir, { chmod => 0750, owner => $user } );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
sub call_chezmoi
|
||
|
{
|
||
|
my $user = shift;
|
||
|
my $ret = system("su -l $user -c \"chezmoi --force apply\"");
|
||
|
if ( $ret != 0 ) {
|
||
|
trap_error("ERROR: Calling chezmoi apply errored with error $ret");
|
||
|
} else {
|
||
|
saydebug("chezmoi finished successfully");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
sub main
|
||
|
{
|
||
|
|
||
|
my %opts;
|
||
|
my ( $impermanence_home, $persist_home, $configuration_file, $data, $ret );
|
||
|
my ( $start, $stop ) = ( 0, 0 );
|
||
|
|
||
|
# define command line parameters
|
||
|
getopts( "vt:d:u:", \%opts );
|
||
|
|
||
|
# verbose mode for debug output
|
||
|
if ( defined $opts{v} ) {
|
||
|
$verbose = 1;
|
||
|
}
|
||
|
|
||
|
# check if using test mode to validate a configuration file
|
||
|
if ( defined $opts{t} ) {
|
||
|
say("test mode enabled");
|
||
|
$configuration_file = $opts{t};
|
||
|
|
||
|
# non-test mode, mount the ramdisk and populates it
|
||
|
} else {
|
||
|
|
||
|
# -d and -u flags are mandatory
|
||
|
if ( !defined $opts{d} || !defined $opts{u} ) {
|
||
|
usage();
|
||
|
}
|
||
|
|
||
|
# test if the script is running as root
|
||
|
if ( $< != 0 ) {
|
||
|
trap_error("$0 must be run as root.");
|
||
|
}
|
||
|
|
||
|
$impermanence_home = File::HomeDir->users_home( $opts{u} );
|
||
|
if ( $impermanence_home !~ m|^/| ) {
|
||
|
trap_error(
|
||
|
"The user \$HOME doesn't start with / , its value is $impermanence_home"
|
||
|
);
|
||
|
}
|
||
|
if ( !-d $impermanence_home ) {
|
||
|
trap_error("The user \$HOME $impermanence_home doesn't exist");
|
||
|
}
|
||
|
|
||
|
$persist_home = $opts{d};
|
||
|
if ( $persist_home !~ m|^/| ) {
|
||
|
trap_error(
|
||
|
"The persistent directory $persist_home must be an absolute path");
|
||
|
}
|
||
|
if ( !-d $persist_home ) {
|
||
|
trap_error("The persistent directory $persist_home doesn't exist");
|
||
|
}
|
||
|
|
||
|
$configuration_file = $persist_home . "/" . $opts{u} . "/impermanence.yml";
|
||
|
}
|
||
|
|
||
|
# exit if the configuration file is not available
|
||
|
if ( !-f $configuration_file ) {
|
||
|
trap_error( "The file " . $configuration_file . " can't be found" );
|
||
|
}
|
||
|
|
||
|
# read file and sanitize content
|
||
|
$data = read_yml_file_to_struct($configuration_file);
|
||
|
if ( !defined $data->{size} || $data->{size} eq 0 ) {
|
||
|
trap_error("size not defined in configuration file");
|
||
|
}
|
||
|
$data = remove_transclusion($data);
|
||
|
|
||
|
# display result and stop if in test mode
|
||
|
if ( defined $opts{t} ) {
|
||
|
print Dumper $data;
|
||
|
exit 0;
|
||
|
}
|
||
|
|
||
|
if ( $ARGV[0] eq "start" ) {
|
||
|
$start = 1;
|
||
|
} elsif ( $ARGV[0] eq "restart" ) {
|
||
|
$stop = 1;
|
||
|
$start = 1;
|
||
|
} elsif ( $ARGV[0] eq "stop" ) {
|
||
|
$stop = 1;
|
||
|
} elsif ( $ARGV[0] eq "status" ) {
|
||
|
exit !is_mounted($impermanence_home);
|
||
|
} else {
|
||
|
usage();
|
||
|
}
|
||
|
|
||
|
if ( $stop && is_mounted($impermanence_home) ) {
|
||
|
my $status = system( "umount", $impermanence_home );
|
||
|
if ( $status != 0 ) {
|
||
|
trap_error("umount did exit with status $status");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ($start) {
|
||
|
if ($isOpenBSD) {
|
||
|
mount_mfs( $opts{u}, $impermanence_home, $data );
|
||
|
populate_ramdisk( $data, $persist_home, $impermanence_home, $opts{u} );
|
||
|
} else {
|
||
|
mount_tmpfs( $opts{u}, $impermanence_home, $data );
|
||
|
populate_ramdisk( $data, $persist_home, $impermanence_home, $opts{u} );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
openlog( "impermanence", 'ndelay', LOG_DAEMON );
|
||
|
main();
|
||
|
closelog();
|