Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,28 @@ sudo sysctl --system

# Operations

## Backup and restore

Before planned maintenance, flush caches and create a snap snapshot:

```bash
sudo diode-node.flush
sudo snap stop diode-node
sudo snap save diode-node
```

When you remove the snap (`sudo snap remove diode-node`), the remove hook automatically archives critical node files (identity and wallet database) to `/var/backups/diode-node/diode_node_backup_<timestamp>.tar.gz`. The `backup-dir` system-files plug must be connected for this path to work; otherwise rely on `snap saved` within 31 days.

To restore from an automatic backup after reinstalling:

```bash
sudo snap install diode-node
sudo snap connect diode-node:backup-dir
sudo snap stop diode-node.service
sudo snap run --shell diode-node -c 'bin/restore_snap_backup /var/backups/diode-node/diode_node_backup_YYYY-MM-DD_HHMMSS.tar.gz'
sudo snap start diode-node.service
```

## See last service restart reason

When running the snap installation then it's a two step process to see the last service restart reason:
Expand Down
55 changes: 55 additions & 0 deletions scripts/restore_snap_backup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/bin/bash
# Restore a diode-node backup created by the snap remove hook.
set -euo pipefail

usage() {
cat <<EOF
Usage: $0 <backup.tar.gz>

Restores node identity and wallet data from an automatic snap-removal backup.
Run after reinstalling diode-node and stopping the service:

sudo snap install diode-node
sudo snap connect diode-node:backup-dir
sudo snap stop diode-node.service
sudo snap run --shell diode-node -c 'bin/restore_snap_backup /var/backups/diode-node/diode_node_backup_YYYY-MM-DD_HHMMSS.tar.gz'
sudo snap start diode-node.service
EOF
}

