diff --git a/ChangeLog b/ChangeLog
index bd59eb998794f51a0e782a45af08a4ea9a8b5f80..ab1282165c12a35b290f68ac5f3f40aa994a18f1 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,54 @@
+2007-04-03  Per Cederqvist  <ceder@sedesopim.lysator.liu.se>
+
+	LYSrdiff partitions are now named after both the disk and the
+	partition, such as "2/0".  Support cloning.  Support watching the
+	backup activity remotely.  Various other fixes and improvements.
+
+	* lysrdiff-status: Adapted to the new disk/part scheme, and the
+	new double "--" in state files.  Have a per-user state file, so
+	that many users can use this script.
+
+	* lysrdiff-set-status.py: New.  Talk to lysrdiff-monitord.py.
+
+	* lysrdiff-move-obsolete: New option: --auto-confirm.  Adapted to
+	the new disk/part scheme, and the new double "--" in state files.
+
+	* lysrdiff-move-job: New.  Move a job from one disk to another.
+	(Actually, the old backup copy is left in place.  Use
+	lysrdiff-move-obsolete to deal with it.)
+
+	* lysrdiff-monitord.py: New.  Keep track of what each disk is
+	doing, and provide a vt100-over-telnet interface to watch it
+	remotely.
+
+	* lysrdiff-label-disk: New.  Create the lysrdiff.id file and the
+	basic directory structure.  Create space filler files that can be
+	removed in an emergency.
+
+	* lysrdiff-df: New.  Run df on all mounted lysrdiff partitions.
+
+	* fetch-backup-work: Fetch all of lenin:/home.
+
+	* distribute-tasks: The lysrdiff argument is now on the form
+	disk/part.
+
+	* Makefile (install): Added lysrdiff-label-disk and lysrdiff-df.
+	(install-one-task): Added lysrdiff-set-status.py and
+	lysrdiff-monitord.py.
+
+	* backup-one-task: Replaced the lysrdiffpart argument with disk
+	and part arguments.  Handle --only-prune.  Log status using
+	lysrdiff-set-status.py.  Removed special-case remoterdiff for
+	sedesopim.  The parts of state file names are now separated by
+	"--" intead of "-", to avoid confusion with categories and
+	subcategories that contain a "-".  Remove increments older than
+	120 days.  Log the disk usage after a backup completes.  Clone the
+	backup to all mounted copies.
+
+	* backup-all: Added --only-prune.  The part argument is now a
+	disk/part string, and the format of the lysrdiff.id file has
+	changed.
+
 2007-01-25  Per Cederqvist  <ceder@sedesopim.lysator.liu.se>
 
 	Removed special cases.
diff --git a/Makefile b/Makefile
index 94f3d5f3003602f43ba757508fe8ca943bfce351..eddadbb7374ab22b0bf02caae97611742697b9ce 100644
--- a/Makefile
+++ b/Makefile
@@ -8,7 +8,11 @@ install: install-one-task
 	cp distribute-tasks $(BINDIR)/
 	cp fetch-backup-work $(BINDIR)/
 	cp lysrdiff-status $(BINDIR)/
+	cp lysrdiff-df $(BINDIR)/
 	cp lysrdiff-move-obsolete $(BINDIR)/
+	cp lysrdiff-label-disk $(BINDIR)/
 
 install-one-task:
 	cp backup-one-task $(BINDIR)/
+	cp lysrdiff-set-status.py $(BINDIR)/
+	cp lysrdiff-monitord.py $(BINDIR)/
diff --git a/backup-all b/backup-all
index e8174873f239423fbc54a604ba7a8d06d2cf35ec..32d04cc35e565932f4906931f68b5aaa315064aa 100755
--- a/backup-all
+++ b/backup-all
@@ -1,7 +1,7 @@
 #!/bin/sh
 
 usage () {
-  echo $0: usage: $0 '[ options ] partno...' >&2
+  echo $0: usage: $0 '[ options ] disk/part [ disk/part ... ]' >&2
   echo ' --failed   Only run backup jobs that failed.' >&2
   echo ' --retry    Only run backup jobs that produced warnings.' >&2
   echo ' --new      Only run backup jobs that have never completed.' >&2
@@ -10,6 +10,7 @@ usage () {
 failed=
 retry=
 new=
+only_prune=
 
 while true
 do
@@ -23,6 +24,9 @@ do
     x--new)
       shift
       new=--new;;
+    x--only-prune)
+      shift
+      only_prune=--only-prune;;
     x--*)
       usage
       exit 1;;
@@ -32,6 +36,7 @@ done
 
 PARTS="$@"
 
+
 if [ "$PARTS" = "" ]
 then
   echo $0: you forgot to specify what parts to back up >&2
@@ -39,10 +44,12 @@ then
 fi
 
 rundf () {
-  df -h /lysrdiff/0/perm | sed -n 1p
+  df -h /lysrdiff/1/perm/0 | sed -n 1p
   for lysrdiffpart in $PARTS
   do
-    df -h /lysrdiff/$lysrdiffpart/perm | sed 1d
+    disk=`echo $lysrdiffpart|sed sx/.*xx`
+    part=`echo $lysrdiffpart|sed sx.*/xx`
+    df -h /lysrdiff/$disk/perm/$part | sed 1d
   done
 }
 
@@ -50,7 +57,9 @@ rundf
 
 for lysrdiffpart in $PARTS
 do
-  if [ "`cat /lysrdiff/$lysrdiffpart/perm/lysrdiff.id`" != "$lysrdiffpart perm" ]
+  disk=`echo $lysrdiffpart|sed sx/.*xx`
+  part=`echo $lysrdiffpart|sed sx.*/xx`
+  if [ "`cat /lysrdiff/$disk/perm/$part/lysrdiff.id`" != "$disk perm $part" ]
   then
     echo lysrdiffpart $lysrdiffpart not found >&2
     exit 1
