bind-dirs allows an AppVM to persist changes outside user directories.
But what if a TVM wants to persist changes inside user directories?

I'm seeking feedback on a system I set up to solve this problem: Is
there a better way to achieve this goal? Or on the contrary, is this
something that might be useful to merge in to Qubes?


Motivation
==========

These days, a lot of software is installed in the home directory and
updated outside of the package manager. We want a way to share those
packages across all AppVMs of a TVM.

E.g. say you want to install Doom Emacs, configure it, then make it
available to all AppVMs of a template. This would require the TVM to:

 1. Make /home/user/.config/{doom,emacs} available in AppVMs
 2. (ideally) allow AppVMs to store persistent changes to these to allow
    customization on a per-VM basis


Solution
========

The solution I came up with was to have a bash script read
a file /etc/underlay/underlay.list and, for each item:

 1. mount /etc/underlay/item to /home/user/item, read-only
 2. mount /home/user/.overlay/item over that mount, read-write

So the files come from the TVM, but any changes are written (CoW) to the
overlay in /home/user/.overlay where they are persisted.

The procedure for updating would be to perform the update in a dispVM
based on the TVM, then copy the update to the TVM (/etc/underlay/...).


Implementation
==============

In the TVM, create:

/usr/bin/mount-underlays.sh

    #!/bin/bash
    
    # Only run in AppVMs
    if [ ! -f /var/run/qubes/this-is-appvm ]; then
        echo "Error: This should only be run in AppVMs" >&2
        exit 8
    fi
    
    DEST="/home/user"
    UNDERLAY_BASE="/etc/underlay"
    OVERLAY_BASE="$DEST/.overlay"
    WORKDIR_BASE="$DEST/.cache/overlay-workdir"
    SYSTEM_LIST="$UNDERLAY_BASE/underlay.list"
    USER_LIST="$OVERLAY_BASE/underlay.list"
    
    # Function to mount a directory with overlay
    mount_overlay() {
        local rel_path="$1"
        local lower_dir="$UNDERLAY_BASE/$rel_path"
        local mount_point="$DEST/$rel_path"
        local upper_dir="$OVERLAY_BASE/$rel_path"
        local work_dir="$WORKDIR_BASE/$rel_path"
        
        # Check if lower directory exists
        if [ ! -d "$lower_dir" ]; then
            echo "Warning: underlay directory $lower_dir does not exist, 
skipping" >&2
            return 1
        fi
        
        # Create necessary directories
        mkdir -p "$mount_point" "$upper_dir" "$work_dir"
        
        # Check if already mounted
        if mountpoint -q "$mount_point" 2>/dev/null; then
            echo "Already mounted: $mount_point" >&2
            return 0
        fi
        
        # Mount overlay filesystem
        if sudo mount -t overlay overlay \
            -o lowerdir="$lower_dir",upperdir="$upper_dir",workdir="$work_dir" \
            "$mount_point" 2>/dev/null; then
            echo "Mounted: $rel_path at $mount_point" >&2
        else
            echo "Failed to mount overlay for $rel_path" >&2
            return 1
        fi
    }
    
    # Function to process a list file
    process_list() {
        local list_file="$1"
        
        if [ ! -f "$list_file" ]; then
            # list file does not exist
            return 0
        fi
        
        echo "Processing: $list_file" >&2
        
        while IFS= read -r line; do
            # Skip comments and empty lines
            # / at the start of a line denotes a comment
            [[ "$line" == /* ]] && continue
            [[ -z "$line" ]] && continue
            
            # Mount the overlay
            mount_overlay "$line"
        done < "$list_file"
    }
    
    # Only proceed if the underlay base exists
    if [ ! -d "$UNDERLAY_BASE" ]; then
        echo "Error: Underlay base ($UNDERLAY_BASE) does not exist" >&2
        exit 3
    fi
    
    process_list "$SYSTEM_LIST"
    process_list "$USER_LIST"


/etc/systemd/system/mount-underlays.service

    [Unit]
    Description=Mount underlay directories
    After=local-fs.target
    ConditionPathExists=/var/run/qubes/this-is-appvm
    ConditionPathExists=/etc/underlay
    
    [Service]
    Type=oneshot
    RemainAfterExit=yes
    ExecStart=-/usr/bin/mount-underlays.sh
    StandardOutput=journal
    StandardError=journal
    TimeoutSec=12
    
    [Install]
    WantedBy=default.target


Example Usage
=============

Example with Doom Emacs:

In the TVM:

 1. In the TVM, create /etc/underlays/.config/{doom,emacs}

 2. Copy your doom and emacs directories there. In the AppVM:
        tar -cf emacs.tar.gz ~/.config/emacs
        qvm-copy emacs.tar.gz

 3. In the TVM, create /etc/underlays/underlays.list

    .config/doom
    .config/emacs

 4. Power off the TVM.

 5. Start a new disposable based on the TVM. In that, run:
        systemctl start mount-underlays
        emacs

-- 
Thanks for reading,

Zaz

-- 
You received this message because you are subscribed to the Google Groups 
"qubes-devel" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/qubes-devel/878qi9l7k6.fsf%40localhost.

Reply via email to