Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add remote borg backup support #4804

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d918dc5
Refactor: Replace literal with $MOUNT_DIR
timdiels Apr 13, 2024
675d6b9
Add backend support for remote repos
timdiels Apr 13, 2024
d013683
Mostly add a borg_remote_repo setting to frontend
timdiels Apr 14, 2024
952a82a
Create borg ssh key if missing
timdiels Apr 15, 2024
ca43e43
Do not download config from remote
timdiels Apr 15, 2024
de4fa35
fix disaster restore
timdiels Apr 22, 2024
f49eddb
Do not require local backup dir when using a remote
timdiels Apr 25, 2024
8afea40
Restore with borg extract, much faster than borg mount
timdiels Jun 9, 2024
35b2cee
Show the borg public key in backup info
timdiels Jun 9, 2024
4f62817
Merge remote-tracking branch 'upstream/main' into feature/remote-borg…
timdiels Jun 9, 2024
7e96d1d
fix shellcheck
timdiels Jun 9, 2024
1a26792
Revert to restoring with borg mount and rsync for local repos
timdiels Jul 9, 2024
16c1b01
Include remote borg backup in the readme
timdiels Jul 9, 2024
eb69252
adjust the cloning temporarily
szaimen Jul 11, 2024
de50186
Do not try to set additional_free_space on a remote repo
timdiels Jul 21, 2024
a25ebd7
Merge remote-tracking branch 'upstream/main' into feature/remote-borg…
timdiels Aug 20, 2024
6dd8653
Merge remote-tracking branch 'upstream/main' into feature/remote-borg…
timdiels Oct 27, 2024
5078c4f
Add comment to keep exclude patterns in sync
timdiels Oct 27, 2024
dc0076f
Remove whitespace before ,
timdiels Oct 27, 2024
98e0ad9
Ignore borg info output unless it fails
timdiels Oct 27, 2024
cc4e85e
Improve last
timdiels Oct 27, 2024
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
4 changes: 3 additions & 1 deletion Containers/borgbackup/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ RUN set -ex; \
rsync \
fuse \
py3-llfuse \
jq
jq \
openssh-client

VOLUME /root

COPY --chmod=770 *.sh /
COPY borg_excludes /

ENTRYPOINT ["/start.sh"]
# hadolint ignore=DL3002
Expand Down
243 changes: 156 additions & 87 deletions Containers/borgbackup/backupscript.sh

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions Containers/borgbackup/borg_excludes
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
nextcloud_aio_volumes/nextcloud_aio_apache/caddy/
nextcloud_aio_volumes/nextcloud_aio_mastercontainer/caddy/
nextcloud_aio_volumes/nextcloud_aio_nextcloud/data/nextcloud.log*
nextcloud_aio_volumes/nextcloud_aio_nextcloud/data/audit.log
nextcloud_aio_volumes/nextcloud_aio_mastercontainer/certs/
nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/daily_backup_running
nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/session_date_file
nextcloud_aio_volumes/nextcloud_aio_mastercontainer/session/
nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/id_borg*
szaimen marked this conversation as resolved.
Show resolved Hide resolved
18 changes: 15 additions & 3 deletions Containers/borgbackup/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Variables
export MOUNT_DIR="/mnt/borgbackup"
export BORG_BACKUP_DIRECTORY="$MOUNT_DIR/borg"
export BORG_BACKUP_DIRECTORY="$MOUNT_DIR/borg" # necessary even when remote to store the aio-lockfile

# Validate BORG_PASSWORD
if [ -z "$BORG_PASSWORD" ] && [ -z "$BACKUP_RESTORE_PASSWORD" ]; then
Expand All @@ -18,6 +18,18 @@ else
fi
export BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes
export BORG_RELOCATED_REPO_ACCESS_IS_OK=yes
if [ -n "$BORG_REMOTE_REPO" ]; then
export BORG_REPO="$BORG_REMOTE_REPO"

# Location to create the borg ssh pub/priv key
export BORGBACKUP_KEY="/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/id_borg"

# Accept any host key the first time connecting to the remote. Strictly speaking should be provided by user but you'd
# have to be very unlucky to get MitM'ed on your first connection.
export BORG_RSH="ssh -o StrictHostKeyChecking=accept-new -i $BORGBACKUP_KEY"
else
export BORG_REPO="$BORG_BACKUP_DIRECTORY"
fi

# Validate BORG_MODE
if [ "$BORG_MODE" != backup ] && [ "$BORG_MODE" != restore ] && [ "$BORG_MODE" != check ] && [ "$BORG_MODE" != "check-repair" ] && [ "$BORG_MODE" != test ]; then
Expand All @@ -36,8 +48,8 @@ fi
rm -f "/nextcloud_aio_volumes/nextcloud_aio_database_dump/backup-is-running"