if [[ $# -ne 1 ]]; then
usage
exit 1
fi

backup_file="$1"

if [[ ! -f "$backup_file" ]]; then
echo "Backup file not found: $backup_file" >&2
exit 1
fi

if [[ -z "${SNAP:-}" || -z "${SNAP_DATA:-}" || -z "${SNAP_USER_DATA:-}" ]]; then
echo "Run inside the diode-node snap (connect backup-dir first):" >&2
echo " sudo snap connect diode-node:backup-dir" >&2
echo " sudo snap run --shell diode-node -c 'bin/restore_snap_backup <backup.tar.gz>'" >&2
exit 1
fi

umask 077
staging_dir=$(mktemp -d)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Setting a restrictive umask is recommended when handling sensitive data like wallet backups to ensure that any temporary files or directories created during the restoration process are not world-readable.

Suggested change
staging_dir=$(mktemp -d)
umask 077
staging_dir=$(mktemp -d)

trap 'rm -rf "$staging_dir"' EXIT

tar -xzf "$backup_file" -C "$staging_dir"

if [[ -d "$staging_dir/snap_user_data" ]]; then
mkdir -p "$SNAP_USER_DATA"
cp -a "$staging_dir/snap_user_data/." "$SNAP_USER_DATA/"
fi

if [[ -d "$staging_dir/snap_data" ]]; then
mkdir -p "$SNAP_DATA"
cp -a "$staging_dir/snap_data/." "$SNAP_DATA/"
fi

echo "Restored backup into $SNAP_DATA and $SNAP_USER_DATA"
101 changes: 101 additions & 0 deletions scripts/snap_backup_on_remove.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/bin/bash
# Automatic wallet backup when the diode-node snap is removed.
# Invoked from snap/hooks/remove; BACKUP_DIR can be overridden for tests.
set -euo pipefail

BACKUP_DIR="${BACKUP_DIR:-/var/backups/diode-node}"
SNAP_NAME="${SNAP_NAME:-diode-node}"
staging_dir=""

log() {
echo "diode-node remove hook: $*" >&2
}

stage_critical_files() {
local snap_data="$1"
local snap_user_data="$2"
local staging_dir="$3"
local has_data=0

if [[ -f "$snap_user_data/node" ]]; then
mkdir -p "$staging_dir/snap_user_data"
cp "$snap_user_data/node" "$staging_dir/snap_user_data/"
has_data=1
fi

if [[ -d "$snap_data" ]]; then
for nodedata in "$snap_data"/nodedata_*; do
if [[ -d "$nodedata" ]]; then
mkdir -p "$staging_dir/snap_data"
cp -a "$nodedata" "$staging_dir/snap_data/"
has_data=1
fi
done
fi

echo "$has_data"
}

write_manifest() {
local staging_dir="$1"
local snap_data="$2"
local snap_user_data="$3"

cat >"$staging_dir/manifest.txt" <<EOF
diode-node automatic backup
created: $(date -u +%Y-%m-%dT%H:%M:%SZ)
snap: ${SNAP_NAME}
snap_data: ${snap_data}
snap_user_data: ${snap_user_data}

Restore with bin/restore_snap_backup after reinstalling the snap
(connect backup-dir: sudo snap connect diode-node:backup-dir).
If this backup is missing, use "snap saved" within 31 days of removal.
EOF
}

main() {
umask 077
local snap_data="${SNAP_DATA:-}"
local snap_user_data="${SNAP_USER_DATA:-}"
Comment thread
dominicletz marked this conversation as resolved.

if [[ -z "$snap_data" || -z "$snap_user_data" ]]; then
log "SNAP_DATA or SNAP_USER_DATA not set, skipping backup"
exit 0
fi

staging_dir=$(mktemp -d)
trap '[[ -n "$staging_dir" ]] && rm -rf "$staging_dir"' EXIT

local has_data
has_data=$(stage_critical_files "$snap_data" "$snap_user_data" "$staging_dir")

if [[ "$has_data" -eq 0 ]]; then
log "no critical node data found, skipping backup"
exit 0
fi

write_manifest "$staging_dir" "$snap_data" "$snap_user_data"

if ! mkdir -p "$BACKUP_DIR" 2>/dev/null; then
log "cannot create backup directory $BACKUP_DIR (is backup-dir plug connected?)"
log "use 'snap saved' within 31 days to recover data via 'snap restore'"
exit 0
fi
Comment on lines +80 to +84
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better security hardening, consider applying chmod 0700 to the backup directory immediately after it is created, rather than at the end of the script. This ensures the directory is secured before the sensitive tarball is written into it.

Suggested change
if ! mkdir -p "$BACKUP_DIR" 2>/dev/null; then
log "cannot create backup directory $BACKUP_DIR (is backup-dir plug connected?)"
log "use 'snap saved' within 31 days to recover data via 'snap restore'"
exit 0
fi
if ! mkdir -p "$BACKUP_DIR" 2>/dev/null; then
log "cannot create backup directory $BACKUP_DIR (is backup-dir plug connected?)"
log "use 'snap saved' within 31 days to recover data via 'snap restore'"
exit 0
fi
chmod 0700 "$BACKUP_DIR" 2>/dev/null || true

chmod 0700 "$BACKUP_DIR" 2>/dev/null || true

local timestamp backup_file
timestamp=$(date -u +%Y-%m-%d_%H%M%S)
backup_file="$BACKUP_DIR/diode_node_backup_${timestamp}.tar.gz"

if ! tar -czf "$backup_file" -C "$staging_dir" .; then
log "failed to write backup archive to $backup_file"
exit 0
fi

chmod 0600 "$backup_file"

log "wallet backup saved to $backup_file"
}

main "$@"
6 changes: 6 additions & 0 deletions snap/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ install:
mkdir -p $(DESTDIR)/meta/hooks
cp snap/configure $(DESTDIR)/meta/hooks/
chmod +x $(DESTDIR)/meta/hooks/configure
cp snap/hooks/remove $(DESTDIR)/meta/hooks/remove
chmod +x $(DESTDIR)/meta/hooks/remove
tar -x --no-same-owner -zf _build/prod/diode_node-`./scripts/version.sh`.tar.gz -C $(DESTDIR)
cp snap/run $(DESTDIR)/bin/
chmod +x $(DESTDIR)/bin/run
cp scripts/snap_backup_on_remove.sh $(DESTDIR)/bin/backup_on_remove
chmod +x $(DESTDIR)/bin/backup_on_remove
cp scripts/restore_snap_backup.sh $(DESTDIR)/bin/restore_snap_backup
chmod +x $(DESTDIR)/bin/restore_snap_backup
3 changes: 3 additions & 0 deletions snap/hooks/remove
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/sh
# Runs before snap data is deleted. See scripts/snap_backup_on_remove.sh.
exec "$SNAP/bin/backup_on_remove"
12 changes: 11 additions & 1 deletion snap/snapcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ lint:
- metadata:
- donation

plugs:
backup-dir:
interface: system-files
write:
- /var/backups/diode-node

hooks:
remove:
plugs: [backup-dir]

parts:
diode-node:
# See 'snapcraft plugins'
Expand Down Expand Up @@ -77,7 +87,7 @@ apps:

shell:
command: bin/run elevated remote
plugs: [network, network-bind]
plugs: [network, network-bind, backup-dir]

rpc:
command: bin/run elevated rpc
Expand Down
120 changes: 120 additions & 0 deletions test/snap_backup_on_remove_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
defmodule SnapBackupOnRemoveTest do
use ExUnit.Case, async: true

@script Path.expand("../scripts/snap_backup_on_remove.sh", __DIR__)

setup do
root = Path.join(System.tmp_dir!(), "snap_backup_test_#{System.unique_integer([:positive])}")
snap_data = Path.join(root, "snap_data")
snap_user_data = Path.join(root, "snap_user_data")
backup_dir = Path.join(root, "backups")
extract_dir = Path.join(root, "extract")

on_exit(fn -> File.rm_rf!(root) end)

%{
root: root,
snap_data: snap_data,
snap_user_data: snap_user_data,
backup_dir: backup_dir,
extract_dir: extract_dir
}
end

defp run_backup(context) do
env = [
{"SNAP_DATA", context.snap_data},
{"SNAP_USER_DATA", context.snap_user_data},
{"SNAP_NAME", "diode-node"},
{"BACKUP_DIR", context.backup_dir}
]

System.cmd("bash", [@script], env: env, stderr_to_stdout: true)
end

test "creates tarball with node identity and nodedata", context do
File.mkdir_p!(Path.join(context.snap_data, "nodedata_prod"))
File.mkdir_p!(context.snap_user_data)

File.write!(Path.join(context.snap_user_data, "node"), "diode_node@host.diode\n")
File.write!(Path.join(context.snap_data, "nodedata_prod/wallet.dat"), "wallet-secrets")

{output, 0} = run_backup(context)
assert output =~ "wallet backup saved"

[backup_name] = File.ls!(context.backup_dir)
assert backup_name =~ ~r/^diode_node_backup_.*\.tar\.gz$/

backup_path = Path.join(context.backup_dir, backup_name)
assert File.stat!(backup_path).mode &&& 0o777 == 0o600
assert File.stat!(context.backup_dir).mode &&& 0o777 == 0o700

File.mkdir_p!(context.extract_dir)

{_, 0} =
System.cmd("tar", ["-xzf", backup_path, "-C", context.extract_dir], stderr_to_stdout: true)

assert File.read!(Path.join(context.extract_dir, "snap_user_data/node")) ==
"diode_node@host.diode\n"

assert File.read!(Path.join(context.extract_dir, "snap_data/nodedata_prod/wallet.dat")) ==
"wallet-secrets"

manifest = File.read!(Path.join(context.extract_dir, "manifest.txt"))
assert manifest =~ "diode-node automatic backup"
assert manifest =~ context.snap_data
end

test "does not backup erl_inetrc even when present", context do
File.mkdir_p!(Path.join(context.snap_data, "nodedata_prod"))
File.mkdir_p!(context.snap_user_data)

File.write!(Path.join(context.snap_user_data, "node"), "diode_node@host.diode\n")

File.write!(
Path.join(context.snap_user_data, "erl_inetrc"),
"{host, {127,0,0,1}, [\"host.diode\", \"diode_node@host.diode\"]}.\n"
)

File.write!(Path.join(context.snap_data, "nodedata_prod/wallet.dat"), "wallet-secrets")

{output, 0} = run_backup(context)
assert output =~ "wallet backup saved"

[backup_name] = File.ls!(context.backup_dir)
backup_path = Path.join(context.backup_dir, backup_name)
File.mkdir_p!(context.extract_dir)

{_, 0} =
System.cmd("tar", ["-xzf", backup_path, "-C", context.extract_dir], stderr_to_stdout: true)

refute File.exists?(Path.join(context.extract_dir, "snap_user_data/erl_inetrc"))
end

test "skips backup when no critical data exists", context do
File.mkdir_p!(context.snap_data)
File.mkdir_p!(context.snap_user_data)

{output, 0} = run_backup(context)
assert output =~ "no critical node data found"
refute File.exists?(context.backup_dir)
end

test "does not fail removal when backup directory is not writable", context do
File.mkdir_p!(context.snap_user_data)
File.write!(Path.join(context.snap_user_data, "node"), "diode_node@test.diode\n")

env = [
{"SNAP_DATA", context.snap_data},
{"SNAP_USER_DATA", context.snap_user_data},
{"SNAP_NAME", "diode-node"},
{"BACKUP_DIR", "/root/diode-node-backup-should-not-exist"}
]

{output, 0} =
System.cmd("bash", [@script], env: env, stderr_to_stdout: true)

assert output =~ "cannot create backup directory"
assert output =~ "snap saved"
end
end
Loading