Compare commits
6 commits
3449bae491
...
master
Author | SHA1 | Date | |
---|---|---|---|
a09cc157ec | |||
6f457fb7dd | |||
9fe2f05491 | |||
b9da9c74c4 | |||
17ace3fb33 | |||
bb060e5eec |
7 changed files with 185 additions and 50 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
shellcheck.log
|
14
Makefile
Normal file
14
Makefile
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
all: shellcheck test
|
||||||
|
|
||||||
|
shellcheck: shellcheck.log
|
||||||
|
|
||||||
|
test:
|
||||||
|
./test/test.sh
|
||||||
|
|
||||||
|
install: dvbackup shellcheck test
|
||||||
|
install -Dm 0755 --owner=root --group=root $< /usr/local/bin/
|
||||||
|
|
||||||
|
shellcheck.log: dvbackup
|
||||||
|
shellcheck $< | tee $@
|
||||||
|
|
||||||
|
.PHONY: all shellcheck test install
|
39
README.md
Normal file
39
README.md
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# Docker Volume Backup
|
||||||
|
|
||||||
|
A simple script which creates tarballs from docker volumes and restores
|
||||||
|
them.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Backup a single volume
|
||||||
|
|
||||||
|
`./dvbackup.sh backup <volume_name> <path/to/tarball.tar>`
|
||||||
|
|
||||||
|
Creates a tarball from the contents of the volume.
|
||||||
|
|
||||||
|
### Restore a single volume
|
||||||
|
|
||||||
|
`./dvbackup.sh restore <path/to/tarball.tar> <volume_name>`
|
||||||
|
|
||||||
|
Restores the tarball into the volume. *This is destructive*, all old
|
||||||
|
contents will be removed from the volume. The volume must already exist.
|
||||||
|
|
||||||
|
### Backup all named volumes
|
||||||
|
|
||||||
|
`./dvbackup.sh backup_all`
|
||||||
|
|
||||||
|
Creates backups from all volumes which do not only contain the character
|
||||||
|
set `[0-9a-f]`.
|
||||||
|
|
||||||
|
This command will output a `<volume_name>.tar` file for every found
|
||||||
|
volume in the current working directory.
|
||||||
|
|
||||||
|
### Restore volumes
|
||||||
|
|
||||||
|
`./dvbackup.sh restore_all [volume.tar...]`
|
||||||
|
|
||||||
|
Restores all given tarballs into their respective volumes. The volumes
|
||||||
|
must already exist and the operation is *destructive* in the same way
|
||||||
|
as the `restore` operation. The default behaviour is to ask before
|
||||||
|
continuing, this can be overridden by setting `DVB_I_KNOW_WHAT_I_DO=y`.
|
||||||
|
Volumes can be created implicitly by setting `DVB_CREATE_VOLUME=y`.
|
81
dvbackup
Executable file
81
dvbackup
Executable file
|
@ -0,0 +1,81 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
if [ -z "$DOCKER" ]; then
|
||||||
|
DOCKER=docker
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
echo_and_run() {
|
||||||
|
echo "$@"
|
||||||
|
"$@"
|
||||||
|
return $?
|
||||||
|
}
|
||||||
|
|
||||||
|
backup() {
|
||||||
|
volume="$1"
|
||||||
|
target="$(realpath "$2")"
|
||||||
|
target_dir="$(dirname "$target")"
|
||||||
|
target_name="$(basename "$target")"
|
||||||
|
|
||||||
|
test -z "$("$DOCKER" volume ls | grep "$volume")" && { echo "Error: No such volume, aborting" >&2; exit 1; }
|
||||||
|
test -z "$target_dir" && { echo "Error: No base folder found for target=$target" >&2; exit 2; }
|
||||||
|
test -z "$target_name" && { echo "Error: No target name found for target=$target" >&2; exit 3; }
|
||||||
|
|
||||||
|
echo_and_run "$DOCKER" run --rm \
|
||||||
|
--mount="type=volume,source=$volume,destination=/data,ro=true" \
|
||||||
|
--mount="type=bind,source=$target_dir,destination=/data2" \
|
||||||
|
busybox /bin/sh -c \
|
||||||
|
"cd /data/ && tar cf '/data2/$target_name' ./* && chown $(id -u):$(id -g) /data2/$target_name"
|
||||||
|
}
|
||||||
|
|
||||||
|
restore() {
|
||||||
|
target="$(realpath "$1")"
|
||||||
|
target_dir="$(dirname "$target")"
|
||||||
|
target_name="$(basename "$target")"
|
||||||
|
volume="$2"
|
||||||
|
|
||||||
|
test -z "$("$DOCKER" volume ls | grep "$volume")" && { echo "Error: No such volume, aborting" >&2; exit 1; }
|
||||||
|
test -z "$target_dir" && { echo "Error: No base folder found for target=$target" >&2; exit 2; }
|
||||||
|
test -z "$target_name" && { echo "Error: No target name found for target=$target" >&2; exit 3; }
|
||||||
|
|
||||||
|
echo_and_run "$DOCKER" run --rm \
|
||||||
|
--mount="type=volume,source=$volume,destination=/data" \
|
||||||
|
--mount="type=bind,source=$target_dir,destination=/data2" \
|
||||||
|
busybox /bin/sh -c \
|
||||||
|
"cd /data/ && rm -rf ./* && tar xf '/data2/$target_name'"
|
||||||
|
}
|
||||||
|
|
||||||
|
backup_all() {
|
||||||
|
"$DOCKER" volume ls \
|
||||||
|
| awk '!/^[a-z]+ +[0-9a-f]+$/ && (NR>1) {print $2}' \
|
||||||
|
| while read -r volume_name; do
|
||||||
|
echo "$volume_name -> $volume_name.tar"
|
||||||
|
backup "$volume_name" "$volume_name.tar"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_all() {
|
||||||
|
if [ -z "$DVB_I_KNOW_WHAT_I_DO" ]; then
|
||||||
|
printf "The following operation will delete all data in the volumes to be restored, are you sure [y/N]? "
|
||||||
|
read -r DVB_I_KNOW_WHAT_I_DO
|
||||||
|
fi
|
||||||
|
if echo "$DVB_I_KNOW_WHAT_I_DO" | grep -Eviq 't|true|1|y|yes'; then
|
||||||
|
echo aborting
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
for tarball in "$@"; do
|
||||||
|
volume_name="${tarball%.tar}"
|
||||||
|
if ! "$DOCKER" volume inspect "$volume_name" 1>&2 2>/dev/null; then
|
||||||
|
if echo "$DVB_CREATE_VOLUME" | grep -Eiq 't|true|1|y|yes'; then
|
||||||
|
"$DOCKER" volume create "$volume_name"
|
||||||
|
else
|
||||||
|
echo "Error: no such volume $volume_name" >&2
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo "$tarball -> $volume_name"
|
||||||
|
restore "$tarball" "$volume_name"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
"$@"
|
50
dvbackup.sh
50
dvbackup.sh
|
@ -1,50 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
backup() {
|
|
||||||
local volume
|
|
||||||
local target
|
|
||||||
local target_dir
|
|
||||||
local target_name
|
|
||||||
volume="$1"
|
|
||||||
target="$(realpath "$2")"
|
|
||||||
target_dir="$(dirname "$target")"
|
|
||||||
target_name="$(basename "$target")"
|
|
||||||
|
|
||||||
test -z "$(docker volume ls | grep "$volume")" && { echo "Error: No such volume, aborting" >&2; exit 1; }
|
|
||||||
test -z "$target_dir" && { echo "Error: No base folder found for target=$target" >&2; exit 2; }
|
|
||||||
test -z "$target_name" && { echo "Error: No target name found for target=$target" >&2; exit 3; }
|
|
||||||
|
|
||||||
set -x
|
|
||||||
docker run --rm --mount="type=volume,source=$volume,destination=/data,ro=true" --mount="type=bind,source=$target_dir,destination=/data2" busybox /bin/sh -c "tar cvf '/data2/$target_name' /data && chown $(id -u):$(id -g) /data2/$target_name"
|
|
||||||
set +x
|
|
||||||
}
|
|
||||||
|
|
||||||
restore() {
|
|
||||||
local volume
|
|
||||||
local target
|
|
||||||
local target_dir
|
|
||||||
local target_name
|
|
||||||
target="$(realpath "$1")"
|
|
||||||
target_dir="$(dirname "$target")"
|
|
||||||
target_name="$(basename "$target")"
|
|
||||||
volume="$2"
|
|
||||||
|
|
||||||
test -z "$(docker volume ls | grep "$volume")" && { echo "Error: No such volume, aborting" >&2; exit 1; }
|
|
||||||
test -z "$target_dir" && { echo "Error: No base folder found for target=$target" >&2; exit 2; }
|
|
||||||
test -z "$target_name" && { echo "Error: No target name found for target=$target" >&2; exit 3; }
|
|
||||||
|
|
||||||
set -x
|
|
||||||
docker run --rm --mount="type=volume,source=$volume,destination=/data" --mount="type=bind,source=$target_dir,destination=/data2" busybox /bin/sh -c "rm -rf /data/* && cd / && tar xvf '/data2/$target_name'"
|
|
||||||
set +x
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
local volumes
|
|
||||||
volumes="$(docker volume ls | tail -n+2 | egrep -v '[a-z]+\s+[0-9a-f]+$' | egrep -o '\s.+$' | sed 's/\s//g')"
|
|
||||||
while read -r line; do
|
|
||||||
echo "$line -> $line.tar"
|
|
||||||
backup "$line" "$line.tar"
|
|
||||||
done <<< "$volumes"
|
|
||||||
}
|
|
||||||
|
|
||||||
"$@"
|
|
26
test/mock-docker.sh
Executable file
26
test/mock-docker.sh
Executable file
|
@ -0,0 +1,26 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
volume() {
|
||||||
|
if [ "$1" = 'ls' ]; then
|
||||||
|
echo "DRIVER VOLUME NAME"
|
||||||
|
echo "local 00c674e3f3c1587d88c2ebf2f91da5843b9dddb3e8df272898bdfd4e596aef79"
|
||||||
|
echo "local $DOCKER_MOCK_VOLUME"
|
||||||
|
return 0
|
||||||
|
elif [ "$1" = 'inspect' ]; then
|
||||||
|
if [ "$2" = "$DOCKER_MOCK_VOLUME" ]; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
docker volume "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if [ "$1" = "volume" ]; then
|
||||||
|
"$@"
|
||||||
|
else
|
||||||
|
docker "$@"
|
||||||
|
fi
|
||||||
|
|
24
test/test.sh
Executable file
24
test/test.sh
Executable file
|
@ -0,0 +1,24 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
|
||||||
|
cd "$(dirname "$0")" || exit 1
|
||||||
|
|
||||||
|
VOLUME_NAME="$(dd if=/dev/random bs=10 count=1 | base32)"
|
||||||
|
docker volume create "$VOLUME_NAME"
|
||||||
|
|
||||||
|
# shellcheck disable=SC2064
|
||||||
|
trap "docker volume rm $VOLUME_NAME && rm -f $VOLUME_NAME.tar" EXIT
|
||||||
|
|
||||||
|
docker run --rm --volume="$VOLUME_NAME:/data" alpine \
|
||||||
|
sh -c 'echo "test" > /data/a.txt'
|
||||||
|
DOCKER=./mock-docker.sh DOCKER_MOCK_VOLUME="$VOLUME_NAME" \
|
||||||
|
../dvbackup backup_all
|
||||||
|
stat "$VOLUME_NAME.tar" || exit 1
|
||||||
|
docker run --rm --volume="$VOLUME_NAME:/data" alpine \
|
||||||
|
sh -c 'rm /data/a.txt' || exit 1
|
||||||
|
DOCKER=./mock-docker.sh DOCKER_MOCK_VOLUME="$VOLUME_NAME" DVB_I_KNOW_WHAT_I_DO=y \
|
||||||
|
../dvbackup restore_all "$VOLUME_NAME.tar" || exit 1
|
||||||
|
docker run --rm --volume="$VOLUME_NAME:/data" alpine \
|
||||||
|
sh -c 'stat /data/a.txt' || exit 1
|
||||||
|
|
||||||
|
exit 0
|
Loading…
Reference in a new issue