# Get a list of all available borg archives
if borg list "$BORG_BACKUP_DIRECTORY" &>/dev/null; then
borg list "$BORG_BACKUP_DIRECTORY" | grep "nextcloud-aio" | awk -F " " '{print $1","$3,$4}' > "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/backup_archives.list"
if borg list &>/dev/null; then
borg list | grep "nextcloud-aio" | awk -F " " '{print $1","$3,$4}' > "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/backup_archives.list"
else
echo "" > "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/backup_archives.list"
fi
Expand Down
1 change: 1 addition & 0 deletions php/containers.json
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@
"image": "nextcloud/aio-borgbackup",
"init": true,
"environment": [
"BORG_REMOTE_REPO=%BORGBACKUP_REMOTE_REPO%",
"BORG_PASSWORD=%BORGBACKUP_PASSWORD%",
"BORG_MODE=%BORGBACKUP_MODE%",
"SELECTED_RESTORE_TIME=%SELECTED_RESTORE_TIME%",
Expand Down
2 changes: 2 additions & 0 deletions php/public/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@
'domain' => $configurationManager->GetDomain(),
'apache_port' => $configurationManager->GetApachePort(),
'borg_backup_host_location' => $configurationManager->GetBorgBackupHostLocation(),
'borg_remote_repo' => $configurationManager->GetBorgRemoteRepo(),
'borg_public_key' => $configurationManager->GetBorgPublicKey(),
'nextcloud_password' => $configurationManager->GetAndGenerateSecret('NEXTCLOUD_PASSWORD'),
'containers' => (new \AIO\ContainerDefinitionFetcher($container->get(\AIO\Data\ConfigurationManager::class), $container))->FetchDefinition(),
'borgbackup_password' => $configurationManager->GetAndGenerateSecret('BORGBACKUP_PASSWORD'),
Expand Down
14 changes: 8 additions & 6 deletions php/src/Controller/ConfigurationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,17 @@ public function SetConfig(Request $request, Response $response, array $args) : R
$this->configurationManager->ChangeMasterPassword($currentMasterPassword, $newMasterPassword);
}

