diff --git a/extra/README.md b/extra/README.md index d5f977b..7c97721 100644 --- a/extra/README.md +++ b/extra/README.md @@ -3,6 +3,6 @@ Additional tools and scripts that help with the hub maintenance - `certs-installer` - scripts for certificates installation in Root CA Truststore (Windows & Linux) - `docker_volume_backupper` - script that backs docker volumes (use regex for name) - `traefik-host-based-routing` - deployment template with local Traefik and self-signed certificates -- `volume-migration` - scripts to rename volumes and migrate user data between usernames +- `volume-renamer` - scripts to rename volumes and migrate user data between usernames diff --git a/extra/volume-migration/list-user-volumes.sh b/extra/volume-migration/list-user-volumes.sh deleted file mode 100755 index b7d500f..0000000 --- a/extra/volume-migration/list-user-volumes.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -# List all JupyterHub user volumes - -echo "JupyterHub user volumes:" -echo "" - -docker volume ls --format '{{.Name}}' | grep -E '^jupyterlab-' | sort | while read -r VOL; do - SIZE=$(docker run --rm -v "$VOL":/data alpine sh -c "du -sh /data 2>/dev/null | cut -f1" 2>/dev/null || echo "?") - printf " %-50s %s\n" "$VOL" "$SIZE" -done - -echo "" diff --git a/extra/volume-migration/migrate-user-volumes.sh b/extra/volume-migration/migrate-user-volumes.sh deleted file mode 100755 index 1c6cfcf..0000000 --- a/extra/volume-migration/migrate-user-volumes.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/bin/bash -# Migrate all JupyterHub user volumes from one username to another -# Handles: home, workspace, cache volumes - -set -e - -SCRIPT_DIR="$(dirname "$0")" - -if [ -z "$1" ] || [ -z "$2" ]; then - echo "Usage: $0 [--delete-source]" - echo "" - echo "Migrates JupyterHub user volumes:" - echo " - jupyterlab-{username}_home" - echo " - jupyterlab-{username}_workspace" - echo " - jupyterlab-{username}_cache" - echo "" - echo "Options:" - echo " --delete-source Delete source volumes after successful copy" - echo "" - echo "Example:" - echo " $0 john john.doe" - echo " $0 john john.doe --delete-source" - exit 1 -fi - -OLD_USER="$1" -NEW_USER="$2" -DELETE_FLAG="" - -if [ "$3" = "--delete-source" ]; then - DELETE_FLAG="--delete-source" -fi - -VOLUME_SUFFIXES="home workspace cache" - -echo "Migrating volumes for user: $OLD_USER -> $NEW_USER" -echo "" - -# Check which volumes exist -VOLUMES_TO_MIGRATE="" -for SUFFIX in $VOLUME_SUFFIXES; do - SOURCE="jupyterlab-${OLD_USER}_${SUFFIX}" - if docker volume inspect "$SOURCE" >/dev/null 2>&1; then - VOLUMES_TO_MIGRATE="$VOLUMES_TO_MIGRATE $SUFFIX" - echo " Found: $SOURCE" - else - echo " Skip: $SOURCE (not found)" - fi -done - -if [ -z "$VOLUMES_TO_MIGRATE" ]; then - echo "" - echo "No volumes found for user: $OLD_USER" - exit 0 -fi - -echo "" -read -p "Proceed with migration? [y/N] " CONFIRM -if [ "$CONFIRM" != "y" ] && [ "$CONFIRM" != "Y" ]; then - echo "Aborted" - exit 0 -fi - -echo "" - -# Migrate each volume -for SUFFIX in $VOLUMES_TO_MIGRATE; do - SOURCE="jupyterlab-${OLD_USER}_${SUFFIX}" - TARGET="jupyterlab-${NEW_USER}_${SUFFIX}" - - echo "--- Migrating $SUFFIX volume ---" - "$SCRIPT_DIR/rename-volume.sh" "$SOURCE" "$TARGET" $DELETE_FLAG - echo "" -done - -echo "User volume migration complete: $OLD_USER -> $NEW_USER" diff --git a/extra/volume-migration/rename-volume.sh b/extra/volume-migration/rename-volume.sh deleted file mode 100755 index dca1644..0000000 --- a/extra/volume-migration/rename-volume.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/bin/bash -# Rename a Docker volume by copying data to a new volume - -set -e - -if [ -z "$1" ] || [ -z "$2" ]; then - echo "Usage: $0 [--delete-source]" - echo "" - echo "Options:" - echo " --delete-source Delete source volume after successful copy" - echo "" - echo "Example:" - echo " $0 jupyterlab-olduser_home jupyterlab-newuser_home" - echo " $0 jupyterlab-olduser_home jupyterlab-newuser_home --delete-source" - exit 1 -fi - -SOURCE="$1" -TARGET="$2" -DELETE_SOURCE=false - -if [ "$3" = "--delete-source" ]; then - DELETE_SOURCE=true -fi - -# Check source volume exists -if ! docker volume inspect "$SOURCE" >/dev/null 2>&1; then - echo "ERROR: Source volume '$SOURCE' does not exist" - exit 1 -fi - -# Check target volume doesn't exist -if docker volume inspect "$TARGET" >/dev/null 2>&1; then - echo "ERROR: Target volume '$TARGET' already exists" - echo "Delete it first or choose a different name" - exit 1 -fi - -echo "Renaming volume:" -echo " Source: $SOURCE" -echo " Target: $TARGET" -echo "" - -# Create target volume -echo "Creating target volume..." -docker volume create "$TARGET" - -# Copy data using alpine container -echo "Copying data..." -CONTAINER_NAME="volume_copy_$(date +%s)_$$" - -if docker run --rm \ - --name "$CONTAINER_NAME" \ - -v "$SOURCE":/source:ro \ - -v "$TARGET":/target \ - alpine \ - sh -c "cp -a /source/. /target/"; then - - echo "Data copied successfully" -else - echo "ERROR: Copy failed" - docker volume rm "$TARGET" 2>/dev/null || true - exit 1 -fi - -# Delete source if requested -if [ "$DELETE_SOURCE" = true ]; then - echo "Deleting source volume..." - docker volume rm "$SOURCE" - echo "Source volume deleted" -fi - -echo "" -echo "Volume rename complete: $SOURCE -> $TARGET" diff --git a/extra/volume-renamer/rename-user-volumes.sh b/extra/volume-renamer/rename-user-volumes.sh new file mode 100755 index 0000000..8d26a62 --- /dev/null +++ b/extra/volume-renamer/rename-user-volumes.sh @@ -0,0 +1,148 @@ +#!/bin/bash +# Rename Docker volumes from one user pattern to another +# Handles Docker's dot-to-hex encoding (. becomes -2e) + +set -e + +show_help() { + cat << EOF +Usage: $0 [--dry-run] [--keep-orig] + +Rename Docker volumes from one user to another (. encoded as -2e). + + --dry-run Show mappings without changes + --keep-orig Keep original volumes + +Example: $0 --dry-run oldnick first.last + jupyterlab-oldnick_home -> jupyterlab-first-2elast_home +EOF + exit 0 +} + +# Parse arguments +DRY_RUN=false +KEEP_ORIG=false +POSITIONAL=() + +while [[ $# -gt 0 ]]; do + case $1 in + --dry-run) + DRY_RUN=true + shift + ;; + --keep-orig) + KEEP_ORIG=true + shift + ;; + -h|--help) + show_help + ;; + -*) + echo "Unknown option: $1" + echo "Use --help for usage" + exit 1 + ;; + *) + POSITIONAL+=("$1") + shift + ;; + esac +done + +# Check required arguments +if [ ${#POSITIONAL[@]} -lt 2 ]; then + show_help +fi + +SOURCE_PATTERN="${POSITIONAL[0]}" +TARGET_USER="${POSITIONAL[1]}" + +# Encode dots as -2e for Docker volume names +TARGET_ENCODED=$(echo "$TARGET_USER" | sed 's/\./-2e/g') + +# Find matching volumes +VOLUMES=$(docker volume ls --format '{{.Name}}' | grep -E "jupyterlab-${SOURCE_PATTERN}[_-]" || true) + +if [ -z "$VOLUMES" ]; then + echo "No volumes found matching pattern: jupyterlab-${SOURCE_PATTERN}[_-]*" + exit 0 +fi + +echo "Volume rename mappings:" +echo "" + +# Build mapping list +declare -a SOURCES +declare -a TARGETS + +while IFS= read -r VOL; do + # Replace source pattern with encoded target + NEW_VOL=$(echo "$VOL" | sed "s/jupyterlab-${SOURCE_PATTERN}/jupyterlab-${TARGET_ENCODED}/") + SOURCES+=("$VOL") + TARGETS+=("$NEW_VOL") + echo " $VOL" + echo " -> $NEW_VOL" + echo "" +done <<< "$VOLUMES" + +if [ "$DRY_RUN" = true ]; then + echo "[DRY RUN] No changes made" + exit 0 +fi + +echo "---" +if [ "$KEEP_ORIG" = true ]; then + echo "Mode: copy (original volumes will be kept)" +else + echo "Mode: move (original volumes will be deleted)" +fi +echo "" + +read -p "Proceed with rename? [y/N] " CONFIRM +if [ "$CONFIRM" != "y" ] && [ "$CONFIRM" != "Y" ]; then + echo "Aborted" + exit 0 +fi + +echo "" + +# Perform renames +for i in "${!SOURCES[@]}"; do + SOURCE="${SOURCES[$i]}" + TARGET="${TARGETS[$i]}" + + echo "Renaming: $SOURCE -> $TARGET" + + # Check target doesn't exist + if docker volume inspect "$TARGET" >/dev/null 2>&1; then + echo " ERROR: Target volume already exists, skipping" + continue + fi + + # Create target volume + docker volume create "$TARGET" >/dev/null + + # Copy data + CONTAINER_NAME="vol_copy_$(date +%s)_$$_$i" + if docker run --rm \ + --name "$CONTAINER_NAME" \ + -v "$SOURCE":/source:ro \ + -v "$TARGET":/target \ + alpine \ + sh -c "cp -a /source/. /target/" 2>/dev/null; then + + echo " Copied successfully" + + # Remove original if not keeping + if [ "$KEEP_ORIG" = false ]; then + docker volume rm "$SOURCE" >/dev/null + echo " Original removed" + fi + else + echo " ERROR: Copy failed" + docker volume rm "$TARGET" 2>/dev/null || true + fi +done + +echo "" +echo "Done"