mirror of
https://github.com/borgmatic-collective/borgmatic.git
synced 2026-07-22 10:13:00 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd46efb193 | ||
|
|
426f54c9cc | ||
|
|
45a537b6b1 | ||
|
|
d6feca169c | ||
|
|
05e2900ab0 | ||
|
|
30b52e5523 | ||
|
|
14aeddc11f | ||
|
|
066399ecdb | ||
|
|
d4bbac4467 | ||
|
|
7516443a89 | ||
|
|
73d67e29b4 | ||
|
|
c3e7425f4c | ||
|
|
cc9dbb1def | ||
|
|
2045edc11b | ||
|
|
1dcac44d6c | ||
|
|
300ead65d3 | ||
|
|
6a0219a7a4 | ||
|
|
80c69aac05 | ||
|
|
7417a3cd00 | ||
|
|
9ca80a54d8 | ||
|
|
5c0b17ef39 | ||
|
|
1697d8aaef | ||
|
|
fef441a8ff | ||
|
|
c1ddc4268b | ||
|
|
e323290e61 | ||
|
|
1ab44d4201 | ||
|
|
71b1c3dfb0 | ||
|
|
695930a607 | ||
|
|
eb2a4ff1f0 | ||
|
|
531d5c80c0 | ||
|
|
067ed27689 | ||
|
|
fa38de2de7 | ||
|
|
e4d1b49c39 | ||
|
|
af7caec509 | ||
|
|
90c1f899fc | ||
|
|
a0691ae4cd | ||
|
|
2f20e6f808 | ||
|
|
7a4636ae0f | ||
|
|
53435dcc3e | ||
|
|
4d01278037 | ||
|
|
2299e5d41e | ||
|
|
d16f5d5df3 |
+10
-1
@@ -1,9 +1,18 @@
|
||||
pipeline:
|
||||
build:
|
||||
image: python:3.7.0-alpine3.8
|
||||
image: python:${PYTHON_VERSION}-alpine${ALPINE_VERSION}
|
||||
pull: true
|
||||
commands:
|
||||
- pip install tox
|
||||
- tox
|
||||
- apk add --no-cache borgbackup
|
||||
- tox -e end-to-end
|
||||
|
||||
matrix:
|
||||
ALPINE_VERSION:
|
||||
- 3.7
|
||||
- 3.8
|
||||
PYTHON_VERSION:
|
||||
- 3.5
|
||||
- 3.6
|
||||
- 3.7
|
||||
|
||||
@@ -1,3 +1,40 @@
|
||||
1.2.14
|
||||
* #103: When generating sample configuration with generate-borgmatic-config, document the defaults
|
||||
for each option.
|
||||
* #116: When running multiple configuration files, attempt all configuration files even if one of
|
||||
them errors. Log a summary of results at the end.
|
||||
* Add borgmatic --version command-line flag to get the current installed version number.
|
||||
|
||||
1.2.13
|
||||
* #100: Support for --stats command-line flag independent of --verbosity.
|
||||
* #117: With borgmatic --init command-line flag, proceed without erroring if a repository already
|
||||
exists.
|
||||
|
||||
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.
|
||||
* #113: Update README with link to a new/forked Docker image.
|
||||
* Prevent deprecated --excludes command-line option from being used.
|
||||
* Refactor README a bit to flow better for first-time users.
|
||||
* Update README with a few additional borgmatic packages (Debian and Ubuntu).
|
||||
|
||||
1.2.11
|
||||
* #108: Support for Borg create --progress via borgmatic command-line flag.
|
||||
|
||||
1.2.10
|
||||
* #105: Support for Borg --chunker-params create option via "chunker_params" in borgmatic's storage
|
||||
section.
|
||||
|
||||
1.2.9
|
||||
* #102: Fix for syntax error that occurred in Python 3.5 and below.
|
||||
* Make automated tests support running in Python 3.5.
|
||||
|
||||
1.2.8
|
||||
* #73: Enable consistency checks for only certain repositories via "check_repositories" option in
|
||||
borgmatic's consistency configuration. Handy for large repositories that take forever to check.
|
||||
* Include link to issue tracker within various command output.
|
||||
* Run continuous integration tests on a matrix of Python and Borg versions.
|
||||
|
||||
1.2.7
|
||||
* #98: Support for Borg --keep-secondly prune option.
|
||||
* Use Black code formatter and Flake8 code checker as part of running automated tests.
|
||||
|
||||
@@ -56,26 +56,11 @@ href="https://asciinema.org/a/203761" target="_blank">screencast</a>.
|
||||
|
||||
## Installation
|
||||
|
||||
To get up and running, follow the [Borg Quick
|
||||
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 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.
|
||||
To get up and running, first [install
|
||||
Borg](https://borgbackup.readthedocs.io/en/latest/installation.html), at
|
||||
least version 1.1.
|
||||
|
||||
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:
|
||||
Then, run the following command to download and install borgmatic:
|
||||
|
||||
```bash
|
||||
sudo pip3 install --upgrade borgmatic
|
||||
@@ -84,19 +69,19 @@ sudo pip3 install --upgrade borgmatic
|
||||
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.
|
||||
|
||||
|
||||
### Other ways to install
|
||||
|
||||
* [A borgmatic Docker image](https://hub.docker.com/r/b3vis/borgmatic/) based
|
||||
on Alpine Linux.
|
||||
* [Another borgmatic Docker image](https://hub.docker.com/r/coaxial/borgmatic/) based
|
||||
on Alpine Linux, updated automatically whenever the Alpine image updates.
|
||||
* [A borgmatic package for
|
||||
Fedora](https://bodhi.fedoraproject.org/updates/?search=borgmatic).
|
||||
* [A borgmatic package for Arch
|
||||
Linux](https://aur.archlinux.org/packages/borgmatic/).
|
||||
* [A borgmatic package for OpenBSD](http://ports.su/sysutils/borgmatic).
|
||||
* [Docker image](https://hub.docker.com/r/monachus/borgmatic/)
|
||||
* [Another Docker image](https://hub.docker.com/r/b3vis/borgmatic/)
|
||||
* [Debian](https://tracker.debian.org/pkg/borgmatic)
|
||||
* [Ubuntu](https://launchpad.net/ubuntu/+source/borgmatic)
|
||||
* [Fedora](https://bodhi.fedoraproject.org/updates/?search=borgmatic)
|
||||
* [Arch Linux](https://aur.archlinux.org/packages/borgmatic/)
|
||||
* [OpenBSD](http://ports.su/sysutils/borgmatic)
|
||||
<br><br>
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
After you install borgmatic, generate a sample configuration file:
|
||||
@@ -120,6 +105,165 @@ borgmatic has added new options since you originally created your
|
||||
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.
|
||||
|
||||
Note that borgmatic skips repository initialization if the repository already
|
||||
exists. This supports use cases like ensuring a repository exists prior to
|
||||
performing a backup.
|
||||
|
||||
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
|
||||
|
||||
A more advanced usage is to create multiple separate configuration files and
|
||||
@@ -243,113 +387,6 @@ That's it! borgmatic will continue using your /etc/borgmatic configuration
|
||||
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
|
||||
|
||||
### Issues
|
||||
@@ -441,9 +478,6 @@ cd borgmatic
|
||||
tox
|
||||
```
|
||||
|
||||
Note that while running borgmatic itself only requires Python 3+, running
|
||||
borgmatic's tests require Python 3.6+.
|
||||
|
||||
If when running tests, you get an error from the
|
||||
[Black](https://black.readthedocs.io/en/stable/) code formatter about files
|
||||
that would be reformatted, you can ask Black to format them for you via the
|
||||
@@ -478,7 +512,7 @@ consistency:
|
||||
```
|
||||
|
||||
This error can be caused by an ssh timeout, which you can rectify by adding
|
||||
the following to the ~/.ssh/config file on the client:
|
||||
the following to the `~/.ssh/config` file on the client:
|
||||
|
||||
```text
|
||||
Host *
|
||||
|
||||
@@ -9,20 +9,6 @@ import tempfile
|
||||
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):
|
||||
'''
|
||||
Given a directory path, expand any tilde (representing a user's home directory) and any globs
|
||||
@@ -104,6 +90,8 @@ def create_archive(
|
||||
storage_config,
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
progress=False,
|
||||
stats=False,
|
||||
json=False,
|
||||
):
|
||||
'''
|
||||
@@ -115,6 +103,7 @@ def create_archive(
|
||||
pattern_file = _write_pattern_file(location_config.get('patterns'))
|
||||
exclude_file = _write_pattern_file(_expand_directories(location_config.get('exclude_patterns')))
|
||||
checkpoint_interval = storage_config.get('checkpoint_interval', None)
|
||||
chunker_params = storage_config.get('chunker_params', None)
|
||||
compression = storage_config.get('compression', None)
|
||||
remote_rate_limit = storage_config.get('remote_rate_limit', None)
|
||||
umask = storage_config.get('umask', None)
|
||||
@@ -135,6 +124,7 @@ def create_archive(
|
||||
+ _make_pattern_flags(location_config, pattern_file.name if pattern_file else None)
|
||||
+ _make_exclude_flags(location_config, exclude_file.name if exclude_file else None)
|
||||
+ (('--checkpoint-interval', str(checkpoint_interval)) if checkpoint_interval else ())
|
||||
+ (('--chunker-params', chunker_params) if chunker_params else ())
|
||||
+ (('--compression', compression) if compression else ())
|
||||
+ (('--remote-ratelimit', str(remote_rate_limit)) if remote_rate_limit else ())
|
||||
+ (('--one-file-system',) if location_config.get('one_file_system') else ())
|
||||
@@ -144,11 +134,12 @@ def create_archive(
|
||||
+ (('--remote-path', remote_path) if remote_path else ())
|
||||
+ (('--umask', str(umask)) if umask else ())
|
||||
+ (('--lock-wait', str(lock_wait)) if lock_wait else ())
|
||||
+ (('--list', '--filter', 'AME') if logger.isEnabledFor(logging.INFO) else ())
|
||||
+ (('--list', '--filter', 'AME-') if logger.isEnabledFor(logging.INFO) else ())
|
||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||
+ (('--stats',) if not dry_run and logger.isEnabledFor(logging.INFO) else ())
|
||||
+ (('--stats',) if not dry_run and (logger.isEnabledFor(logging.INFO) or stats) else ())
|
||||
+ (('--debug', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ (('--dry-run',) if dry_run else ())
|
||||
+ (('--progress',) if progress else ())
|
||||
+ (('--json',) if json else ())
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,39 @@
|
||||
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. If the repository
|
||||
already exists, then log and skip initialization.
|
||||
'''
|
||||
info_command = (local_path, 'info', repository)
|
||||
logger.debug(' '.join(info_command))
|
||||
|
||||
if subprocess.call(info_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0:
|
||||
logger.info('Repository already exists. Skipping initialization.')
|
||||
return
|
||||
|
||||
init_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(init_command))
|
||||
subprocess.check_call(init_command)
|
||||
@@ -31,7 +31,13 @@ def _make_prune_flags(retention_config):
|
||||
|
||||
|
||||
def prune_archives(
|
||||
dry_run, repository, storage_config, retention_config, local_path='borg', remote_path=None
|
||||
dry_run,
|
||||
repository,
|
||||
storage_config,
|
||||
retention_config,
|
||||
local_path='borg',
|
||||
remote_path=None,
|
||||
stats=False,
|
||||
):
|
||||
'''
|
||||
Given dry-run flag, a local or remote repository path, a storage config dict, and a
|
||||
@@ -51,6 +57,7 @@ def prune_archives(
|
||||
+ (('--info',) if logger.getEffectiveLevel() == logging.INFO else ())
|
||||
+ (('--debug', '--list', '--show-rc') if logger.isEnabledFor(logging.DEBUG) else ())
|
||||
+ (('--dry-run',) if dry_run else ())
|
||||
+ (('--stats',) if stats else ())
|
||||
)
|
||||
|
||||
logger.debug(' '.join(full_command))
|
||||
|
||||
+157
-35
@@ -5,15 +5,19 @@ import os
|
||||
from subprocess import CalledProcessError
|
||||
import sys
|
||||
|
||||
import pkg_resources
|
||||
|
||||
from borgmatic.borg import (
|
||||
check as borg_check,
|
||||
create as borg_create,
|
||||
environment as borg_environment,
|
||||
prune as borg_prune,
|
||||
list as borg_list,
|
||||
info as borg_info,
|
||||
init as borg_init,
|
||||
)
|
||||
from borgmatic.commands import hook
|
||||
from borgmatic.config import collect, convert, validate
|
||||
from borgmatic.config import checks, collect, convert, validate
|
||||
from borgmatic.signals import configure_signals
|
||||
from borgmatic.verbosity import verbosity_to_log_level
|
||||
|
||||
@@ -27,7 +31,7 @@ LEGACY_CONFIG_PATH = '/etc/borgmatic/config'
|
||||
def parse_arguments(*arguments):
|
||||
'''
|
||||
Given command-line arguments with which this script was invoked, parse the arguments and return
|
||||
them as an ArgumentParser instance.
|
||||
them as an argparse.ArgumentParser instance.
|
||||
'''
|
||||
config_paths = collect.get_default_config_paths()
|
||||
|
||||
@@ -53,6 +57,26 @@ def parse_arguments(*arguments):
|
||||
dest='excludes_filename',
|
||||
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(
|
||||
'-p',
|
||||
'--prune',
|
||||
@@ -78,6 +102,20 @@ def parse_arguments(*arguments):
|
||||
action='store_true',
|
||||
help='Display summary information on archives',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--progress',
|
||||
dest='progress',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='Display progress with --create option for each file as it is backed up',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--stats',
|
||||
dest='stats',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='Display statistics of archive with --create or --prune option',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--json',
|
||||
dest='json',
|
||||
@@ -100,9 +138,37 @@ def parse_arguments(*arguments):
|
||||
default=0,
|
||||
help='Display verbose progress (1 for some, 2 for lots)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--version',
|
||||
dest='version',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help='Display installed version number of borgmatic and exit',
|
||||
)
|
||||
|
||||
args = parser.parse_args(arguments)
|
||||
|
||||
if args.excludes_filename:
|
||||
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.dry_run:
|
||||
raise ValueError('The --init option cannot be used with the --dry-run option')
|
||||
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:
|
||||
raise ValueError('The --progress option can only be used with the --create option')
|
||||
|
||||
if args.stats and not (args.create or args.prune):
|
||||
raise ValueError('The --stats option can only be used with the --create or --prune options')
|
||||
|
||||
if args.json and not (args.create or args.list or args.info):
|
||||
raise ValueError(
|
||||
'The --json option can only be used with the --create, --list, or --info options'
|
||||
@@ -115,7 +181,7 @@ def parse_arguments(*arguments):
|
||||
|
||||
# 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.
|
||||
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
|
||||
|
||||
args.prune = True
|
||||
@@ -139,12 +205,20 @@ def run_configuration(config_filename, args): # pragma: no cover
|
||||
try:
|
||||
local_path = location.get('local_path', 'borg')
|
||||
remote_path = location.get('remote_path')
|
||||
borg_create.initialize_environment(storage)
|
||||
borg_environment.initialize(storage)
|
||||
|
||||
if args.create:
|
||||
hook.execute_hook(hooks.get('before_backup'), config_filename, 'pre-backup')
|
||||
|
||||
_run_commands(args, consistency, local_path, location, remote_path, retention, storage)
|
||||
_run_commands(
|
||||
args=args,
|
||||
consistency=consistency,
|
||||
local_path=local_path,
|
||||
location=location,
|
||||
remote_path=remote_path,
|
||||
retention=retention,
|
||||
storage=storage,
|
||||
)
|
||||
|
||||
if args.create:
|
||||
hook.execute_hook(hooks.get('after_backup'), config_filename, 'post-backup')
|
||||
@@ -153,25 +227,26 @@ def run_configuration(config_filename, args): # pragma: no cover
|
||||
raise
|
||||
|
||||
|
||||
def _run_commands(args, consistency, local_path, location, remote_path, retention, storage):
|
||||
def _run_commands(*, args, consistency, local_path, location, remote_path, retention, storage):
|
||||
json_results = []
|
||||
for unexpanded_repository in location['repositories']:
|
||||
_run_commands_on_repository(
|
||||
args,
|
||||
consistency,
|
||||
json_results,
|
||||
local_path,
|
||||
location,
|
||||
remote_path,
|
||||
retention,
|
||||
storage,
|
||||
unexpanded_repository,
|
||||
args=args,
|
||||
consistency=consistency,
|
||||
json_results=json_results,
|
||||
local_path=local_path,
|
||||
location=location,
|
||||
remote_path=remote_path,
|
||||
retention=retention,
|
||||
storage=storage,
|
||||
unexpanded_repository=unexpanded_repository,
|
||||
)
|
||||
if args.json:
|
||||
sys.stdout.write(json.dumps(json_results))
|
||||
|
||||
|
||||
def _run_commands_on_repository(
|
||||
*,
|
||||
args,
|
||||
consistency,
|
||||
json_results,
|
||||
@@ -180,10 +255,20 @@ def _run_commands_on_repository(
|
||||
remote_path,
|
||||
retention,
|
||||
storage,
|
||||
unexpanded_repository,
|
||||
unexpanded_repository
|
||||
): # pragma: no cover
|
||||
repository = os.path.expanduser(unexpanded_repository)
|
||||
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:
|
||||
logger.info('{}: Pruning archives{}'.format(repository, dry_run_label))
|
||||
borg_prune.prune_archives(
|
||||
@@ -193,6 +278,7 @@ def _run_commands_on_repository(
|
||||
retention,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
stats=args.stats,
|
||||
)
|
||||
if args.create:
|
||||
logger.info('{}: Creating archive{}'.format(repository, dry_run_label))
|
||||
@@ -203,8 +289,10 @@ def _run_commands_on_repository(
|
||||
storage,
|
||||
local_path=local_path,
|
||||
remote_path=remote_path,
|
||||
progress=args.progress,
|
||||
stats=args.stats,
|
||||
)
|
||||
if args.check:
|
||||
if args.check and checks.repository_enabled_for_checks(repository, consistency):
|
||||
logger.info('{}: Running consistency checks'.format(repository))
|
||||
borg_check.check_archives(
|
||||
repository, storage, consistency, local_path=local_path, remote_path=remote_path
|
||||
@@ -229,23 +317,57 @@ def _run_commands_on_repository(
|
||||
sys.stdout.write(output)
|
||||
|
||||
|
||||
def main(): # pragma: no cover
|
||||
try:
|
||||
configure_signals()
|
||||
args = parse_arguments(*sys.argv[1:])
|
||||
logging.basicConfig(level=verbosity_to_log_level(args.verbosity), format='%(message)s')
|
||||
|
||||
config_filenames = tuple(collect.collect_config_filenames(args.config_paths))
|
||||
logger.debug('Ensuring legacy configuration is upgraded')
|
||||
convert.guard_configuration_upgraded(LEGACY_CONFIG_PATH, config_filenames)
|
||||
|
||||
if len(config_filenames) == 0:
|
||||
raise ValueError(
|
||||
'Error: No configuration files found in: {}'.format(' '.join(args.config_paths))
|
||||
)
|
||||
|
||||
for config_filename in config_filenames:
|
||||
def collect_configuration_run_summary_logs(config_filenames, args):
|
||||
'''
|
||||
Given a sequence of configuration filenames and parsed command-line arguments as an
|
||||
argparse.ArgumentParser instance, run each configuration file and yield a series of
|
||||
logging.LogRecord instances containing summary information about each run.
|
||||
'''
|
||||
for config_filename in config_filenames:
|
||||
try:
|
||||
run_configuration(config_filename, args)
|
||||
except (ValueError, OSError, CalledProcessError) as error:
|
||||
print(error, file=sys.stderr)
|
||||
yield logging.makeLogRecord(
|
||||
dict(
|
||||
levelno=logging.INFO,
|
||||
msg='{}: Successfully ran configuration file'.format(config_filename),
|
||||
)
|
||||
)
|
||||
except (ValueError, OSError, CalledProcessError) as error:
|
||||
yield logging.makeLogRecord(
|
||||
dict(
|
||||
levelno=logging.CRITICAL,
|
||||
msg='{}: Error running configuration file'.format(config_filename),
|
||||
)
|
||||
)
|
||||
yield logging.makeLogRecord(dict(levelno=logging.CRITICAL, msg=error))
|
||||
|
||||
if not config_filenames:
|
||||
yield logging.makeLogRecord(
|
||||
dict(
|
||||
levelno=logging.CRITICAL,
|
||||
msg='{}: No configuration files found'.format(' '.join(args.config_paths)),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def main(): # pragma: no cover
|
||||
configure_signals()
|
||||
args = parse_arguments(*sys.argv[1:])
|
||||
logging.basicConfig(level=verbosity_to_log_level(args.verbosity), format='%(message)s')
|
||||
|
||||
if args.version:
|
||||
print(pkg_resources.require('borgmatic')[0].version)
|
||||
sys.exit(0)
|
||||
|
||||
config_filenames = tuple(collect.collect_config_filenames(args.config_paths))
|
||||
logger.debug('Ensuring legacy configuration is upgraded')
|
||||
convert.guard_configuration_upgraded(LEGACY_CONFIG_PATH, config_filenames)
|
||||
|
||||
summary_logs = tuple(collect_configuration_run_summary_logs(config_filenames, args))
|
||||
|
||||
logger.info('\nsummary:')
|
||||
[logger.handle(log) for log in summary_logs if log.levelno >= logger.getEffectiveLevel()]
|
||||
|
||||
if any(log.levelno == logging.CRITICAL for log in summary_logs):
|
||||
logger.critical('\nNeed some help? https://torsion.org/borgmatic/#issues')
|
||||
sys.exit(1)
|
||||
|
||||
@@ -38,6 +38,8 @@ def main(): # pragma: no cover
|
||||
print()
|
||||
print('Please edit the file to suit your needs. The values are just representative.')
|
||||
print('All fields are optional except where indicated.')
|
||||
print()
|
||||
print('If you ever need help: https://torsion.org/borgmatic/#issues')
|
||||
except (ValueError, OSError) as error:
|
||||
print(error, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
def repository_enabled_for_checks(repository, consistency):
|
||||
'''
|
||||
Given a repository name and a consistency configuration dict, return whether the repository
|
||||
is enabled to have consistency checks run.
|
||||
'''
|
||||
if not consistency.get('check_repositories'):
|
||||
return True
|
||||
|
||||
return repository in consistency['check_repositories']
|
||||
@@ -30,14 +30,14 @@ map:
|
||||
- user@backupserver:sourcehostname.borg
|
||||
one_file_system:
|
||||
type: bool
|
||||
desc: Stay in same file system (do not cross mount points).
|
||||
desc: Stay in same file system (do not cross mount points). Defaults to false.
|
||||
example: true
|
||||
read_special:
|
||||
type: bool
|
||||
desc: |
|
||||
Use Borg's --read-special flag to allow backup of block and other special
|
||||
devices. Use with caution, as it will lead to problems if used when
|
||||
backing up special devices such as /dev/zero.
|
||||
backing up special devices such as /dev/zero. Defaults to false.
|
||||
example: false
|
||||
bsd_flags:
|
||||
type: bool
|
||||
@@ -48,7 +48,7 @@ map:
|
||||
desc: |
|
||||
Mode in which to operate the files cache. See
|
||||
https://borgbackup.readthedocs.io/en/stable/usage/create.html#description for
|
||||
details.
|
||||
details. Defaults to "ctime,size,inode".
|
||||
example: ctime,size,inode
|
||||
local_path:
|
||||
type: scalar
|
||||
@@ -102,11 +102,13 @@ map:
|
||||
type: bool
|
||||
desc: |
|
||||
Exclude directories that contain a CACHEDIR.TAG file. See
|
||||
http://www.brynosaurus.com/cachedir/spec.html for details.
|
||||
http://www.brynosaurus.com/cachedir/spec.html for details. Defaults to false.
|
||||
example: true
|
||||
exclude_if_present:
|
||||
type: scalar
|
||||
desc: Exclude directories that contain a file with the given filename.
|
||||
desc: |
|
||||
Exclude directories that contain a file with the given filename. Defaults to not
|
||||
set.
|
||||
example: .nobackup
|
||||
storage:
|
||||
desc: |
|
||||
@@ -121,7 +123,7 @@ map:
|
||||
The standard output of this command is used to unlock the encryption key. Only
|
||||
use on repositories that were initialized with passcommand/repokey encryption.
|
||||
Note that if both encryption_passcommand and encryption_passphrase are set,
|
||||
then encryption_passphrase takes precedence.
|
||||
then encryption_passphrase takes precedence. Defaults to not set.
|
||||
example: "secret-tool lookup borg-repository repo-name"
|
||||
encryption_passphrase:
|
||||
type: scalar
|
||||
@@ -129,7 +131,7 @@ map:
|
||||
Passphrase to unlock the encryption key with. Only use on repositories that were
|
||||
initialized with passphrase/repokey encryption. Quote the value if it contains
|
||||
punctuation, so it parses correctly. And backslash any quote or backslash
|
||||
literals as well.
|
||||
literals as well. Defaults to not set.
|
||||
example: "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
|
||||
checkpoint_interval:
|
||||
type: int
|
||||
@@ -138,6 +140,13 @@ map:
|
||||
https://borgbackup.readthedocs.io/en/stable/faq.html#if-a-backup-stops-mid-way-does-the-already-backed-up-data-stay-there
|
||||
for details. Defaults to checkpoints every 1800 seconds (30 minutes).
|
||||
example: 1800
|
||||
chunker_params:
|
||||
type: scalar
|
||||
desc: |
|
||||
Specify the parameters passed to then chunker (CHUNK_MIN_EXP, CHUNK_MAX_EXP,
|
||||
HASH_MASK_BITS, HASH_WINDOW_SIZE). See https://borgbackup.readthedocs.io/en/stable/internals.html
|
||||
for details. Defaults to "19,23,21,4095".
|
||||
example: 19,23,21,4095
|
||||
compression:
|
||||
type: scalar
|
||||
desc: |
|
||||
@@ -147,25 +156,27 @@ map:
|
||||
example: lz4
|
||||
remote_rate_limit:
|
||||
type: int
|
||||
desc: Remote network upload rate limit in kiBytes/second.
|
||||
desc: Remote network upload rate limit in kiBytes/second. Defaults to unlimited.
|
||||
example: 100
|
||||
ssh_command:
|
||||
type: scalar
|
||||
desc: Command to use instead of just "ssh". This can be used to specify ssh options.
|
||||
desc: |
|
||||
Command to use instead of just "ssh". This can be used to specify ssh options.
|
||||
Defaults to not set.
|
||||
example: ssh -i /path/to/private/key
|
||||
umask:
|
||||
type: scalar
|
||||
desc: Umask to be used for borg create.
|
||||
desc: Umask to be used for borg create. Defaults to 0077.
|
||||
example: 0077
|
||||
lock_wait:
|
||||
type: int
|
||||
desc: Maximum seconds to wait for acquiring a repository/cache lock.
|
||||
desc: Maximum seconds to wait for acquiring a repository/cache lock. Defaults to 1.
|
||||
example: 5
|
||||
archive_name_format:
|
||||
type: scalar
|
||||
desc: |
|
||||
Name of the archive. Borg placeholders can be used. See the output of
|
||||
"borg help placeholders" for details. Default is
|
||||
"borg help placeholders" for details. Defaults to
|
||||
"{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}". If you specify this option, you must
|
||||
also specify a prefix in the retention section to avoid accidental pruning of
|
||||
archives with a different archive name format. And you should also specify a
|
||||
@@ -214,7 +225,7 @@ map:
|
||||
desc: |
|
||||
When pruning, only consider archive names starting with this prefix.
|
||||
Borg placeholders can be used. See the output of "borg help placeholders" for
|
||||
details. Default is "{hostname}-".
|
||||
details. Defaults to "{hostname}-".
|
||||
example: sourcehostname
|
||||
consistency:
|
||||
desc: |
|
||||
@@ -236,17 +247,27 @@ map:
|
||||
example:
|
||||
- repository
|
||||
- archives
|
||||
check_repositories:
|
||||
seq:
|
||||
- type: scalar
|
||||
desc: |
|
||||
Paths to a subset of the repositories in the location section on which to run
|
||||
consistency checks. Handy in case some of your repositories are very large, and
|
||||
so running consistency checks on them would take too long. Defaults to running
|
||||
consistency checks on all repositories configured in the location section.
|
||||
example:
|
||||
- user@backupserver:sourcehostname.borg
|
||||
check_last:
|
||||
type: int
|
||||
desc: Restrict the number of checked archives to the last n. Applies only to the
|
||||
"archives" check.
|
||||
"archives" check. Defaults to checking all archives.
|
||||
example: 3
|
||||
prefix:
|
||||
type: scalar
|
||||
desc: |
|
||||
When performing the "archives" check, only consider archive names starting with
|
||||
this prefix. Borg placeholders can be used. See the output of
|
||||
"borg help placeholders" for details. Default is "{hostname}-".
|
||||
"borg help placeholders" for details. Defaults to "{hostname}-".
|
||||
example: sourcehostname
|
||||
hooks:
|
||||
desc: |
|
||||
|
||||
@@ -51,6 +51,19 @@ def apply_logical_validation(config_filename, parsed_configuration):
|
||||
('If you provide an archive_name_format, you must also specify a retention prefix.',),
|
||||
)
|
||||
|
||||
location_repositories = parsed_configuration.get('location', {}).get('repositories')
|
||||
check_repositories = parsed_configuration.get('consistency', {}).get('check_repositories', [])
|
||||
for repository in check_repositories:
|
||||
if repository not in location_repositories:
|
||||
raise Validation_error(
|
||||
config_filename,
|
||||
(
|
||||
'Unknown repository in the consistency section\'s check_repositories: {}'.format(
|
||||
repository
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
consistency_prefix = parsed_configuration.get('consistency', {}).get('prefix')
|
||||
if archive_name_format and not consistency_prefix:
|
||||
logger.warning(
|
||||
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
if which black; then
|
||||
black --skip-string-normalization --line-length 100 --check .
|
||||
else
|
||||
echo "Skipping black due to not being installed."
|
||||
fi
|
||||
@@ -40,6 +40,8 @@ for sub_command in prune create check list info; do
|
||||
| grep -v '^--list$' \
|
||||
| grep -v '^--nobsdflags$' \
|
||||
| grep -v '^--pattern$' \
|
||||
| grep -v '^--progress$' \
|
||||
| grep -v '^--stats$' \
|
||||
| grep -v '^--read-special$' \
|
||||
| grep -v '^--repository-only$' \
|
||||
| grep -v '^--show-rc$' \
|
||||
|
||||
@@ -2,13 +2,38 @@
|
||||
|
||||
set -e
|
||||
|
||||
projects_token=${1:-}
|
||||
github_token=${2:-}
|
||||
|
||||
if [[ -z $github_token ]]; then
|
||||
echo "Usage: $0 [projects-token] [github-token]"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f NEWS ]]; then
|
||||
echo "Missing NEWS file. Try running from root of repository."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
version=$(head --lines=1 NEWS)
|
||||
git tag $version
|
||||
git push origin $version
|
||||
git push github $version
|
||||
|
||||
# Build borgmatic and publish to pypi.
|
||||
rm -fr dist
|
||||
python3 setup.py bdist_wheel
|
||||
python3 setup.py sdist
|
||||
twine upload -r pypi dist/borgmatic-*.tar.gz
|
||||
twine upload -r pypi dist/borgmatic-*-py3-none-any.whl
|
||||
|
||||
# Set release changelogs on projects.evoworx.org and GitHub.
|
||||
release_changelog="$(cat NEWS | sed '/^$/q' | grep -v '^\S')"
|
||||
escaped_release_changelog="$(echo "$release_changelog" | sed -z 's/\n/\\n/g' | sed -z 's/\"/\\"/g')"
|
||||
curl --silent --request POST \
|
||||
"https://projects.torsion.org/api/v1/repos/witten/borgmatic/releases?access_token=$projects_token" \
|
||||
--header "Accept: application/json" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data "{\"body\": \"$escaped_release_changelog\", \"draft\": false, \"name\": \"borgmatic $version\", \"prerelease\": false, \"tag_name\": \"$version\"}"
|
||||
|
||||
github-release create --token="$github_token" --owner=witten --repo=borgmatic --tag="$version" \
|
||||
--name="borgmatic $version" --body="$release_changelog"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
|
||||
VERSION = '1.2.7'
|
||||
VERSION = '1.2.14'
|
||||
|
||||
|
||||
setup(
|
||||
@@ -11,7 +11,7 @@ setup(
|
||||
author='Dan Helfman',
|
||||
author_email='witten@torsion.org',
|
||||
url='https://torsion.org/borgmatic',
|
||||
classifiers=(
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: System Administrators',
|
||||
@@ -19,7 +19,7 @@ setup(
|
||||
'Programming Language :: Python',
|
||||
'Topic :: Security :: Cryptography',
|
||||
'Topic :: System :: Archiving :: Backup',
|
||||
),
|
||||
],
|
||||
packages=find_packages(),
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
@@ -30,6 +30,5 @@ setup(
|
||||
},
|
||||
obsoletes=['atticmatic'],
|
||||
install_requires=('pykwalify>=1.6.0,<14.06', 'ruamel.yaml>0.15.0,<0.16.0', 'setuptools'),
|
||||
tests_require=('flexmock', 'pytest'),
|
||||
include_package_data=True,
|
||||
)
|
||||
|
||||
+18
-2
@@ -1,7 +1,23 @@
|
||||
black==18.9b0
|
||||
appdirs==1.4.3
|
||||
atomicwrites==1.2.1
|
||||
attrs==18.2.0
|
||||
black==18.9b0; python_version >= '3.6'
|
||||
Click==7.0
|
||||
coverage==4.5.1
|
||||
docopt==0.6.2
|
||||
flake8==3.5.0
|
||||
flexmock==0.10.2
|
||||
mccabe==0.6.1
|
||||
more-itertools==4.3.0
|
||||
pluggy==0.7.1
|
||||
py==1.6.0
|
||||
pycodestyle==2.3.1
|
||||
pyflakes==2.0.0
|
||||
pykwalify==1.7.0
|
||||
pytest==3.8.1
|
||||
pytest==3.8.2
|
||||
pytest-cov==2.6.0
|
||||
python-dateutil==2.7.3
|
||||
PyYAML==3.13
|
||||
ruamel.yaml>0.15.0,<0.16.0
|
||||
six==1.11.0
|
||||
toml==0.10.0
|
||||
|
||||
@@ -9,16 +9,20 @@ import tempfile
|
||||
def generate_configuration(config_path, repository_path):
|
||||
'''
|
||||
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(f'generate-borgmatic-config --destination {config_path}'.split(' '))
|
||||
subprocess.check_call(
|
||||
'generate-borgmatic-config --destination {}'.format(config_path).split(' ')
|
||||
)
|
||||
config = (
|
||||
open(config_path)
|
||||
.read()
|
||||
.replace('user@backupserver:sourcehostname.borg', repository_path)
|
||||
.replace('- /home', f'- {config_path}')
|
||||
.replace('- /home', '- {}'.format(config_path))
|
||||
.replace('- /etc', '')
|
||||
.replace('- /var/log/syslog*', '')
|
||||
+ 'storage:\n encryption_passphrase: "test"'
|
||||
)
|
||||
config_file = open(config_path, 'w')
|
||||
config_file.write(config)
|
||||
@@ -31,23 +35,30 @@ def test_borgmatic_command():
|
||||
repository_path = os.path.join(temporary_directory, 'test.borg')
|
||||
|
||||
try:
|
||||
subprocess.check_call(
|
||||
f'borg init --encryption repokey {repository_path}'.split(' '),
|
||||
env={'BORG_PASSPHRASE': '', **os.environ},
|
||||
)
|
||||
|
||||
config_path = os.path.join(temporary_directory, 'test.yaml')
|
||||
generate_configuration(config_path, repository_path)
|
||||
|
||||
# Run borgmatic to generate a backup archive, and then list it to make sure it exists.
|
||||
subprocess.check_call(f'borgmatic --config {config_path}'.split(' '))
|
||||
output = subprocess.check_output(
|
||||
f'borgmatic --config {config_path} --list --json'.split(' '),
|
||||
encoding=sys.stdout.encoding,
|
||||
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.
|
||||
subprocess.check_call('borgmatic --config {}'.format(config_path).split(' '))
|
||||
output = subprocess.check_output(
|
||||
'borgmatic --config {} --list --json'.format(config_path).split(' ')
|
||||
).decode(sys.stdout.encoding)
|
||||
parsed_output = json.loads(output)
|
||||
|
||||
assert len(parsed_output) == 1
|
||||
assert len(parsed_output[0]['archives']) == 1
|
||||
|
||||
# Also exercise the info flag.
|
||||
output = subprocess.check_output(
|
||||
'borgmatic --config {} --info --json'.format(config_path).split(' ')
|
||||
).decode(sys.stdout.encoding)
|
||||
parsed_output = json.loads(output)
|
||||
|
||||
assert len(parsed_output) == 1
|
||||
assert 'repository' in parsed_output[0]
|
||||
finally:
|
||||
shutil.rmtree(temporary_directory)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import subprocess
|
||||
|
||||
from flexmock import flexmock
|
||||
import pytest
|
||||
|
||||
@@ -16,16 +18,6 @@ def test_parse_arguments_with_no_arguments_uses_defaults():
|
||||
assert parser.json is False
|
||||
|
||||
|
||||
def test_parse_arguments_with_path_arguments_overrides_defaults():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
parser = module.parse_arguments('--config', 'myconfig', '--excludes', 'myexcludes')
|
||||
|
||||
assert parser.config_paths == ['myconfig']
|
||||
assert parser.excludes_filename == 'myexcludes'
|
||||
assert parser.verbosity is 0
|
||||
|
||||
|
||||
def test_parse_arguments_with_multiple_config_paths_parses_as_list():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
@@ -35,7 +27,7 @@ def test_parse_arguments_with_multiple_config_paths_parses_as_list():
|
||||
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']
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(config_paths)
|
||||
|
||||
@@ -46,7 +38,7 @@ def test_parse_arguments_with_verbosity_flag_overrides_default():
|
||||
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')
|
||||
assert parser.json is True
|
||||
|
||||
@@ -88,16 +80,101 @@ def test_parse_arguments_with_invalid_arguments_exits():
|
||||
module.parse_arguments('--posix-me-harder')
|
||||
|
||||
|
||||
def test_parse_arguments_with_json_flag_with_list_or_info_flag_does_not_raise_any_error():
|
||||
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_allows_init_and_prune():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
module.parse_arguments('--config', 'myconfig', '--init', '--encryption', 'repokey', '--prune')
|
||||
|
||||
|
||||
def test_parse_arguments_allows_init_and_create():
|
||||
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
|
||||
|
||||
module.parse_arguments('--config', 'myconfig', '--init', '--encryption', 'repokey', '--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', '--encryption', 'repokey', '--dry-run'
|
||||
)
|
||||
|
||||
|
||||
def test_parse_arguments_allows_progress_and_create():
|
||||
module.parse_arguments('--progress', '--create', '--list')
|
||||
|
||||
|
||||
def test_parse_arguments_disallows_progress_without_create():
|
||||
with pytest.raises(ValueError):
|
||||
module.parse_arguments('--progress', '--list')
|
||||
|
||||
|
||||
def test_parse_arguments_with_stats_and_create_flags_does_not_raise():
|
||||
module.parse_arguments('--stats', '--create', '--list')
|
||||
|
||||
|
||||
def test_parse_arguments_with_stats_and_prune_flags_does_not_raise():
|
||||
module.parse_arguments('--stats', '--prune', '--list')
|
||||
|
||||
|
||||
def test_parse_arguments_with_stats_flag_but_no_create_or_prune_flag_raises_value_error():
|
||||
with pytest.raises(ValueError):
|
||||
module.parse_arguments('--stats', '--list')
|
||||
|
||||
|
||||
def test_parse_arguments_allows_json_with_list_or_info():
|
||||
module.parse_arguments('--list', '--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):
|
||||
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):
|
||||
module.parse_arguments('--list', '--info', '--json')
|
||||
|
||||
|
||||
def test_borgmatic_version_matches_news_version():
|
||||
borgmatic_version = subprocess.check_output(('borgmatic', '--version')).decode('ascii')
|
||||
news_version = open('NEWS').readline()
|
||||
|
||||
assert borgmatic_version == news_version
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import subprocess
|
||||
|
||||
|
||||
def test_setup_version_matches_news_version():
|
||||
setup_version = subprocess.check_output(('python', 'setup.py', '--version')).decode('ascii')
|
||||
news_version = open('NEWS').readline()
|
||||
|
||||
assert setup_version == news_version
|
||||
@@ -1,5 +1,4 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
from flexmock import flexmock
|
||||
|
||||
@@ -7,52 +6,6 @@ from borgmatic.borg import create as module
|
||||
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():
|
||||
flexmock(module.os.path).should_receive('expanduser').and_return('foo')
|
||||
flexmock(module.glob).should_receive('glob').and_return([])
|
||||
@@ -266,7 +219,7 @@ def test_create_archive_with_log_info_calls_borg_with_info_parameter():
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
insert_subprocess_mock(CREATE_COMMAND + ('--list', '--filter', 'AME', '--info', '--stats'))
|
||||
insert_subprocess_mock(CREATE_COMMAND + ('--list', '--filter', 'AME-', '--info', '--stats'))
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
module.create_archive(
|
||||
@@ -287,7 +240,7 @@ def test_create_archive_with_log_debug_calls_borg_with_debug_parameter():
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
insert_subprocess_mock(
|
||||
CREATE_COMMAND + ('--list', '--filter', 'AME', '--stats', '--debug', '--show-rc')
|
||||
CREATE_COMMAND + ('--list', '--filter', 'AME-', '--stats', '--debug', '--show-rc')
|
||||
)
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
@@ -331,7 +284,7 @@ def test_create_archive_with_dry_run_and_log_info_calls_borg_without_stats_param
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
insert_subprocess_mock(CREATE_COMMAND + ('--list', '--filter', 'AME', '--info', '--dry-run'))
|
||||
insert_subprocess_mock(CREATE_COMMAND + ('--list', '--filter', 'AME-', '--info', '--dry-run'))
|
||||
insert_logging_mock(logging.INFO)
|
||||
|
||||
module.create_archive(
|
||||
@@ -355,7 +308,7 @@ def test_create_archive_with_dry_run_and_log_debug_calls_borg_without_stats_para
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
insert_subprocess_mock(
|
||||
CREATE_COMMAND + ('--list', '--filter', 'AME', '--debug', '--show-rc', '--dry-run')
|
||||
CREATE_COMMAND + ('--list', '--filter', 'AME-', '--debug', '--show-rc', '--dry-run')
|
||||
)
|
||||
insert_logging_mock(logging.DEBUG)
|
||||
|
||||
@@ -390,6 +343,25 @@ def test_create_archive_with_checkpoint_interval_calls_borg_with_checkpoint_inte
|
||||
)
|
||||
|
||||
|
||||
def test_create_archive_with_chunker_params_calls_borg_with_chunker_params_parameters():
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar')).and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
flexmock(module).should_receive('_make_pattern_flags').and_return(())
|
||||
flexmock(module).should_receive('_make_exclude_flags').and_return(())
|
||||
insert_subprocess_mock(CREATE_COMMAND + ('--chunker-params', '1,2,3,4'))
|
||||
|
||||
module.create_archive(
|
||||
dry_run=False,
|
||||
repository='repo',
|
||||
location_config={
|
||||
'source_directories': ['foo', 'bar'],
|
||||
'repositories': ['repo'],
|
||||
'exclude_patterns': None,
|
||||
},
|
||||
storage_config={'chunker_params': '1,2,3,4'},
|
||||
)
|
||||
|
||||
|
||||
def test_create_archive_with_compression_calls_borg_with_compression_parameters():
|
||||
flexmock(module).should_receive('_expand_directories').and_return(('foo', 'bar')).and_return(())
|
||||
flexmock(module).should_receive('_write_pattern_file').and_return(None)
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,79 @@
|
||||
import logging
|
||||
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.borg import init as module
|
||||
from ..test_verbosity import insert_logging_mock
|
||||
|
||||
|
||||
INFO_REPOSITORY_EXISTS_RESPONSE_CODE = 0
|
||||
INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE = 2
|
||||
INIT_COMMAND = ('borg', 'init', 'repo', '--encryption', 'repokey')
|
||||
|
||||
|
||||
def insert_info_command_mock(info_response):
|
||||
subprocess = flexmock(module.subprocess)
|
||||
subprocess.should_receive('call').and_return(info_response)
|
||||
|
||||
|
||||
def insert_init_command_mock(init_command, **kwargs):
|
||||
subprocess = flexmock(module.subprocess)
|
||||
subprocess.should_receive('check_call').with_args(init_command, **kwargs).once()
|
||||
|
||||
|
||||
def test_initialize_repository_calls_borg_with_parameters():
|
||||
insert_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE)
|
||||
insert_init_command_mock(INIT_COMMAND)
|
||||
|
||||
module.initialize_repository(repository='repo', encryption_mode='repokey')
|
||||
|
||||
|
||||
def test_initialize_repository_skips_initialization_when_repository_already_exists():
|
||||
insert_info_command_mock(INFO_REPOSITORY_EXISTS_RESPONSE_CODE)
|
||||
flexmock(module.subprocess).should_receive('check_call').never()
|
||||
|
||||
module.initialize_repository(repository='repo', encryption_mode='repokey')
|
||||
|
||||
|
||||
def test_initialize_repository_with_append_only_calls_borg_with_append_only_parameter():
|
||||
insert_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE)
|
||||
insert_init_command_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_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE)
|
||||
insert_init_command_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_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE)
|
||||
insert_init_command_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_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE)
|
||||
insert_init_command_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_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE)
|
||||
insert_init_command_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_info_command_mock(INFO_REPOSITORY_NOT_FOUND_RESPONSE_CODE)
|
||||
insert_init_command_mock(INIT_COMMAND + ('--remote-path', 'borg1'))
|
||||
|
||||
module.initialize_repository(repository='repo', encryption_mode='repokey', remote_path='borg1')
|
||||
@@ -3,12 +3,12 @@ import sys
|
||||
|
||||
from flexmock import flexmock
|
||||
|
||||
from borgmatic.commands import borgmatic
|
||||
from borgmatic.commands import borgmatic as module
|
||||
|
||||
|
||||
def test__run_commands_handles_multiple_json_outputs_in_array():
|
||||
def test_run_commands_handles_multiple_json_outputs_in_array():
|
||||
(
|
||||
flexmock(borgmatic)
|
||||
flexmock(module)
|
||||
.should_receive('_run_commands_on_repository')
|
||||
.times(3)
|
||||
.replace_with(
|
||||
@@ -36,7 +36,7 @@ def test__run_commands_handles_multiple_json_outputs_in_array():
|
||||
)
|
||||
)
|
||||
|
||||
borgmatic._run_commands(
|
||||
module._run_commands(
|
||||
args=flexmock(json=True),
|
||||
consistency=None,
|
||||
local_path=None,
|
||||
@@ -45,3 +45,29 @@ def test__run_commands_handles_multiple_json_outputs_in_array():
|
||||
retention=None,
|
||||
storage=None,
|
||||
)
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_logs_info_for_success():
|
||||
flexmock(module).should_receive('run_configuration')
|
||||
|
||||
logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=()))
|
||||
|
||||
assert any(log for log in logs if log.levelno == module.logging.INFO)
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_logs_critical_for_error():
|
||||
flexmock(module).should_receive('run_configuration').and_raise(ValueError)
|
||||
|
||||
logs = tuple(module.collect_configuration_run_summary_logs(('test.yaml',), args=()))
|
||||
|
||||
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
|
||||
|
||||
|
||||
def test_collect_configuration_run_summary_logs_critical_for_missing_configs():
|
||||
logs = tuple(
|
||||
module.collect_configuration_run_summary_logs(
|
||||
config_filenames=(), args=flexmock(config_paths=())
|
||||
)
|
||||
)
|
||||
|
||||
assert any(log for log in logs if log.levelno == module.logging.CRITICAL)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
from borgmatic.config import checks as module
|
||||
|
||||
|
||||
def test_repository_enabled_for_checks_defaults_to_enabled_for_all_repositories():
|
||||
enabled = module.repository_enabled_for_checks('repo.borg', consistency={})
|
||||
|
||||
assert enabled
|
||||
|
||||
|
||||
def test_repository_enabled_for_checks_is_enabled_for_specified_repositories():
|
||||
enabled = module.repository_enabled_for_checks(
|
||||
'repo.borg', consistency={'check_repositories': ['repo.borg', 'other.borg']}
|
||||
)
|
||||
|
||||
assert enabled
|
||||
|
||||
|
||||
def test_repository_enabled_for_checks_is_disabled_for_other_repositories():
|
||||
enabled = module.repository_enabled_for_checks(
|
||||
'repo.borg', consistency={'check_repositories': ['other.borg']}
|
||||
)
|
||||
|
||||
assert not enabled
|
||||
@@ -51,6 +51,29 @@ def test_apply_logical_validation_warns_if_archive_name_format_present_without_c
|
||||
)
|
||||
|
||||
|
||||
def test_apply_locical_validation_raises_if_unknown_repository_in_check_repositories():
|
||||
with pytest.raises(module.Validation_error):
|
||||
module.apply_logical_validation(
|
||||
'config.yaml',
|
||||
{
|
||||
'location': {'repositories': ['repo.borg', 'other.borg']},
|
||||
'retention': {'keep_secondly': 1000},
|
||||
'consistency': {'check_repositories': ['repo.borg', 'unknown.borg']},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_apply_locical_validation_does_not_raise_if_known_repository_in_check_repositories():
|
||||
module.apply_logical_validation(
|
||||
'config.yaml',
|
||||
{
|
||||
'location': {'repositories': ['repo.borg', 'other.borg']},
|
||||
'retention': {'keep_secondly': 1000},
|
||||
'consistency': {'check_repositories': ['repo.borg']},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_apply_logical_validation_does_not_raise_or_warn_if_archive_name_format_and_prefix_present():
|
||||
logger = flexmock(module.logger)
|
||||
logger.should_receive('warning').never()
|
||||
|
||||
@@ -6,9 +6,11 @@ from borgmatic import verbosity as module
|
||||
|
||||
|
||||
def insert_logging_mock(log_level):
|
||||
""" Mocks the isEnabledFor from python logging. """
|
||||
'''
|
||||
Mock the isEnabledFor from Python logging.
|
||||
'''
|
||||
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)
|
||||
|
||||
|
||||
|
||||
@@ -5,10 +5,11 @@ skipsdist = True
|
||||
[testenv]
|
||||
usedevelop = True
|
||||
deps = -rtest_requirements.txt
|
||||
whitelist_externals = sh
|
||||
commands =
|
||||
py.test --cov-report term-missing:skip-covered --cov=borgmatic --ignore=tests/end-to-end \
|
||||
tests []
|
||||
black --skip-string-normalization --line-length 100 --check .
|
||||
sh scripts/black
|
||||
flake8 .
|
||||
|
||||
[testenv:black]
|
||||
|
||||
Reference in New Issue
Block a user