if (isset($request->getParsedBody()['borg_backup_host_location'])) {
if (isset($request->getParsedBody()['borg_backup_host_location']) || isset($request->getParsedBody()['borg_remote_repo'])) {
$location = $request->getParsedBody()['borg_backup_host_location'] ?? '';
$this->configurationManager->SetBorgBackupHostLocation($location);
$borgRemoteRepo = $request->getParsedBody()['borg_remote_repo'] ?? '';
$this->configurationManager->SetBorgLocationVars($location, $borgRemoteRepo);
}

if (isset($request->getParsedBody()['borg_restore_host_location']) || isset($request->getParsedBody()['borg_restore_password'])) {
if (isset($request->getParsedBody()['borg_restore_host_location']) || isset($request->getParsedBody()['borg_restore_remote_repo']) || isset($request->getParsedBody()['borg_restore_password'])) {
$restoreLocation = $request->getParsedBody()['borg_restore_host_location'] ?? '';
$borgRemoteRepo = $request->getParsedBody()['borg_restore_remote_repo'] ?? '';
$borgPassword = $request->getParsedBody()['borg_restore_password'] ?? '';
$this->configurationManager->SetBorgRestoreHostLocationAndPassword($restoreLocation, $borgPassword);
$this->configurationManager->SetBorgRestoreLocationVarsAndPassword($restoreLocation, $borgRemoteRepo, $borgPassword);
}

if (isset($request->getParsedBody()['daily_backup_time'])) {
Expand Down Expand Up @@ -131,8 +133,8 @@ public function SetConfig(Request $request, Response $response, array $args) : R
$this->configurationManager->SetCollaboraDictionaries($collaboraDictionaries);
}

if (isset($request->getParsedBody()['delete_borg_backup_host_location'])) {
$this->configurationManager->DeleteBorgBackupHostLocation();
if (isset($request->getParsedBody()['delete_borg_backup_location_vars'])) {
$this->configurationManager->DeleteBorgBackupLocationVars();
}

return $response->withStatus(201)->withHeader('Location', '/');
Expand Down
89 changes: 60 additions & 29 deletions php/src/Data/ConfigurationManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -424,55 +424,69 @@ public function GetAIOURL() : string {
/**
* @throws InvalidSettingConfigurationException
*/
public function SetBorgBackupHostLocation(string $location) : void {
$isValidPath = false;
if (str_starts_with($location, '/') && !str_ends_with($location, '/')) {
$isValidPath = true;
} elseif ($location === 'nextcloud_aio_backupdir') {
$isValidPath = true;
}

if (!$isValidPath) {
throw new InvalidSettingConfigurationException("The path must start with '/', and must not end with '/'!");
}

public function SetBorgLocationVars(string $location, string $repo) : void {
$this->ValidateBorgLocationVars($location, $repo);

$config = $this->GetConfig();
$config['borg_backup_host_location'] = $location;
$config['borg_remote_repo'] = $repo;
$this->WriteConfig($config);
}

public function DeleteBorgBackupHostLocation() : void {
private function ValidateBorgLocationVars(string $location, string $repo) : void {
if ($location === '' && $repo === '') {
throw new InvalidSettingConfigurationException("Please enter a path or a remote repo url!");
} elseif ($location !== '' && $repo !== '') {
throw new InvalidSettingConfigurationException("Location and remote repo url are mutually exclusive!");
}

if ($location !== '') {
$isValidPath = false;
if (str_starts_with($location, '/') && !str_ends_with($location, '/')) {
$isValidPath = true;
} elseif ($location === 'nextcloud_aio_backupdir') {
$isValidPath = true;
}

if (!$isValidPath) {
throw new InvalidSettingConfigurationException("The path must start with '/', and must not end with '/'!");
}
} else {
$this->ValidateBorgRemoteRepo($repo);
}
}

private function ValidateBorgRemoteRepo(string $repo) : void {
$commonMsg = "For valid urls, see the remote examples at https://borgbackup.readthedocs.io/en/stable/usage/general.html#repository-urls";
if ($repo === "") {
// Ok, remote repo is optional
} elseif (!str_contains($repo, "@")) {
throw new InvalidSettingConfigurationException("The remote repo must contain '@'. $commonMsg");
} elseif (!str_contains($repo, ":")) {
throw new InvalidSettingConfigurationException("The remote repo must contain ':'. $commonMsg");
}
}

public function DeleteBorgBackupLocationVars() : void {
$config = $this->GetConfig();
$config['borg_backup_host_location'] = '';
$config['borg_remote_repo'] = '';
$this->WriteConfig($config);
}

/**
/**
* @throws InvalidSettingConfigurationException
*/
public function SetBorgRestoreHostLocationAndPassword(string $location, string $password) : void {
if ($location === '') {
throw new InvalidSettingConfigurationException("Please enter a path!");
}

$isValidPath = false;
if (str_starts_with($location, '/') && !str_ends_with($location, '/')) {
$isValidPath = true;
} elseif ($location === 'nextcloud_aio_backupdir') {
$isValidPath = true;
}

if (!$isValidPath) {
throw new InvalidSettingConfigurationException("The path must start with '/', and must not end with '/'!");
szaimen marked this conversation as resolved.
Show resolved Hide resolved
}
public function SetBorgRestoreLocationVarsAndPassword(string $location, string $repo, string $password) : void {
$this->ValidateBorgLocationVars($location, $repo);

if ($password === '') {
throw new InvalidSettingConfigurationException("Please enter the password!");
}

$config = $this->GetConfig();
$config['borg_backup_host_location'] = $location;
$config['borg_remote_repo'] = $repo;
$config['borg_restore_password'] = $password;
$config['instance_restore_attempt'] = 1;
$this->WriteConfig($config);
Expand Down Expand Up @@ -567,6 +581,23 @@ public function GetBorgBackupHostLocation() : string {
return $config['borg_backup_host_location'];
}

public function GetBorgRemoteRepo() : string {
$config = $this->GetConfig();
if(!isset($config['borg_remote_repo'])) {
$config['borg_remote_repo'] = '';
}

return $config['borg_remote_repo'];
}

public function GetBorgPublicKey() : string {
if (!file_exists(DataConst::GetBackupPublicKey())) {
return "";
}

return trim(file_get_contents(DataConst::GetBackupPublicKey()));
}

public function GetBorgRestorePassword() : string {
$config = $this->GetConfig();
if(!isset($config['borg_restore_password'])) {
Expand Down
4 changes: 4 additions & 0 deletions php/src/Data/DataConst.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public static function GetConfigFile() : string {
return self::GetDataDirectory() . '/configuration.json';
}

public static function GetBackupPublicKey() : string {
return self::GetDataDirectory() . '/id_borg.pub';
}

public static function GetBackupSecretFile() : string {
return self::GetDataDirectory() . '/backupsecret';
}
Expand Down
2 changes: 2 additions & 0 deletions php/src/Docker/DockerActionManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ public function CreateContainer(Container $container) : void {
$replacements[1] = $this->configurationManager->GetBaseDN();
} elseif ($out[1] === 'AIO_TOKEN') {
$replacements[1] = $this->configurationManager->GetToken();
} elseif ($out[1] === 'BORGBACKUP_REMOTE_REPO') {
$replacements[1] = $this->configurationManager->GetBorgRemoteRepo();
} elseif ($out[1] === 'BORGBACKUP_MODE') {
$replacements[1] = $this->configurationManager->GetBackupMode();
} elseif ($out[1] === 'AIO_URL') {
Expand Down
Loading