@@ -62,12 +71,16 @@ done
 total=0
 for lysrdiffpart in $PARTS
 do
-  total=`expr $total + \`wc -l </lysrdiff/$lysrdiffpart/perm/lysrdiff/tasks\``
+  disk=`echo $lysrdiffpart|sed sx/.*xx`
+  part=`echo $lysrdiffpart|sed sx.*/xx`
+  total=`expr $total + \`wc -l </lysrdiff/$disk/perm/$part/lysrdiff/tasks\``
 done
 
 ctr=1
 for lysrdiffpart in $PARTS
 do
+  disk=`echo $lysrdiffpart|sed sx/.*xx`
+  part=`echo $lysrdiffpart|sed sx.*/xx`
   while read category subcategory server serverpath
   do
 
@@ -88,11 +101,11 @@ do
     fi
 
     /nobackup/backup.lysator/bin/backup-one-task \
-      $failed $retry $new \
-      $lysrdiffpart "$category" "$subcategory" "$server" "$serverpath" \
-      "$ctr/$total $category $subcategory"
+      $failed $retry $new $only_prune \
+      $disk $part "$category" "$subcategory" "$server" "$serverpath" \
+      "$ctr/$total $disk/$part $category $subcategory"
     ctr=`expr $ctr + 1`
-  done < /lysrdiff/$lysrdiffpart/perm/lysrdiff/tasks
+  done < /lysrdiff/$disk/perm/$part/lysrdiff/tasks
 done
 
 rundf
diff --git a/backup-one-task b/backup-one-task
index 5370a8f4a4bcc25ada2abf458c418f7c9bad7a1b..8e6f9adb60e80517251556b5d42ace84d35d12d0 100755
--- a/backup-one-task
+++ b/backup-one-task
@@ -1,17 +1,23 @@
 #!/bin/sh
 
 usage () {
-    echo $0: usage: $0 [ options ] lysrdiffpart category subcategory server serverpath msg >&2
+    echo $0: usage: $0 [ options ] disk part category subcategory server serverpath msg >&2
     echo Example: $0 0 home ceder inservitus /export/home/ceder "1/123" >&2
     echo Recognized options: >&2
     echo ' --failed     Only run failed backups' >&2
     echo ' --retry      Only run backups with output from rdiff-backup' >&2
     echo ' --new        Only run backups that never completed' >&2
+    echo ' --only-prune Only prune old increments' >&2
+    echo 'Backups are always performed to the "perm" disk.' >&2
+    echo 'Once the backup is finished, it is cloned to all mounted copies.' >&2
 }
 
 failed=0
 retry=0
 new=0
+only_prune=0
+
+ss=/nobackup/backup.lysator/bin/lysrdiff-set-status.py
 
 while [ $# -gt 1 ]
 do
@@ -25,6 +31,9 @@ do
     x--new)
       shift
       new=1;;
+    x--only-prune)
+      shift
+      only_prune=1;;
     x--*)
       usage
       exit 1;;
@@ -32,27 +41,28 @@ do
   esac
 done  
 
-if [ $# != 6 ]
+if [ $# != 7 ]
 then
     usage
     exit 1
 fi
 
-lysrdiffpart="$1"
-category="$2"
-subcategory="$3"
-server="$4"
-serverpath="$5"
-msg="$6"
+disk="$1"
+part="$2"
+lysrdiffpart="$1/$2"
+category="$3"
+subcategory="$4"
+server="$5"
+serverpath="$6"
+msg="$7"
 
 case "$server" in
     wrath) remoterdiff=/usr/bin/rdiff-backup;;
     manhattan) remoterdiff=/usr/bin/rdiff-backup;;
-    sedesopim) remoterdiff=/usr/bin/rdiff-backup;;
     *) remoterdiff=/opt/LYSrdiff/bin/rdiff-backup;;
 esac
 
-lysrdiff="/lysrdiff/$lysrdiffpart/perm/lysrdiff"
+lysrdiff="/lysrdiff/$disk/perm/$part/lysrdiff"
 base="$lysrdiff/backups/$category/$subcategory"
 exclude="$base"/exclude
 excl_abs="$base"/exclude.abs
@@ -62,19 +72,21 @@ summaryfile="$base"/backup-summary.txt
 lockdir="$base"/lock
 state="$lysrdiff"/state
 origin="$base"/origin
+rdiffdir="$files/rdiff-backup-data"
+incrementsdir="$rdiffdir/increments"
 
 mkdir -p "$files"
 mkdir "$lockdir" || exit 1
 
-statebase="$state"/"$category"-"$subcategory"
+statebase="$state"/"$category"--"$subcategory"
 
-if [ $failed = 1 ] && [ ! -f "$statebase"-fail ]
+if [ $failed = 1 ] && [ ! -f "$statebase"--fail ]
 then
     rmdir "$lockdir"
     exit 0
 fi
 
-if [ $new = 1 ] && [ -f "$statebase"-end ]
+if [ $new = 1 ] && [ -f "$statebase"--end ]
 then
     rmdir "$lockdir"
     exit 0
@@ -101,7 +113,7 @@ then
 
     # Leave the origin.new file.  Flag this as a failure.
     touch "$base"/last-failure
-    touch "$statebase"-fail
+    touch "$statebase"--fail
     rmdir "$lockdir"
 
     exit 0
@@ -113,7 +125,27 @@ unset SSH_AUTH_SOCK
 
 CLR='\e[2K\r'
 
