Support for Borg repository initialization via borgmatic --init command-line flag (#110).
This commit is contained in:
parent
2045edc11b
commit
cc9dbb1def
13 changed files with 440 additions and 217 deletions
6
NEWS
6
NEWS
|
@ -1,7 +1,9 @@
|
||||||
1.2.12.dev0
|
1.2.12
|
||||||
|
* #110: Support for Borg repository initialization via borgmatic --init command-line flag.
|
||||||
* #111: Update Borg create --filter values so a dry run lists files to back up.
|
* #111: Update Borg create --filter values so a dry run lists files to back up.
|
||||||
* #113: Update README with link to a new/forked Docker image.
|
* #113: Update README with link to a new/forked Docker image.
|
||||||
* Error when deprecated --excludes command-line option is used.
|
* Prevent deprecated --excludes command-line option from being used.
|
||||||
|
* Refactor README a bit to flow better for first-time users.
|
||||||
|
|
||||||
1.2.11
|
1.2.11
|
||||||
* #108: Support for Borg create --progress via borgmatic command-line flag.
|
* #108: Support for Borg create --progress via borgmatic command-line flag.
|
||||||
|
|
287
README.md
287
README.md
|
@ -58,28 +58,9 @@ href="https://asciinema.org/a/203761" target="_blank">screencast</a>.
|
||||||
|
|
||||||
To get up and running, first [install
|
To get up and running, first [install
|
||||||
Borg](https://borgbackup.readthedocs.io/en/latest/installation.html), at
|
Borg](https://borgbackup.readthedocs.io/en/latest/installation.html), at
|
||||||
least version 1.1. Then, follow the [Borg Quick
|
least version 1.1.
|
||||||
Start](https://borgbackup.readthedocs.org/en/latest/quickstart.html) to create
|
|
||||||
a repository on a local or remote host.
|
|
||||||
|
|
||||||
Note that if you plan to run borgmatic on a schedule with cron, and you
|
Then, run the following command to download and install borgmatic:
|
||||||
encrypt your Borg repository with a passphrase instead of a key file, you'll
|
|
||||||
either need to set the borgmatic `encryption_passphrase` configuration
|
|
||||||
variable or set the `BORG_PASSPHRASE` environment variable. See the
|
|
||||||
[repository encryption
|
|
||||||
section](https://borgbackup.readthedocs.io/en/latest/quickstart.html#repository-encryption)
|
|
||||||
of the Quick Start for more info.
|
|
||||||
|
|
||||||
Alternatively, the passphrase can be specified programatically by setting
|
|
||||||
either the borgmatic `encryption_passcommand` configuration variable or the
|
|
||||||
`BORG_PASSCOMMAND` environment variable. See the [Borg Security
|
|
||||||
FAQ](http://borgbackup.readthedocs.io/en/stable/faq.html#how-can-i-specify-the-encryption-passphrase-programmatically)
|
|
||||||
for more info.
|
|
||||||
|
|
||||||
If the repository is on a remote host, make sure that your local root user has
|
|
||||||
key-based ssh access to the desired user account on the remote host.
|
|
||||||
|
|
||||||
To install borgmatic, run the following command to download and install it:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo pip3 install --upgrade borgmatic
|
sudo pip3 install --upgrade borgmatic
|
||||||
|
@ -88,6 +69,7 @@ sudo pip3 install --upgrade borgmatic
|
||||||
Note that your pip binary may have a different name than "pip3". Make sure
|
Note that your pip binary may have a different name than "pip3". Make sure
|
||||||
you're using Python 3, as borgmatic does not support Python 2.
|
you're using Python 3, as borgmatic does not support Python 2.
|
||||||
|
|
||||||
|
|
||||||
### Other ways to install
|
### Other ways to install
|
||||||
|
|
||||||
* [A borgmatic Docker image](https://hub.docker.com/r/monachus/borgmatic/) based
|
* [A borgmatic Docker image](https://hub.docker.com/r/monachus/borgmatic/) based
|
||||||
|
@ -101,6 +83,7 @@ you're using Python 3, as borgmatic does not support Python 2.
|
||||||
* [A borgmatic package for OpenBSD](http://ports.su/sysutils/borgmatic).
|
* [A borgmatic package for OpenBSD](http://ports.su/sysutils/borgmatic).
|
||||||
<br><br>
|
<br><br>
|
||||||
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
After you install borgmatic, generate a sample configuration file:
|
After you install borgmatic, generate a sample configuration file:
|
||||||
|
@ -124,6 +107,161 @@ borgmatic has added new options since you originally created your
|
||||||
configuration file.
|
configuration file.
|
||||||
|
|
||||||
|
|
||||||
|
### Encryption
|
||||||
|
|
||||||
|
Note that if you plan to run borgmatic on a schedule with cron, and you
|
||||||
|
encrypt your Borg repository with a passphrase instead of a key file, you'll
|
||||||
|
either need to set the borgmatic `encryption_passphrase` configuration
|
||||||
|
variable or set the `BORG_PASSPHRASE` environment variable. See the
|
||||||
|
[repository encryption
|
||||||
|
section](https://borgbackup.readthedocs.io/en/latest/quickstart.html#repository-encryption)
|
||||||
|
of the Quick Start for more info.
|
||||||
|
|
||||||
|
Alternatively, the passphrase can be specified programatically by setting
|
||||||
|
either the borgmatic `encryption_passcommand` configuration variable or the
|
||||||
|
`BORG_PASSCOMMAND` environment variable. See the [Borg Security
|
||||||
|
FAQ](http://borgbackup.readthedocs.io/en/stable/faq.html#how-can-i-specify-the-encryption-passphrase-programmatically)
|
||||||
|
for more info.
|
||||||
|
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Initialization
|
||||||
|
|
||||||
|
Before you can create backups with borgmatic, you first need to initialize a
|
||||||
|
Borg repository so you have a destination for your backup archives. (But skip
|
||||||
|
this step if you already have a Borg repository.) To create a repository, run
|
||||||
|
a command like the following:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
borgmatic --init --encryption repokey
|
||||||
|
```
|
||||||
|
|
||||||
|
This uses the borgmatic configuration file you created above to determine
|
||||||
|
which local or remote repository to create, and encrypts it with the
|
||||||
|
encryption passphrase specified there if one is provided. Read about [Borg
|
||||||
|
encryption
|
||||||
|
modes](https://borgbackup.readthedocs.io/en/latest/usage/init.html#encryption-modes)
|
||||||
|
for the menu of available encryption modes.
|
||||||
|
|
||||||
|
Also, optionally check out the [Borg Quick
|
||||||
|
Start](https://borgbackup.readthedocs.org/en/latest/quickstart.html) for more
|
||||||
|
background about repository initialization.
|
||||||
|
|
||||||
|
If the repository is on a remote host, make sure that your local user has
|
||||||
|
key-based SSH access to the desired user account on the remote host.
|
||||||
|
|
||||||
|
|
||||||
|
### Backups
|
||||||
|
|
||||||
|
You can run borgmatic and start a backup simply by invoking it without
|
||||||
|
arguments:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
borgmatic
|
||||||
|
```
|
||||||
|
|
||||||
|
This will also prune any old backups as per the configured retention policy,
|
||||||
|
and check backups for consistency problems due to things like file damage.
|
||||||
|
|
||||||
|
If you'd like to see the available command-line arguments, view the help:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
borgmatic --help
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that borgmatic prunes archives *before* creating an archive, so as to
|
||||||
|
free up space for archiving. This means that when a borgmatic run finishes,
|
||||||
|
there may still be prune-able archives. Not to worry, as they will get cleaned
|
||||||
|
up at the start of the next run.
|
||||||
|
|
||||||
|
|
||||||
|
### Verbosity
|
||||||
|
|
||||||
|
By default, the backup will proceed silently except in the case of errors. But
|
||||||
|
if you'd like to to get additional information about the progress of the
|
||||||
|
backup as it proceeds, use the verbosity option:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
borgmatic --verbosity 1
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, for even more progress spew:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
borgmatic --verbosity 2
|
||||||
|
```
|
||||||
|
|
||||||
|
### À la carte
|
||||||
|
|
||||||
|
If you want to run borgmatic with only pruning, creating, or checking enabled,
|
||||||
|
the following optional flags are available:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
borgmatic --prune
|
||||||
|
borgmatic --create
|
||||||
|
borgmatic --check
|
||||||
|
```
|
||||||
|
|
||||||
|
You can run with only one of these flags provided, or you can mix and match
|
||||||
|
any number of them. This supports use cases like running consistency checks
|
||||||
|
from a different cron job with a different frequency, or running pruning with
|
||||||
|
a different verbosity level.
|
||||||
|
|
||||||
|
Additionally, borgmatic provides convenient flags for Borg's
|
||||||
|
[list](https://borgbackup.readthedocs.io/en/stable/usage/list.html) and
|
||||||
|
[info](https://borgbackup.readthedocs.io/en/stable/usage/info.html)
|
||||||
|
functionality:
|
||||||
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
borgmatic --list
|
||||||
|
borgmatic --info
|
||||||
|
```
|
||||||
|
|
||||||
|
You can include an optional `--json` flag with `--create`, `--list`, or
|
||||||
|
`--info` to get the output formatted as JSON.
|
||||||
|
|
||||||
|
|
||||||
|
## Autopilot
|
||||||
|
|
||||||
|
If you want to run borgmatic automatically, say once a day, the you can
|
||||||
|
configure a job runner to invoke it periodically.
|
||||||
|
|
||||||
|
### cron
|
||||||
|
|
||||||
|
If you're using cron, download the [sample cron
|
||||||
|
file](https://projects.torsion.org/witten/borgmatic/src/master/sample/cron/borgmatic).
|
||||||
|
Then, from the directory where you downloaded it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mv borgmatic /etc/cron.d/borgmatic
|
||||||
|
sudo chmod +x /etc/cron.d/borgmatic
|
||||||
|
```
|
||||||
|
|
||||||
|
You can modify the cron file if you'd like to run borgmatic more or less frequently.
|
||||||
|
|
||||||
|
### systemd
|
||||||
|
|
||||||
|
If you're using systemd instead of cron to run jobs, download the [sample
|
||||||
|
systemd service
|
||||||
|
file](https://projects.torsion.org/witten/borgmatic/src/master/sample/systemd/borgmatic.service)
|
||||||
|
and the [sample systemd timer
|
||||||
|
file](https://projects.torsion.org/witten/borgmatic/src/master/sample/systemd/borgmatic.timer).
|
||||||
|
Then, from the directory where you downloaded them:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mv borgmatic.service borgmatic.timer /etc/systemd/system/
|
||||||
|
sudo systemctl enable borgmatic.timer
|
||||||
|
sudo systemctl start borgmatic.timer
|
||||||
|
```
|
||||||
|
|
||||||
|
Feel free to modify the timer file based on how frequently you'd like
|
||||||
|
borgmatic to run.
|
||||||
|
|
||||||
|
|
||||||
|
## Advanced configuration
|
||||||
|
|
||||||
### Multiple configuration files
|
### Multiple configuration files
|
||||||
|
|
||||||
A more advanced usage is to create multiple separate configuration files and
|
A more advanced usage is to create multiple separate configuration files and
|
||||||
|
@ -247,113 +385,6 @@ That's it! borgmatic will continue using your /etc/borgmatic configuration
|
||||||
files.
|
files.
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
You can run borgmatic and start a backup simply by invoking it without
|
|
||||||
arguments:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
borgmatic
|
|
||||||
```
|
|
||||||
|
|
||||||
This will also prune any old backups as per the configured retention policy,
|
|
||||||
and check backups for consistency problems due to things like file damage.
|
|
||||||
|
|
||||||
If you'd like to see the available command-line arguments, view the help:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
borgmatic --help
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that borgmatic prunes archives *before* creating an archive, so as to
|
|
||||||
free up space for archiving. This means that when a borgmatic run finishes,
|
|
||||||
there may still be prune-able archives. Not to worry, as they will get cleaned
|
|
||||||
up at the start of the next run.
|
|
||||||
|
|
||||||
### Verbosity
|
|
||||||
|
|
||||||
By default, the backup will proceed silently except in the case of errors. But
|
|
||||||
if you'd like to to get additional information about the progress of the
|
|
||||||
backup as it proceeds, use the verbosity option:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
borgmatic --verbosity 1
|
|
||||||
```
|
|
||||||
|
|
||||||
Or, for even more progress spew:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
borgmatic --verbosity 2
|
|
||||||
```
|
|
||||||
|
|
||||||
### À la carte
|
|
||||||
|
|
||||||
If you want to run borgmatic with only pruning, creating, or checking enabled,
|
|
||||||
the following optional flags are available:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
borgmatic --prune
|
|
||||||
borgmatic --create
|
|
||||||
borgmatic --check
|
|
||||||
```
|
|
||||||
|
|
||||||
You can run with only one of these flags provided, or you can mix and match
|
|
||||||
any number of them. This supports use cases like running consistency checks
|
|
||||||
from a different cron job with a different frequency, or running pruning with
|
|
||||||
a different verbosity level.
|
|
||||||
|
|
||||||
Additionally, borgmatic provides convenient flags for Borg's
|
|
||||||
[list](https://borgbackup.readthedocs.io/en/stable/usage/list.html) and
|
|
||||||
[info](https://borgbackup.readthedocs.io/en/stable/usage/info.html)
|
|
||||||
functionality:
|
|
||||||
|
|
||||||
|
|
||||||
```bash
|
|
||||||
borgmatic --list
|
|
||||||
borgmatic --info
|
|
||||||
```
|
|
||||||
|
|
||||||
You can include an optional `--json` flag with `--create`, `--list`, or
|
|
||||||
`--info` to get the output formatted as JSON.
|
|
||||||
|
|
||||||
|
|
||||||
## Autopilot
|
|
||||||
|
|
||||||
If you want to run borgmatic automatically, say once a day, the you can
|
|
||||||
configure a job runner to invoke it periodically.
|
|
||||||
|
|
||||||
### cron
|
|
||||||
|
|
||||||
If you're using cron, download the [sample cron
|
|
||||||
file](https://projects.torsion.org/witten/borgmatic/src/master/sample/cron/borgmatic).
|
|
||||||
Then, from the directory where you downloaded it:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo mv borgmatic /etc/cron.d/borgmatic
|
|
||||||
sudo chmod +x /etc/cron.d/borgmatic
|
|
||||||
```
|
|
||||||
|
|
||||||
You can modify the cron file if you'd like to run borgmatic more or less frequently.
|
|
||||||
|
|
||||||
### systemd
|
|
||||||
|
|
||||||
If you're using systemd instead of cron to run jobs, download the [sample
|
|
||||||
systemd service
|
|
||||||
file](https://projects.torsion.org/witten/borgmatic/src/master/sample/systemd/borgmatic.service)
|
|
||||||
and the [sample systemd timer
|
|
||||||
file](https://projects.torsion.org/witten/borgmatic/src/master/sample/systemd/borgmatic.timer).
|
|
||||||
Then, from the directory where you downloaded them:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo mv borgmatic.service borgmatic.timer /etc/systemd/system/
|
|
||||||
sudo systemctl enable borgmatic.timer
|
|
||||||
sudo systemctl start borgmatic.timer
|
|
||||||
```
|
|
||||||
|
|
||||||
Feel free to modify the timer file based on how frequently you'd like
|
|
||||||
borgmatic to run.
|
|
||||||
|
|
||||||
|
|
||||||
## Support and contributing
|
## Support and contributing
|
||||||
|
|
||||||
### Issues
|
### Issues
|
||||||
|
|
|
@ -9,20 +9,6 @@ import tempfile
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def initialize_environment(storage_config):
|
|
||||||
passcommand = storage_config.get('encryption_passcommand')
|
|
||||||
if passcommand:
|
|
||||||
os.environ['BORG_PASSCOMMAND'] = passcommand
|
|
||||||
|
|
||||||
passphrase = storage_config.get('encryption_passphrase')
|
|
||||||
if passphrase:
|
|
||||||
os.environ['BORG_PASSPHRASE'] = passphrase
|
|
||||||
|
|
||||||
ssh_command = storage_config.get('ssh_command')
|
|
||||||
if ssh_command:
|
|
||||||
os.environ['BORG_RSH'] = ssh_command
|
|
||||||
|
|
||||||
|
|
||||||
def _expand_directory(directory):
|
def _expand_directory(directory):
|
||||||
'''
|
'''
|
||||||
Given a directory path, expand any tilde (representing a user's home directory) and any globs
|
Given a directory path, expand any tilde (representing a user's home directory) and any globs
|
||||||
|
|
15
borgmatic/borg/environment.py
Normal file
15
borgmatic/borg/environment.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(storage_config):
|
||||||
|
passcommand = storage_config.get('encryption_passcommand')
|
||||||
|
if passcommand:
|
||||||
|
os.environ['BORG_PASSCOMMAND'] = passcommand
|
||||||
|
|
||||||
|
passphrase = storage_config.get('encryption_passphrase')
|
||||||
|
if passphrase:
|
||||||
|
os.environ['BORG_PASSPHRASE'] = passphrase
|
||||||
|
|
||||||
|
ssh_command = storage_config.get('ssh_command')
|
||||||
|
if ssh_command:
|
||||||
|
os.environ['BORG_RSH'] = ssh_command
|
31
borgmatic/borg/init.py
Normal file
31
borgmatic/borg/init.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def initialize_repository(
|
||||||
|
repository,
|
||||||
|
encryption_mode,
|
||||||
|
append_only=None,
|
||||||
|
storage_quota=None,
|
||||||
|
local_path='borg',
|
||||||
|
remote_path=None,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Given a local or remote repository path, a Borg encryption mode, whether the repository should
|
||||||
|
be append-only, and the storage quota to use, initialize the repository.
|
||||||
|
'''
|
||||||
|
full_command = (
|
||||||
|
(local_path, 'init', repository)
|
||||||
|
+ (('--encryption', encryption_mode) if encryption_mode else ())
|
||||||
|
+ (('--append-only',) if append_only else ())
|
||||||
|
+ (('--storage-quota', storage_quota) if storage_quota else ())
|
||||||
|
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||||
|
+ (('--debug',) if logger.isEnabledFor(logging.DEBUG) else ())
|
||||||
|
+ (('--remote-path', remote_path) if remote_path else ())
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(' '.join(full_command))
|
||||||
|
subprocess.check_call(full_command)
|
|
@ -8,9 +8,11 @@ import sys
|
||||||
from borgmatic.borg import (
|
from borgmatic.borg import (
|
||||||
check as borg_check,
|
check as borg_check,
|
||||||
create as borg_create,
|
create as borg_create,
|
||||||
|
environment as borg_environment,
|
||||||
prune as borg_prune,
|
prune as borg_prune,
|
||||||
list as borg_list,
|
list as borg_list,
|
||||||
info as borg_info,
|
info as borg_info,
|
||||||
|
init as borg_init,
|
||||||
)
|
)
|
||||||
from borgmatic.commands import hook
|
from borgmatic.commands import hook
|
||||||
from borgmatic.config import checks, collect, convert, validate
|
from borgmatic.config import checks, collect, convert, validate
|
||||||
|
@ -53,6 +55,26 @@ def parse_arguments(*arguments):
|
||||||
dest='excludes_filename',
|
dest='excludes_filename',
|
||||||
help='Deprecated in favor of exclude_patterns within configuration',
|
help='Deprecated in favor of exclude_patterns within configuration',
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-I', '--init', dest='init', action='store_true', help='Initialize an empty Borg repository'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-e',
|
||||||
|
'--encryption',
|
||||||
|
dest='encryption_mode',
|
||||||
|
help='Borg repository encryption mode (for use with --init)',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--append-only',
|
||||||
|
dest='append_only',
|
||||||
|
action='store_true',
|
||||||
|
help='Create an append-only repository (for use with --init)',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--storage-quota',
|
||||||
|
dest='storage_quota',
|
||||||
|
help='Create a repository with a fixed storage quota (for use with --init)',
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'-p',
|
'-p',
|
||||||
'--prune',
|
'--prune',
|
||||||
|
@ -111,7 +133,21 @@ def parse_arguments(*arguments):
|
||||||
args = parser.parse_args(arguments)
|
args = parser.parse_args(arguments)
|
||||||
|
|
||||||
if args.excludes_filename:
|
if args.excludes_filename:
|
||||||
raise ValueError('The --excludes option has been replaced with exclude_patterns in configuration')
|
raise ValueError(
|
||||||
|
'The --excludes option has been replaced with exclude_patterns in configuration'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (args.encryption_mode or args.append_only or args.storage_quota) and not args.init:
|
||||||
|
raise ValueError(
|
||||||
|
'The --encryption, --append-only, and --storage-quota options can only be used with the --init option'
|
||||||
|
)
|
||||||
|
|
||||||
|
if args.init and (args.prune or args.create or args.dry_run):
|
||||||
|
raise ValueError(
|
||||||
|
'The --init option cannot be used with the --prune, --create, or --dry-run options'
|
||||||
|
)
|
||||||
|
if args.init and not args.encryption_mode:
|
||||||
|
raise ValueError('The --encryption option is required with the --init option')
|
||||||
|
|
||||||
if args.progress and not args.create:
|
if args.progress and not args.create:
|
||||||
raise ValueError('The --progress option can only be used with the --create option')
|
raise ValueError('The --progress option can only be used with the --create option')
|
||||||
|
@ -128,7 +164,7 @@ def parse_arguments(*arguments):
|
||||||
|
|
||||||
# If any of the action flags are explicitly requested, leave them as-is. Otherwise, assume
|
# If any of the action flags are explicitly requested, leave them as-is. Otherwise, assume
|
||||||
# defaults: Mutate the given arguments to enable the default actions.
|
# defaults: Mutate the given arguments to enable the default actions.
|
||||||
if args.prune or args.create or args.check or args.list or args.info:
|
if args.init or args.prune or args.create or args.check or args.list or args.info:
|
||||||
return args
|
return args
|
||||||
|
|
||||||
args.prune = True
|
args.prune = True
|
||||||
|
@ -152,7 +188,7 @@ def run_configuration(config_filename, args): # pragma: no cover
|
||||||
try:
|
try:
|
||||||
local_path = location.get('local_path', 'borg')
|
local_path = location.get('local_path', 'borg')
|
||||||
remote_path = location.get('remote_path')
|
remote_path = location.get('remote_path')
|
||||||
borg_create.initialize_environment(storage)
|
borg_environment.initialize(storage)
|
||||||
|
|
||||||
if args.create:
|
if args.create:
|
||||||
hook.execute_hook(hooks.get('before_backup'), config_filename, 'pre-backup')
|
hook.execute_hook(hooks.get('before_backup'), config_filename, 'pre-backup')
|
||||||
|
@ -206,6 +242,16 @@ def _run_commands_on_repository(
|
||||||
): # pragma: no cover
|
): # pragma: no cover
|
||||||
repository = os.path.expanduser(unexpanded_repository)
|
repository = os.path.expanduser(unexpanded_repository)
|
||||||
dry_run_label = ' (dry run; not making any changes)' if args.dry_run else ''
|
dry_run_label = ' (dry run; not making any changes)' if args.dry_run else ''
|
||||||
|
if args.init:
|
||||||
|
logger.info('{}: Initializing repository'.format(repository))
|
||||||
|
borg_init.initialize_repository(
|
||||||
|
repository,
|
||||||
|
args.encryption_mode,
|
||||||
|
args.append_only,
|
||||||
|
args.storage_quota,
|
||||||
|
local_path=local_path,
|
||||||
|
remote_path=remote_path,
|
||||||
|
)
|
||||||
if args.prune:
|
if args.prune:
|
||||||
logger.info('{}: Pruning archives{}'.format(repository, dry_run_label))
|
logger.info('{}: Pruning archives{}'.format(repository, dry_run_label))
|
||||||
borg_prune.prune_archives(
|
borg_prune.prune_archives(
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -1,7 +1,7 @@
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
|
||||||
VERSION = '1.2.12.dev0'
|
VERSION = '1.2.12'
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
|
|
|
@ -9,7 +9,8 @@ import tempfile
|
||||||
def generate_configuration(config_path, repository_path):
|
def generate_configuration(config_path, repository_path):
|
||||||
'''
|
'''
|
||||||
Generate borgmatic configuration into a file at the config path, and update the defaults so as
|
Generate borgmatic configuration into a file at the config path, and update the defaults so as
|
||||||
to work for testing (including injecting the given repository path).
|
to work for testing (including injecting the given repository path and tacking on an encryption
|
||||||
|
passphrase).
|
||||||
'''
|
'''
|
||||||
subprocess.check_call(
|
subprocess.check_call(
|
||||||
'generate-borgmatic-config --destination {}'.format(config_path).split(' ')
|
'generate-borgmatic-config --destination {}'.format(config_path).split(' ')
|
||||||
|
@ -21,6 +22,7 @@ def generate_configuration(config_path, repository_path):
|
||||||
.replace('- /home', '- {}'.format(config_path))
|
.replace('- /home', '- {}'.format(config_path))
|
||||||
.replace('- /etc', '')
|
.replace('- /etc', '')
|
||||||
.replace('- /var/log/syslog*', '')
|
.replace('- /var/log/syslog*', '')
|
||||||
|
+ 'storage:\n encryption_passphrase: "test"'
|
||||||
)
|
)
|
||||||
config_file = open(config_path, 'w')
|
config_file = open(config_path, 'w')
|
||||||
config_file.write(config)
|
config_file.write(config)
|
||||||
|
@ -33,14 +35,13 @@ def test_borgmatic_command():
|
||||||
repository_path = os.path.join(temporary_directory, 'test.borg')
|
repository_path = os.path.join(temporary_directory, 'test.borg')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
subprocess.check_call(
|
|
||||||
'borg init --encryption repokey {}'.format(repository_path).split(' '),
|
|
||||||
env={'BORG_PASSPHRASE': '', **os.environ},
|
|
||||||
)
|
|
||||||
|
|
||||||
config_path = os.path.join(temporary_directory, 'test.yaml')
|
config_path = os.path.join(temporary_directory, 'test.yaml')
|
||||||
generate_configuration(config_path, repository_path)
|
generate_configuration(config_path, repository_path)
|
||||||
|
|
||||||
|
subprocess.check_call(
|
||||||
|
'borgmatic -v 2 --config {} --init --encryption repokey'.format(config_path).split(' ')
|
||||||
|
)
|
||||||
|
|
||||||
# Run borgmatic to generate a backup archive, and then list it to make sure it exists.
|
# Run borgmatic to generate a backup archive, and then list it to make sure it exists.
|
||||||
subprocess.check_call('borgmatic --config {}'.format(config_path).split(' '))
|
subprocess.check_call('borgmatic --config {}'.format(config_path).split(' '))
|
||||||
output = subprocess.check_output(
|
output = subprocess.check_output(
|
||||||
|
|
|
@ -16,13 +16,6 @@ def test_parse_arguments_with_no_arguments_uses_defaults():
|
||||||
assert parser.json is False
|
assert parser.json is False
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_disallows_deprecated_excludes_option():
|
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
module.parse_arguments('--config', 'myconfig', '--excludes', 'myexcludes')
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_multiple_config_paths_parses_as_list():
|
def test_parse_arguments_with_multiple_config_paths_parses_as_list():
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
@ -32,7 +25,7 @@ def test_parse_arguments_with_multiple_config_paths_parses_as_list():
|
||||||
assert parser.verbosity is 0
|
assert parser.verbosity is 0
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_verbosity_flag_overrides_default():
|
def test_parse_arguments_with_verbosity_overrides_default():
|
||||||
config_paths = ['default']
|
config_paths = ['default']
|
||||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(config_paths)
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(config_paths)
|
||||||
|
|
||||||
|
@ -43,7 +36,7 @@ def test_parse_arguments_with_verbosity_flag_overrides_default():
|
||||||
assert parser.verbosity == 1
|
assert parser.verbosity == 1
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_json_flag_overrides_default():
|
def test_parse_arguments_with_json_overrides_default():
|
||||||
parser = module.parse_arguments('--list', '--json')
|
parser = module.parse_arguments('--list', '--json')
|
||||||
assert parser.json is True
|
assert parser.json is True
|
||||||
|
|
||||||
|
@ -85,25 +78,81 @@ def test_parse_arguments_with_invalid_arguments_exits():
|
||||||
module.parse_arguments('--posix-me-harder')
|
module.parse_arguments('--posix-me-harder')
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_progress_and_create_flags_does_not_raise():
|
def test_parse_arguments_disallows_deprecated_excludes_option():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
module.parse_arguments('--config', 'myconfig', '--excludes', 'myexcludes')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_disallows_encryption_mode_without_init():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
module.parse_arguments('--config', 'myconfig', '--encryption', 'repokey')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_requires_encryption_mode_with_init():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
module.parse_arguments('--config', 'myconfig', '--init')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_disallows_append_only_without_init():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
module.parse_arguments('--config', 'myconfig', '--append-only')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_disallows_storage_quota_without_init():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
module.parse_arguments('--config', 'myconfig', '--storage-quota', '5G')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_disallows_init_and_prune():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
module.parse_arguments('--config', 'myconfig', '--init', '--prune')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_disallows_init_and_create():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
module.parse_arguments('--config', 'myconfig', '--init', '--create')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_disallows_init_and_dry_run():
|
||||||
|
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
module.parse_arguments('--config', 'myconfig', '--init', '--dry-run')
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_arguments_allows_progress_and_create():
|
||||||
module.parse_arguments('--progress', '--create', '--list')
|
module.parse_arguments('--progress', '--create', '--list')
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_progress_flag_but_no_create_flag_raises_value_error():
|
def test_parse_arguments_disallows_progress_without_create():
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
module.parse_arguments('--progress', '--list')
|
module.parse_arguments('--progress', '--list')
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_json_flag_with_list_or_info_flag_does_not_raise_any_error():
|
def test_parse_arguments_allows_json_with_list_or_info():
|
||||||
module.parse_arguments('--list', '--json')
|
module.parse_arguments('--list', '--json')
|
||||||
module.parse_arguments('--info', '--json')
|
module.parse_arguments('--info', '--json')
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_json_flag_but_no_list_or_info_flag_raises_value_error():
|
def test_parse_arguments_disallows_json_without_list_or_info():
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
module.parse_arguments('--json')
|
module.parse_arguments('--json')
|
||||||
|
|
||||||
|
|
||||||
def test_parse_arguments_with_json_flag_and_both_list_and_info_flag_raises_value_error():
|
def test_parse_arguments_disallows_json_with_both_list_and_info():
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
module.parse_arguments('--list', '--info', '--json')
|
module.parse_arguments('--list', '--info', '--json')
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
|
|
||||||
from flexmock import flexmock
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
@ -7,52 +6,6 @@ from borgmatic.borg import create as module
|
||||||
from ..test_verbosity import insert_logging_mock
|
from ..test_verbosity import insert_logging_mock
|
||||||
|
|
||||||
|
|
||||||
def test_initialize_environment_with_passcommand_should_set_environment():
|
|
||||||
orig_environ = os.environ
|
|
||||||
|
|
||||||
try:
|
|
||||||
os.environ = {}
|
|
||||||
module.initialize_environment({'encryption_passcommand': 'command'})
|
|
||||||
assert os.environ.get('BORG_PASSCOMMAND') == 'command'
|
|
||||||
finally:
|
|
||||||
os.environ = orig_environ
|
|
||||||
|
|
||||||
|
|
||||||
def test_initialize_environment_with_passphrase_should_set_environment():
|
|
||||||
orig_environ = os.environ
|
|
||||||
|
|
||||||
try:
|
|
||||||
os.environ = {}
|
|
||||||
module.initialize_environment({'encryption_passphrase': 'pass'})
|
|
||||||
assert os.environ.get('BORG_PASSPHRASE') == 'pass'
|
|
||||||
finally:
|
|
||||||
os.environ = orig_environ
|
|
||||||
|
|
||||||
|
|
||||||
def test_initialize_environment_with_ssh_command_should_set_environment():
|
|
||||||
orig_environ = os.environ
|
|
||||||
|
|
||||||
try:
|
|
||||||
os.environ = {}
|
|
||||||
module.initialize_environment({'ssh_command': 'ssh -C'})
|
|
||||||
assert os.environ.get('BORG_RSH') == 'ssh -C'
|
|
||||||
finally:
|
|
||||||
os.environ = orig_environ
|
|
||||||
|
|
||||||
|
|
||||||
def test_initialize_environment_without_configuration_should_not_set_environment():
|
|
||||||
orig_environ = os.environ
|
|
||||||
|
|
||||||
try:
|
|
||||||
os.environ = {}
|
|
||||||
module.initialize_environment({})
|
|
||||||
assert os.environ.get('BORG_PASSCOMMAND') is None
|
|
||||||
assert os.environ.get('BORG_PASSPHRASE') is None
|
|
||||||
assert os.environ.get('BORG_RSH') is None
|
|
||||||
finally:
|
|
||||||
os.environ = orig_environ
|
|
||||||
|
|
||||||
|
|
||||||
def test_expand_directory_with_basic_path_passes_it_through():
|
def test_expand_directory_with_basic_path_passes_it_through():
|
||||||
flexmock(module.os.path).should_receive('expanduser').and_return('foo')
|
flexmock(module.os.path).should_receive('expanduser').and_return('foo')
|
||||||
flexmock(module.glob).should_receive('glob').and_return([])
|
flexmock(module.glob).should_receive('glob').and_return([])
|
||||||
|
|
49
tests/unit/borg/test_environment.py
Normal file
49
tests/unit/borg/test_environment.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
from borgmatic.borg import environment as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_initialize_with_passcommand_should_set_environment():
|
||||||
|
orig_environ = os.environ
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.environ = {}
|
||||||
|
module.initialize({'encryption_passcommand': 'command'})
|
||||||
|
assert os.environ.get('BORG_PASSCOMMAND') == 'command'
|
||||||
|
finally:
|
||||||
|
os.environ = orig_environ
|
||||||
|
|
||||||
|
|
||||||
|
def test_initialize_with_passphrase_should_set_environment():
|
||||||
|
orig_environ = os.environ
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.environ = {}
|
||||||
|
module.initialize({'encryption_passphrase': 'pass'})
|
||||||
|
assert os.environ.get('BORG_PASSPHRASE') == 'pass'
|
||||||
|
finally:
|
||||||
|
os.environ = orig_environ
|
||||||
|
|
||||||
|
|
||||||
|
def test_initialize_with_ssh_command_should_set_environment():
|
||||||
|
orig_environ = os.environ
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.environ = {}
|
||||||
|
module.initialize({'ssh_command': 'ssh -C'})
|
||||||
|
assert os.environ.get('BORG_RSH') == 'ssh -C'
|
||||||
|
finally:
|
||||||
|
os.environ = orig_environ
|
||||||
|
|
||||||
|
|
||||||
|
def test_initialize_without_configuration_should_not_set_environment():
|
||||||
|
orig_environ = os.environ
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.environ = {}
|
||||||
|
module.initialize({})
|
||||||
|
assert os.environ.get('BORG_PASSCOMMAND') is None
|
||||||
|
assert os.environ.get('BORG_PASSPHRASE') is None
|
||||||
|
assert os.environ.get('BORG_RSH') is None
|
||||||
|
finally:
|
||||||
|
os.environ = orig_environ
|
58
tests/unit/borg/test_init.py
Normal file
58
tests/unit/borg/test_init.py
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
from borgmatic.borg import init as module
|
||||||
|
from ..test_verbosity import insert_logging_mock
|
||||||
|
|
||||||
|
|
||||||
|
def insert_subprocess_mock(check_call_command, **kwargs):
|
||||||
|
subprocess = flexmock(module.subprocess)
|
||||||
|
subprocess.should_receive('check_call').with_args(check_call_command, **kwargs).once()
|
||||||
|
|
||||||
|
|
||||||
|
INIT_COMMAND = ('borg', 'init', 'repo', '--encryption', 'repokey')
|
||||||
|
|
||||||
|
|
||||||
|
def test_initialize_repository_calls_borg_with_parameters():
|
||||||
|
insert_subprocess_mock(INIT_COMMAND)
|
||||||
|
|
||||||
|
module.initialize_repository(repository='repo', encryption_mode='repokey')
|
||||||
|
|
||||||
|
|
||||||
|
def test_initialize_repository_with_append_only_calls_borg_with_append_only_parameter():
|
||||||
|
insert_subprocess_mock(INIT_COMMAND + ('--append-only',))
|
||||||
|
|
||||||
|
module.initialize_repository(repository='repo', encryption_mode='repokey', append_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_initialize_repository_with_storage_quota_calls_borg_with_storage_quota_parameter():
|
||||||
|
insert_subprocess_mock(INIT_COMMAND + ('--storage-quota', '5G'))
|
||||||
|
|
||||||
|
module.initialize_repository(repository='repo', encryption_mode='repokey', storage_quota='5G')
|
||||||
|
|
||||||
|
|
||||||
|
def test_initialize_repository_with_log_info_calls_borg_with_info_parameter():
|
||||||
|
insert_subprocess_mock(INIT_COMMAND + ('--info',))
|
||||||
|
insert_logging_mock(logging.INFO)
|
||||||
|
|
||||||
|
module.initialize_repository(repository='repo', encryption_mode='repokey')
|
||||||
|
|
||||||
|
|
||||||
|
def test_initialize_repository_with_log_debug_calls_borg_with_debug_parameter():
|
||||||
|
insert_subprocess_mock(INIT_COMMAND + ('--debug',))
|
||||||
|
insert_logging_mock(logging.DEBUG)
|
||||||
|
|
||||||
|
module.initialize_repository(repository='repo', encryption_mode='repokey')
|
||||||
|
|
||||||
|
|
||||||
|
def test_initialize_repository_with_local_path_calls_borg_via_local_path():
|
||||||
|
insert_subprocess_mock(('borg1',) + INIT_COMMAND[1:])
|
||||||
|
|
||||||
|
module.initialize_repository(repository='repo', encryption_mode='repokey', local_path='borg1')
|
||||||
|
|
||||||
|
|
||||||
|
def test_initialize_repository_with_remote_path_calls_borg_with_remote_path_parameter():
|
||||||
|
insert_subprocess_mock(INIT_COMMAND + ('--remote-path', 'borg1'))
|
||||||
|
|
||||||
|
module.initialize_repository(repository='repo', encryption_mode='repokey', remote_path='borg1')
|
|
@ -6,9 +6,11 @@ from borgmatic import verbosity as module
|
||||||
|
|
||||||
|
|
||||||
def insert_logging_mock(log_level):
|
def insert_logging_mock(log_level):
|
||||||
""" Mocks the isEnabledFor from python logging. """
|
'''
|
||||||
|
Mock the isEnabledFor from Python logging.
|
||||||
|
'''
|
||||||
logging = flexmock(module.logging.Logger)
|
logging = flexmock(module.logging.Logger)
|
||||||
logging.should_receive('isEnabledFor').replace_with(lambda lvl: lvl >= log_level)
|
logging.should_receive('isEnabledFor').replace_with(lambda level: level >= log_level)
|
||||||
logging.should_receive('getEffectiveLevel').replace_with(lambda: log_level)
|
logging.should_receive('getEffectiveLevel').replace_with(lambda: log_level)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue