Initial commit

This commit is contained in:
Jan Eitzinger 2024-04-28 08:31:12 +02:00
parent c784018463
commit ad29df951a
9 changed files with 712 additions and 1 deletions

4
.perltidyrc Normal file
View File

@ -0,0 +1,4 @@
-i=2
-ce
-sbl
-wn

11
Makefile Normal file
View File

@ -0,0 +1,11 @@
PREFIX=/usr/local
test:
./impermanence.pl -v -t tests/impermanence.yml
-./impermanence.pl -t tests/errored.yml
-./impermanence.pl -t tests/size_undefined.yml
install:
install -o root -g wheel -m 555 impermanence.pl ${PREFIX}/bin/impermanence
install -o root -g wheel -m 555 openbsd/impermanence.rc /etc/rc.d/impermanence
install -o root -g wheel -m 444 impermanence.1 ${PREFIX}/man/man1/impermanence.1

131
README.md
View File

@ -1,3 +1,132 @@
# home-impermanence # home-impermanence
A fork of https://tildegit.org/solene/home-impermanence from Solène Rapenne OpenBSD and Linux compatible implementation of the [impermanence project from the NixOS community](https://nixos.wiki/wiki/Impermanence)
Such a tool permits to have your $HOME mounted with a memory filesystem and populate it from an explicit list of files and directories hooked from a persistent storage directory (like a place in your /home partition), the point is to have a clean and reproducible environment every time you log in with only the content you selected. No more extra files when you start a program only once.
# Installation
Run `make install` as root, this will copy the program file in `/usr/local/bin/impermanence` and the service file in `/etc/rc.d/impermanence`.
On OpenBSD, You need some packages as dependencies:
- p5-File-HomeDir
- p5-List-MoreUtils
- p5-YAML
On Alpine Linux, you need the following packages: `apk add perl-file-homedir perl-list-moreutils perl-yaml`.
# Configuration
The configuration is done in two parts, system wide to configure the **impermanence** service that will mount the memory filesystem and populate it.
## System wide
### OpenBSD
Using rcctl: `rcctl set impermanence flags -d /home/persist/ -u my-user` and `rcctl enable impermanence`.
### Alpine Linux
Lazy way is to enable the service local with `rc-update add local`, and create two files `/etc/local.d/99-impermanence.start` and `/etc/local.d/99-impermanence.stop` with the following content:
```
#!/bin/sh
/usr/local/bin/impermanence -d /home/persist -u my-user start
```
and
```
#!/bin/sh
/usr/local/bin/impermanence -d /home/persist -u my-user stop
```
## User configuration
The user configuration will be done in `/home/persist/my-user/impermanence.yml` if you chose `-d /home/persist` for the service and `-u my-user`.
The configuration file describes the size of the memory filesystem, the list of files and the list of directories that should be added to the filesystem as symbolic links from the persistent directory.
There are currently three keys:
- **size**: which is a parameter to mount_mfs -s to give the ramdisk size
- **files**: which is a list of files relative to $HOME
- **directores**: which is a list of directories relative to $HOME
Minimalistic example of `/home/persist/my-user/impermanence.yml`:
```
size: 200m
files:
- .bashrc
- .gitconfig
- .profile
- .tmux.conf
- .xsession
directories:
- .config
- .local/share
- .mozilla
- .ssh
- Data
- Documents
- Downloads
- dev
```
# home-impermanence rc service
## restart
The restart parameter to the service will unmount the device and recreate it, allowing a fresh restart.
It is a bad idea to use while the user is connected.
## start
Creates and populates the home filesystem.
## stop
Umount the home filesystem.
It is a bad idea to use while the user is connected.
## status
Tells if the mount is currently done.
# Tips
## I configured something in a GUI program, how do I know what changed on disk?
If you want to add a file to the persistent area after a change, you may want to know exactly what changed on disk to add the file or directory to your configuration file.
Using `find` it's easy to scan all the files from the ramdisk (excluding the symbolic links) and order them by date of change.
This can be done with `find -x ~/ -type f -exec ls -altr {} +`, the last files are the most recently modified.
## Beware file loss
When using this way of life, you need to remember all changes that don't belong in the persistent areas will be lost. For example, this will happen for all new files or directories at the root of your $HOME.
Impermanence requires the user to be aware of what files must stay over time, this is the point of impermanence after all.
## I want to make a new file/directory persistent
If you are using your system and want to keep a newly created file or directory, move it to your persistent area at the correct place and create a symbolic link, this will allow a drop-in replacement without rebooting.
Then, update your configuration file to add the new entry.
## How does a good configuration file look
There are no good or bad configuration file content (except if it's invalid obviously). The whole point of impermanence is to hand-pick every directories and files you want to run your session, by admitting all others files will be thrown away at reboot.
While you can list `.config` and `.local` which is a very large include, you could rather list only a subset of those, which will make a long list and require a few guess&fix sessions to get the things right.
The less directories are at the top level, the more you will pinpoint the exact configuration you want to keep over time.
## Restarting impermanence
If you are actively tweaking your configuration file, you may have issues when impermanence is unmounting the ramdisk device even with your graphical session stopped, a process may still be running and using the partition. You would have to find the running PID by looking at processes and their owner and kill it.
As a side effect, you shouldn't be able to stop impermanence while you are using your session because the system will prevent the ramdisk to be umounted.

159
impermanence.1 Normal file
View File

@ -0,0 +1,159 @@
.Dd $Mdocdate: March 14 2022 $
.Dt IMPERMANENCE 1
.Os
.Sh NAME
.Nm impermanence
.Nd manage a ramdisk with a collection of persistent files
.Sh SYNOPSIS
.Nm
.Fl t Ar conf
.Nm
.Op Fl v
.Fl d Ar directory
.Fl u Ar user
.Op Cm start | Cm stop | Cm status | Cm restart
.Sh DESCRIPTION
.Pp
.Nm
is a program to manage an user directory mounted as a volatile
ramdisk and filling it with symlinks to persistent files and
directories taken from an user managed configuration file.
.Pp
The advantages of such a setup is to have a reproducible home
environment that won't keep undesirable changes over time which
happen regularly when running new software or different desktop
environment.
.Pp
However, this requires the user to be mindful of the setup to prevent
data loss.
.Sh OPTIONS
.Bl -tag -width -u user -c /home/persistent
.It Fl t Ar conf
Enable test mode which will parse the configuration file
.Ar conf ,
validates it and output its content in a human readable format
before exiting gracefully.
.It Op Fl v
Enable verbose mode adding debug information to stderr and syslog.
.It Fl d Ar directory
Tell
.Nm
where to look for the user persistent directory.
Note that it will look for a directory with the user name in the
directory given in this parameter.
.It Fl u Ar username
Tell
.Nm
which user will have its $HOME directory mounted as ramdisk.
.It Cm start
Create the ramdisk using
.Xr mount_mfs 8
and fill it with the listed content using
.Xr symlink 2
from the persistent directory for each file and directory listed
in the configuration file.
.It Cm stop
Umount the ramdisk using
.Xr umount 8 ,
all data that wasn't listed in the configuration file at start time
will be lost.
.It Cm restart
Do a stop and a start.
.It Cm status
Returns 0 if the user directory is already mounted as mfs, returns
1 otherwise.
.El
.Sh FILES
.Nm
will look for a file
.Sy impermanence.yml
in the directory
.Ar user
found in the directory
.Ar directory.
.Pp
The
.Sy impermanence.yml
file is a YAML formatted configuration file using three keys.
.Bd -literal -offset indent
size: 200m
files:
- .bashrc
- .xsession
directories:
- .config
- .ssh
- Documents
.Ed
.Pp
With
.Cm size
being the size parameter given to
.Xr mount_mfs 8
.Cm files
being the list of files relative to the user persistent directory
that must be symlinked to the ramdisk.
.Cm directories
being the list of directories relative to the user persistent
directory that must be symlinked to the ramdisk.
.Sh EXIT STATUS
.Pp
In case of a fatal error,
.Nm
will exit on a status code 2.
.Pp
In case of a misusage,
.Nm
will exit on a status code 1.
.Pp
In normal operations,
.Nm
will exit on a status code 0.
.Sh EXAMPLES
Let's say you want user
.Em alice
to have its
.Em /home/alice
$HOME directory mounted with a ram disk.
.Pp
You need to create a place where to store
.Cm alice
files, you can move
.Sy /home/alice
to
.Sy /home/persistent/alice
and recreate
.Sy /home/alice .
.Pp
Create a file in
.Sy /home/persistent/alice/impermanence.yml
following the file format to list every file and
directories that must be linked from
.Sy /home/persistent/alice/
to
.Sy /home/alice/
when
.Nm
is started using the following command line:
.Bd -literal -offset indent
impermanence -u alice -d /home/persistent start
.Ed
.Sh DIAGNOSTICS
.Nm
has a verbose mode to get more information.
Normal output and verbose output if enabled are both sent to
.Xr syslog 3 .
.Sh SEE ALSO
.Xr mount_mfs 8 ,
.Xr symlink 2,
.Xr syslog 3,
.Xr umount 8
.Sh HISTORY
This software tries to be an OpenBSD implementation of the NixOS
community impermanence module.
.Sh AUTHORS
.An See the LICENSE file for the authors .
.Sh LICENSE
See the LICENSE file for the terms of redistribution.
.Sh BUGS
Some programs may misbehave under these conditions.

355
impermanence.pl Executable file
View File

@ -0,0 +1,355 @@
#!/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();

22
openbsd/impermanence.rc Executable file
View File

@ -0,0 +1,22 @@
#!/bin/ksh
#
daemon="/usr/local/bin/impermanence"
. /etc/rc.d/rc.subr
rc_reload=NO
rc_start() {
$daemon ${daemon_flags} start
}
rc_stop() {
$daemon ${daemon_flags} stop
}
rc_check() {
$daemon ${daemon_flags} status
}
rc_cmd $1

2
tests/errored.yml Normal file
View File

@ -0,0 +1,2 @@
key:
value

25
tests/impermanence.yml Normal file
View File

@ -0,0 +1,25 @@
size: 500m
files:
- .Xdefaults
- .Xresources
- .bashrc
- .gitconfig
- .kshrc
- .profile
- .xsession
- .config/fish/config.fish
- Datastore/Music/Band1/file.ogg
directories:
- .config/fish
- .config/kak
- .mozilla
- foo/bar/those
- Documents
- Downloads
- Datastore/Music
- Datastore
- Datastore/
- Datastore/Music/Band1
- .config
- foo/bar
- foo/bar/hello

4
tests/size_undefined.yml Normal file
View File

@ -0,0 +1,4 @@
files:
- foo
directories:
- bar