-echo -ne "${msg}: fetching exclude file"
+AGE=120D
+
+echo -ne "${CLR}${msg}: removing increments older than $AGE"
+$ss $disk $part "${msg}: removing increments older than $AGE"
+
+/opt/LYSrdiff/bin/rdiff-backup \
+    --remove-older-than $AGE \
+    --null-separator \
+    --force \
+    -v \
+    "$files" > "$rdifflogfile" 2>&1
+
+if [ $only_prune = 1 ]
+then
+    rmdir "$lockdir"
+    $ss $disk $part ""
+    exit 0
+fi
+
+echo -ne "${CLR}${msg}: fetching exclude file"
+$ss $disk $part "${msg}: fetching exclude file"
 
 # Fetch an up-to-date exclude file.
 rm -f "$exclude"
@@ -129,10 +161,10 @@ sed < "$exclude" \
 
 
 # See how long time the previous backup took.
-if [ -f "$statebase"-start ] && [ -f "$statebase"-end ]
+if [ -f "$statebase"--start ] && [ -f "$statebase"--end ]
 then
-    startdec="`stat --format '%Y' \"$statebase\"-start`"
-    enddec="`stat --format '%Y' \"$statebase\"-end`"
+    startdec="`stat --format '%Y' \"$statebase\"--start`"
+    enddec="`stat --format '%Y' \"$statebase\"--end`"
     seconds=`expr $enddec - $startdec`
     ETA=`date +' (ETA: %H:%M:%S)' -d "$seconds seconds"`
 else
@@ -140,9 +172,10 @@ else
 fi
 
 touch "$base"/backup-attempt-start
-touch "$statebase"-attempt
+touch "$statebase"--attempt
 
 echo -ne "${CLR}${msg}: running rdiff-backup$ETA"
+$ss $disk $part "${msg}: rdiff-backup$ETA"
 
 schema="ssh -o BatchMode=yes -o ServerAliveInterval=120"
 schema="$schema -a -k -x -i /root/.ssh/backupkey"
@@ -157,48 +190,81 @@ schema="$schema %s $remoterdiff --server"
 exit=$?
 
 echo -ne "${CLR}"
+$ss $disk $part "${msg}: updating status files"
 
 if [ $exit = 0 ]
 then
     touch "$base"/last-good-backup
     mv "$base"/backup-attempt-start "$base"/last-good-start
-    touch "$statebase"-end
-    mv "$statebase"-attempt "$statebase"-start
+    touch "$statebase"--end
+    mv "$statebase"--attempt "$statebase"--start
+
+    starttime="`stat --format '%y' \"$statebase\"--start|sed 's/\..*//'`"
+    endtime="`stat --format '%y' \"$statebase\"--end|sed 's/\..*//'`"
+    startdec="`stat --format '%Y' \"$statebase\"--start`"
+    enddec="`stat --format '%Y' \"$statebase\"--end`"
+
+    echo -ne "${CLR}${msg}: running du"
+    $ss $disk $part "${msg}: running du"
 
-    starttime="`stat --format '%y' \"$statebase\"-start|sed 's/\..*//'`"
-    endtime="`stat --format '%y' \"$statebase\"-end|sed 's/\..*//'`"
-    startdec="`stat --format '%Y' \"$statebase\"-start`"
-    enddec="`stat --format '%Y' \"$statebase\"-end`"
+    sizes=""
 
-    echo $starttime $endtime time=`expr $enddec - $startdec` >> "$summaryfile"
+    if [ -d "$files" ]
+    then
+	totalsize=`du -ks "$files" | awk '{print $1}'`
+	sizes="$sizes totalsize=$totalsize"
+	echo $totalsize > "$base"/totalsize
+    fi
+
+    if [ -d "$rdiffdir" ]
+    then
+	metasize=`du -ks "$rdiffdir" --exclude increments | awk '{print $1}'`
+	sizes="$sizes metasize=$metasize"
+	echo $metasize > "$base"/metasize
+    fi
+	
+    if [ -d "$incrementsdir" ]
+    then
+	incrementsize=`du -ks "$incrementsdir" | awk '{print $1}'`
+	sizes="$sizes incrementsize=$incrementsize"
+	echo $incrementsize > "$base"/incrementsize
+    fi
+
+    echo -ne "${CLR}"
+    $ss $disk $part "${msg}: updating log file"
+
+    time=`expr $enddec - $startdec`
+    echo $time > "$base"/time
+    echo $starttime $endtime time=$time"$sizes" \
+	>> "$summaryfile"
 
     rm -f "$base"/last-failure
-    rm -f "$statebase"-fail
+    rm -f "$statebase"--fail
     if [ `wc -c < "$rdifflogfile"` = 0 ]
     then
-      touch "$statebase"-nowarn
+      touch "$statebase"--nowarn
       touch "$base"/last-nowarn-backup
-      rm -f "$statebase"-neverwarnfree
-      rm -f "$statebase"-warn
+      rm -f "$statebase"--neverwarnfree
+      rm -f "$statebase"--warn
       logger -p local5.info -t LYSrdiff "$lysrdiffpart $category $subcategory: OK"
     else
       echo ${msg}: WARNING: output from rdiff-backup:
       echo
       sed 's/^/ /' "$rdifflogfile"
       echo
-      if [ ! -f "$statebase"-warn ]
+      if [ ! -f "$statebase"--warn ]
       then
-        touch "$statebase"-warn
+        touch "$statebase"--warn
       fi
-      if [ ! -f "$statebase"-nowarn ] && [ ! -f "$statebase"-neverwarnfree ]
+      if [ ! -f "$statebase"--nowarn ] && [ ! -f "$statebase"--neverwarnfree ]
       then
-	touch "$statebase"-neverwarnfree
+	touch "$statebase"--neverwarnfree
       fi
       logger -p local5.info -t LYSrdiff "$lysrdiffpart $category $subcategory: OK with warnings"
     fi
 else
     mv "$base"/backup-attempt-start "$base"/last-failure
-    mv "$statebase"-attempt "$statebase"-fail
+    mv "$statebase"--attempt "$statebase"--fail
     echo ${msg}: FAIL:
     echo
     sed 's/^/  /' "$rdifflogfile"
@@ -206,4 +272,42 @@ else
     logger -p local5.info -t LYSrdiff "$lysrdiffpart $category $subcategory: FAIL"
 fi
 
+for copy in A B
+do
+    id="/lysrdiff/$disk/$copy/$part/lysrdiff.id"
+    if [ -f "$id" ]
+    then
+	if [ "`cat $id`" != "$disk $copy $part" ]
+	then
+	    echo wrong disk mounted at /lysrdiff/$disk/$copy/$part >&2
+	else
+	    copylysrdiff="/lysrdiff/$disk/$copy/$part/lysrdiff"
+	    copybase="$copylysrdiff/backups/$category/$subcategory"
+	    copystate="$copylysrdiff"/state
+	    copystatebase="$copystate"/"$category"--"$subcategory"
+	    cp /lysrdiff/$disk/perm/$part/lysrdiff/tasks \
+		/lysrdiff/$disk/$copy/$part/lysrdiff/tasks.tmp \
+	    && mv -f /lysrdiff/$disk/$copy/$part/lysrdiff/tasks.tmp \
+		/lysrdiff/$disk/$copy/$part/lysrdiff/tasks
+
+	    mkdir -p "$copybase"
+	    if mkdir "$copybase"/lock
+	    then
+		echo -ne "${CLR}${msg}: copying to $copy"
+		$ss $disk $part "${msg}: copying to $copy"
+
+    	        rsync -a --delete "$base"/ "$copybase"
+	        rm -f "$copystatebase"--* || true
+	        cp -a "$statebase"--* "$copystate"
+	        rmdir "$copybase"/lock
+		echo -ne "${CLR}"
+	    else
+		echo copy $disk $copy $part already locked >&2
+	    fi
+	fi
+    fi
+done
+
+$ss $disk $part ""
+
 rmdir "$lockdir"
diff --git a/distribute-tasks b/distribute-tasks
index 8bd4b6f6230e732ba4bd34b5cf21b17e8c185c9c..10badd27bccb90b2e72b2221724600472ac19703 100755
--- a/distribute-tasks
+++ b/distribute-tasks
@@ -10,12 +10,13 @@ ROOT = "/lysrdiff"
 
 class JobInfo(object):
     def __init__(self, category, subcategory, host, directory,
-                 lysrdiffpart=None):
+                 disk=None, part=None):
         self.__category = category
         self.__subcategory = subcategory
         self.__host = host
         self.__directory = directory
-        self.__lysrdiffpart = lysrdiffpart
+        self.__disk = disk
+        self.__part = part
         self.__active = False
 
     def set_active(self):
@@ -25,7 +26,7 @@ class JobInfo(object):
         return self.__active
 
     def set_lysrdiffpart(self, part):
-        self.__lysrdiffpart = part
+        self.__disk, self.__part = part
 
     def category(self):
         return self.__category
@@ -40,7 +41,10 @@ class JobInfo(object):
         return self.__directory
 
     def lysrdiffpart(self):
-        return self.__lysrdiffpart
+        if self.__disk is None:
+            return None
+        else:
+            return (self.__disk, self.__part)
 
     def source(self):
         return (self.host(), self.directory())
@@ -49,8 +53,8 @@ class JobInfo(object):
         return (self.category(), self.subcategory())
 
     def rdiff_dest(self):
-        return "/lysrdiff/%d/perm/lysrdiff/backups/%s/%s" % (
-            self.lysrdiffpart(), self.category(), self.subcategory())
+        return "/lysrdiff/%d/perm/%d/lysrdiff/backups/%s/%s" % (
+            self.__disk, self.__part, self.category(), self.subcategory())
 
     def task_desc(self):
         return "%s %s %s %s" % (
@@ -58,7 +62,9 @@ class JobInfo(object):
             self.host(), self.directory())
 
 def newtasks():
-    return int(open("/nobackup/backup.lysator/var/newtasks").read())
+    where = open("/nobackup/backup.lysator/var/newtasks").read()
+    disk, part = where.split("/")
+    return (int(disk), int(part))
 
 # key: (host, directory)
 # value: JobInfo
@@ -74,19 +80,19 @@ ordered_tasks = []
 fatal = False
 interactive = False
 
-def tasklist_file(lysrdiffpart):
-    return "/lysrdiff/%d/perm/lysrdiff/tasks" % lysrdiffpart
+def tasklist_file(disk, part):
+    return "/lysrdiff/%d/perm/%d/lysrdiff/tasks" % (disk, part)
 
-def parse_line(line, lysrdiffpart=None):
+def parse_line(line, disk=None, part=None):
     [category, subcategory, host, directory] = line.split()
-    info = JobInfo(category, subcategory, host, directory, lysrdiffpart)
+    info = JobInfo(category, subcategory, host, directory, disk, part)
     return info
 
-def read_tasks(lysrdiffpart):
+def read_tasks(disk, part):
     global fatal
 
-    for line in file(tasklist_file(lysrdiffpart), "r"):
-        info = parse_line(line, lysrdiffpart)
+    for line in file(tasklist_file(disk, part), "r"):
+        info = parse_line(line, disk, part)
 
         if (info.host(), info.directory()) in tasks_per_source:
             sys.stderr.write("Duplicate backup detected!\n  %s\n  %s\n" % (
@@ -172,14 +178,15 @@ def write_task_lists(partitions):
         part = job.lysrdiffpart()
         if part in partitions:
             if part not in files:
-                fn = tasklist_file(job.lysrdiffpart())
+                fn = tasklist_file(job.lysrdiffpart()[0],
+                                   job.lysrdiffpart()[1])
                 os.mkdir(fn + ".lock")
                 files[part] = file(fn + ".new", "w")
             files[part].write(job.task_desc() + "\n")
 
     for lysrdiffpart, fp in files.items():
         fp.close()
-        fn = tasklist_file(lysrdiffpart)
+        fn = tasklist_file(lysrdiffpart[0], lysrdiffpart[1])
         if skipped:
             os.system("diff -u %s %s" % (fn, fn + ".new"))
             raw_input("[confirm]")
@@ -196,10 +203,14 @@ def main():
         if opt in ("-i", "--interactive"):
             interactive = True
 
-    partitions = sets.Set([int(x) for x in args])
+    partitions = sets.Set()
+    for spec in args:
+        disk, part = spec.split("/")
+        partitions.add((int(disk), int(part)))
 
-    for lysrdiffpart in range(2):
-        read_tasks(lysrdiffpart)
+    for disk in [1, 2]:
+        for part in [0, 1]:
+            read_tasks(disk, part)
     if interactive:
         read_new_tasks()
     if not fatal:
diff --git a/fetch-backup-work b/fetch-backup-work
index fe9658cd90a7ac215586ed55453f17f9b4ee382a..f515bc565dce42a5fa7f8e421fcfcf43bee748f0 100755
--- a/fetch-backup-work
+++ b/fetch-backup-work
@@ -87,9 +87,7 @@ echo lenin var-log lenin /var/log >> $NT
 echo lenin var-spool-postfix lenin /var/spool/postfix >> $NT
 echo lenin var-lib-mailman lenin /var/lib/mailman >> $NT
 
-# FIXME: Not yet backed up: lenin:/home (where all the user mail exists)
 ssh lenin 'cd /home && ls -1 | while read d ; do [ -d "$d" ] && [ ! -L "$d" ] && echo "$d" ; done' \
-| grep '^[tvamqpojk]' \
 | awk '{ print "mail", $1, "lenin", "/home/" $1 }' \
 | sort >> $NT
 
diff --git a/lysrdiff-df b/lysrdiff-df
new file mode 100755
index 0000000000000000000000000000000000000000..88308ab3c30fceec3828e670e5b97ffb3211f15a
--- /dev/null
+++ b/lysrdiff-df
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+parts=""
+for d in 1 2
+do
+    for p in 0 1
+    do
+        for copy in perm A B
+	do
+	    if [ -f /lysrdiff/$d/$copy/$p/lysrdiff.id ]
+	    then
+		parts=" $parts /lysrdiff/$d/$copy/$p"
+	    fi
+	done
+    done
+done
+
+df -h $parts
diff --git a/lysrdiff-label-disk b/lysrdiff-label-disk
new file mode 100755
index 0000000000000000000000000000000000000000..e5fc686a057e14d8f1e0a978301840fb288cc373
--- /dev/null
+++ b/lysrdiff-label-disk
@@ -0,0 +1,54 @@
+#!/bin/sh
+
+disk=$1
+copy=$2
+part=$3
+
+cd /lysrdiff/$disk/$copy/$part || exit 1
+if [ -d lost+found ]
+then :
+else
+    echo lost+found missing >&2
+    exit 1
+fi
+
+if [ -f lysrdiff.id ]
+then
+    if [ "`cat lysrdiff.id`" = "$disk $copy $part" ]
+    then
+	echo Good lysrdiff.id already exited >&2
+    else
+	echo Disk mismatch error. >&2
+	echo Mounted: `cat lysrdiff.id` >&2
+	echo Expected: $disk $copy $part >&2
+	exit 1
+    fi
+else
+    echo $disk $copy $part > lysrdiff.id
+fi
+
+cat > README << EOF
+This disc belongs to Lysator.  It contains backups.  Please contact
+root@lysator.liu.se if you find this disc.  Please don't read the
+files on it, as it would violate the personal integrity of the members
+of Lysator.
+
+Your cooperation will be appreciated.
+EOF
+
+mkdir -p spacefiller
+for i in 0 1 2 3 4 5 6 7 8 9
+do
+    fn=spacefiller/filler-$i
+    if [ -f $fn ] || dd if=/dev/zero of=$fn bs=1024 count=10240
+    then :
+    else
+	rm $fn
+	break
+    fi
+done
+
+mkdir -p lysrdiff/backups
+mkdir -p lysrdiff/state
+
+[ -f lysrdiff/tasks ] || touch lysrdiff/tasks
diff --git a/lysrdiff-monitord.py b/lysrdiff-monitord.py
new file mode 100755
index 0000000000000000000000000000000000000000..34207d2ec9cdd99dedd402f688001482c07f95da
--- /dev/null
+++ b/lysrdiff-monitord.py
@@ -0,0 +1,223 @@
+#!/usr/bin/env python
+
+import socket
+import select
+import sys
+import time
+
+class Client(object):
+    def __init__(self, accept_retval, dispatcher):
+        self.__s, peer = accept_retval
+        self.__peer_ip = peer[0]
+        self.__peer_port = peer[1]
+        dispatcher.register(self)
+        self.__writebuf = ""
+        self.__readbuf = ""
+        self.__got_eof = False
+        self.__dispatcher = dispatcher
+
+    def fileno(self):
+        return self.__s.fileno()
+
+    def want_read(self):
+        if self.__got_eof:
+            return []
+        else:
+            return [self]
+
+    def want_write(self):
+        if self.__writebuf:
+            return [self]
+        else:
+            return []
+
+    def read_event(self):
+        try:
+            bytes = self.__s.recv(100)
+        except socket.error, e:
+            if e[0] == errno.ECONNTIMEOUT:
+                self.got_eof(True)
+                return
+            raise
+        if bytes == "":
+            self.got_eof()
+            return
+
+        self.__readbuf += bytes
+        self.parse()
+
+    def got_eof(self, error=False):
+        self.__got_eof = True
+        if not error:
+            self.parse()
+        if len(self.__readbuf) > 0:
+            sys.stderr.write("EOF with %d bytes pending from client.\n" % (
+                len(self.__readbuf)))
+        if len(self.__writebuf) == 0 or error:
+            self.__dispatcher.unregister(self)
+            self.__s.close()
+            del self
+
+    def write_event(self):
+        rv = self.__s.send(self.__writebuf)
+        if rv > 0:
+            self.__writebuf = self.__writebuf[rv:]
+
+    def parse(self):
+        ix = self.__readbuf.find("\n")
+        if ix < 0:
+            return
+        cmd = self.__readbuf[:ix]
+        self.__readbuf = self.__readbuf[ix+1:]
+        self.handle_cmd(cmd)
+
+    def handle_cmd(self, cmd):
+        if cmd == "":
+            pass
+        elif cmd.startswith("set-status "):
+            split = cmd.split(None, 3)
+            if len(split) == 3:
+                cmd, disk, part = split
+                status = ""
+            else:
+                cmd, disk, part, status = split
+            dispatcher.set_status(int(disk), int(part), status)
+
+        else:
+            sys.stderr.write("Unknown command received from client.\n")
+
+    def write(self, buf):
+        self.__writebuf += buf
+        self.write_event()
+
+class Server(object):
+    IP = '127.0.0.1'
+    PORT = 9933
+    CLIENT = Client
+
+    def __init__(self, dispatcher):
+        self.__s = socket.socket()
+        self.__s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
+        self.__s.bind((self.IP, self.PORT))
+        self.__s.listen(3)
+        self.__s.setblocking(False)
+        dispatcher.register(self)
+        self.__dispatcher = dispatcher
+
+    def want_read(self):
+        return [self]
+
+    def want_write(self):
+        return []
+
+    def fileno(self):
+        return self.__s.fileno()
+
+    def read_event(self):
+        self.CLIENT(self.__s.accept(), self.__dispatcher)
+
+class Vt100Client(Client):
+
+    def handle_cmd(self, cmd):
+        pass
+
+    def __init__(self, accept_retval, dispatcher):
+        Client.__init__(self, accept_retval, dispatcher)
+        dispatcher.register_vt100(self)
+
+class Vt100Server(Server):
+    IP = ''
+    PORT = 9934
+    CLIENT = Vt100Client
+
+class Dispatcher(object):
+    def __init__(self):
+        self.__clients = set()
+        self.__vt100 = set()
+        self.__break = False
+        self.__status = {}
+
+    def register(self, client):
+        self.__clients.add(client)
+
+    def unregister(self, client):
+        self.__clients.remove(client)
+        self.__vt100.discard(client)
+        self.__break = True
+        self.write_monitor_status()
+
+    def toploop(self):
+        while True:
+            r = []
+            w = []
+            self.__break = False
+
+            for c in self.__clients:
+                r += c.want_read()
+                w += c.want_write()
+
+            r, w, e = select.select(r, w, [], None)
+
+            for c in w:
+                if self.__break:
+                    break
+                c.write_event()
+
+            for c in r:
+                if self.__break:
+                    break
+                c.read_event()
+
+    def register_vt100(self, client):
+        self.__vt100.add(client)
+
+        msg = "\033[2J\033[HLYSrdiff status\r\n==============="
+        for disk in self.__status.keys():
+            for part in self.__status[disk].keys():
+                msg += self.format_status_vt100(disk, part)
+        client.write(msg)
+        self.write_monitor_status()
+
+    def write_monitor_status(self):
+        msg = self.format_monitor_status()
+        for c in self.__vt100:
+            c.write(msg)
+
+    def format_monitor_status(self):
+        msg = "\033[9;1H"
+        msg += "%d clients" % len(self.__vt100)
+        msg += "\033[K"
+        msg += "\033[H"
+        return msg
+
+    def set_status(self, disk, part, status):
+        if disk not in self.__status:
+            self.__status[disk] = {}
+
+        timestamp = time.strftime("%Y-%m-%d %H:%M:%S",
+                                  time.localtime(time.time()))
+        self.__status[disk][part] = (timestamp, status)
+
+        msg = self.format_status_vt100(disk, part)
+        for c in self.__vt100:
+            c.write(msg)
+
+        print timestamp, "%d/%d" % (disk, part), status
+
+    def format_status_vt100(self, disk, part):
+        timestamp, status = self.__status[disk][part]
+        
+        msg = "\033[%d;1H" % (2 + 2 * disk + part)
+        msg += timestamp
+        msg += " "
+        msg += status
+        msg += "\033[K"
+        msg += "\033[H"
+
+        return msg
+
+if __name__ == '__main__':
+    dispatcher = Dispatcher()
+    Server(dispatcher)
+    Vt100Server(dispatcher)
+    dispatcher.toploop()
diff --git a/lysrdiff-move-job b/lysrdiff-move-job
new file mode 100755
index 0000000000000000000000000000000000000000..9b3c2f557c7cf2d160fd272f282d372505bfb18a
--- /dev/null
+++ b/lysrdiff-move-job
@@ -0,0 +1,100 @@
+#!/bin/sh
+
+uncloned=0
+
+if [ x"$1" = x"--uncloned" ]
+then
+    uncloned=1
+    shift
+fi
+
+if [ $# != 2 ]
+then
+    echo usage: "$0 [ --uncloned ] from to" >&2
+    exit 2
+fi
+
+srcdisk=`echo $1|sed sx/.*xx`
+srcpart=`echo $1|sed sx.*/xx`
+dstdisk=`echo $2|sed sx/.*xx`
+dstpart=`echo $2|sed sx.*/xx`
+
+srctask=/lysrdiff/$srcdisk/perm/$srcpart/lysrdiff/tasks
+dsttask=/lysrdiff/$dstdisk/perm/$dstpart/lysrdiff/tasks
+
+mkdir $srctask.lock || exit 1
+mkdir $dsttask.lock || {
+	rmdir $srctask.lock
+	exit 1
+}
+
+while true
+do
+    MOVEDTASK=/tmp/movedtask.$$
+    head -1 $srctask > $MOVEDTASK || exit 1
+    sed 1d $srctask > $srctask.new || exit 1
+    cat $dsttask $MOVEDTASK > $dsttask.new || exit 1
+
+    read category subcategory rest < $MOVEDTASK
+
+    if [ $uncloned = 0 ]
+    then
+	break
+    fi
+
+    if [ -z "`ls /lysrdiff/$srcdisk/?/$srcpart/lysrdiff/backups/$category/$subcategory 2>/dev/null`" ]
+    then
+	break
+    fi
+
+    echo "Already cloned: $category $subcategory"
+    cat $MOVEDTASK >> $srctask.new || exit 1
+    mv $srctask.new $srctask
+    rm $dsttask.new
+    rm $MOVEDTASK
+done
+
+rm $MOVEDTASK
+
+echo "Moving $category $subcategory"
+ss=/nobackup/backup.lysator/bin/lysrdiff-set-status.py
+$ss $srcdisk $srcpart "Moving $category $subcategory to $dstdisk $dstpart"
+$ss $dstdisk $dstpart "Moving $category $subcategory from $srcdisk $srcpart"
+
+srcroot="/lysrdiff/$srcdisk/perm/$srcpart/lysrdiff"
+dstroot="/lysrdiff/$dstdisk/perm/$dstpart/lysrdiff"
+srcbase="$srcroot/backups/$category/$subcategory"
+dstbase="$dstroot/backups/$category/$subcategory"
+srcstate="$srcroot/state"
+dststate="$dstroot/state"
+
+if [ -d "$dstbase" ]
+then
+    echo "$dstbase": already exists >&2
+    rmdir $srctask.lock
+    rmdir $dsttask.lock
+    exit 1
+fi
+
+if [ ! -d "$srcbase" ]
+then
+    echo "$srcbase": no such dir >&2
+    rmdir $srctask.lock
+    rmdir $dsttask.lock
+    exit 1
+fi
+
+mkdir "$srcbase"/lock
+mkdir -p "$dstbase"/lock
+rsync -a "$srcbase/" "$dstbase" || exit 1
+cp -a "$srcstate/$category--$subcategory--"* "$dststate" || exit 1
+rmdir "$srcbase"/lock
+rmdir "$dstbase"/lock
+
+$ss $srcdisk $srcpart ""
+$ss $dstdisk $dstpart ""
+
+mv $srctask.new $srctask
+mv $dsttask.new $dsttask
+rmdir $srctask.lock
+rmdir $dsttask.lock
diff --git a/lysrdiff-move-obsolete b/lysrdiff-move-obsolete
index 364684cee15a18fb99e0a1fb1db4135585f105b0..4455b7311c7b36eadd0d2ce1bfd2e8a00b3ae77f 100755
--- a/lysrdiff-move-obsolete
+++ b/lysrdiff-move-obsolete
@@ -2,21 +2,31 @@
 
 shopt -s extglob
 
-for base in /lysrdiff/*/perm/lysrdiff
+auto_confirm=0
+
+if [ "$1" = --auto-confirm ]
+then
+    auto_confirm=1
+fi
+
+for base in /lysrdiff/*/*/*/lysrdiff
 do
   cd $base/backups || continue
   for cat in *
   do
+    [ "$cat" = '*' ] && continue
     (
 	cd "$cat" || continue
 	for subcat in *
 	do
+	  [ "$subcat" = '*' ] && continue
+	  
 	  fgrep "$cat $subcat " $base/tasks >/dev/null
 	  if [ $? -eq 1 ]
 	  then
 	      echo $base $cat $subcat seems obsolete
-	      ls -l $base/state/"$cat"-"$subcat"-+([a-z]) | sed 's/^/  /'
-	      for otherbase in /lysrdiff/*/perm/lysrdiff
+	      ls -l $base/state/"$cat"--"$subcat"--+([a-z]) | sed 's/^/  /'
+	      for otherbase in /lysrdiff/*/perm/*/lysrdiff
 	      do
 		if [ $base != $otherbase ]
 		then
@@ -25,7 +35,7 @@ do
 		    then
 			echo '  found in '$otherbase:
 			echo -n '    '
-			ls -l $otherbase/state/"$cat-$subcat-end"
+			ls -l $otherbase/state/"$cat--$subcat--end"
 		    fi
 		fi
 	      done
@@ -34,13 +44,16 @@ do
 		  echo '    ALREADY OBSOLETED!'
 		  continue
 	      fi
-	      echo -n '[CONFIRM]'
-	      read line
+	      if [ $auto_confirm = 0 ]
+	      then
+		  echo -n '[CONFIRM]'
+		  read line
+	      fi
 	      mkdir -p $base/obsolete/backups/"$cat"
 	      mkdir -p $base/obsolete/state
 	      mv $base/backups/"$cat"/"$subcat" \
 	         $base/obsolete/backups/"$cat"/"$subcat"
-	      mv $base/state/"$cat"-"$subcat"-+([a-z]) $base/obsolete/state
+	      mv $base/state/"$cat"--"$subcat"--+([a-z]) $base/obsolete/state
 	      echo
 	  fi
 	done
diff --git a/lysrdiff-set-status.py b/lysrdiff-set-status.py
new file mode 100755
index 0000000000000000000000000000000000000000..c88e9d1b37fd983175f48996eb78da4bdd521fa7
--- /dev/null
+++ b/lysrdiff-set-status.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python
+
+import sys
+import errno
+import socket
+
+def sendmsg(disk, part, msg):
+    s = socket.socket()
+    try:
+        s.connect(("127.0.0.1", 9933))
+    except socket.error, e:
+        if e[0] == errno.ECONNREFUSED:
+            return
+        raise
+    s.send("set-status %d %d %s\n" % (disk, part, msg))
+    s.close()
+
+if __name__ == '__main__':
+    sendmsg(int(sys.argv[1]), int(sys.argv[2]), sys.argv[3])
diff --git a/lysrdiff-status b/lysrdiff-status
index 82d57d14048d372539a45d338a2e93d2da46052f..048037ee2f29249bd29c97032b38f5e1fe452aa6 100755
--- a/lysrdiff-status
+++ b/lysrdiff-status
@@ -1,53 +1,55 @@
 #!/bin/sh
 
-df -hl /lysrdiff/*/perm/lysrdiff
+df -hl /lysrdiff/*/perm/*/lysrdiff
 echo
 
-statecache=/tmp/cached-state
+statecache=/tmp/cached-state-$USER
 
-for part in /lysrdiff/*/perm/lysrdiff
+for base in /lysrdiff/*/perm/*/lysrdiff
 do
-  partno=`echo $part|sed 's%/lysrdiff/\([0-9]*\)/perm/lysrdiff%\1%'`
-  find $part/state -type f -printf "  %TY-%Tm-%Td %TH:%TM:%TS $partno %P\n" \
-      | sort > $statecache.$partno.$$
-  mv -f $statecache.$partno.$$ $statecache-$partno
-  echo -n $partno':'
-  echo -n ' 'Tasks: `wc -l < $part/tasks`
-  echo -n ' 'Fresh: `find $part/state -name \*-start -ctime 0 -print|wc -l`
-  echo -n ' '1day: `find $part/state -name \*-start -ctime 1 -print|wc -l`
-  echo -n ' 'Stale: `find $part/state -name \*-start -ctime +1 -print|wc -l`
-  echo -n ' 'Tot: `ls $part/state/*-end|wc -l`
-  echo -n ' 'Warn: `find $part/state -name \*-warn -print|wc -l`
-  echo ' 'Err: `ls $part/state/*-fail 2>/dev/null|wc -l`
+  disk=`echo $base|sed 's%/lysrdiff/\([0-9]*\)/perm/\([0-9]*\)/lysrdiff%\1%'`
+  part=`echo $base|sed 's%/lysrdiff/\([0-9]*\)/perm/\([0-9]*\)/lysrdiff%\2%'`
+  find $base/state -type f \
+      -printf "  %TY-%Tm-%Td %TH:%TM:%TS $disk/$part %P\n" \
+      | sort > $statecache.$disk.$part.$$
+  mv -f $statecache.$disk.$part.$$ $statecache-$disk.$part
+  echo -n $disk/$part':'
+  echo -n ' 'Tasks: `wc -l < $base/tasks`
+  echo -n ' 'Fresh: `find $base/state -name \*--start -ctime 0 -print|wc -l`
+  echo -n ' '1day: `find $base/state -name \*--start -ctime 1 -print|wc -l`
+  echo -n ' 'Stale: `find $base/state -name \*--start -ctime +1 -print|wc -l`
+  echo -n ' 'Tot: `ls $base/state/*--end|wc -l`
+  echo -n ' 'Warn: `find $base/state -name \*--warn -print|wc -l`
+  echo ' 'Err: `ls $base/state/*--fail 2>/dev/null|wc -l`
 done
 echo
 
 echo Warnings:
 echo
-grep -h -- '-warn$' $statecache-* | sort
+grep -h -- '--warn$' $statecache-* | sort
 echo
 echo Failures:
 echo
-grep -h -- '-fail$' $statecache-* | sort
+grep -h -- '--fail$' $statecache-* | sort
 echo
 echo Top 5 most stale backups:
 echo
-grep -h -- '-start$' $statecache-* | sort | head -5
+grep -h -- '--start$' $statecache-* | sort | head -5
 echo
 echo Most stale per part:
 echo
 for i in $statecache-*
 do
-    grep -h -- '-start$' $i | sort | head -1
+    grep -h -- '--start$' $i | sort | head -1
 done
 echo
 echo Freshest per part:
 echo
 for i in $statecache-*
 do
-    grep -h -- '-start$' $i | sort | tail -1
+    grep -h -- '--start$' $i | sort | tail -1
 done
 echo
 echo All unfinished backup attempts:
 echo
-grep -h -- '-attempt$' $statecache-* | sort | head -5
+grep -h -- '--attempt$' $statecache-* | sort | head -5