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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 8 additions & 21 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:

asan_ubsan:

runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 25
needs: [lint]

Expand All @@ -73,7 +73,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'
python-version: '3.14'

- name: Install system packages
run: |
Expand Down Expand Up @@ -141,21 +141,13 @@ jobs:
${{ fromJSON(
github.event_name == 'pull_request' && '{
"include": [
{"os": "ubuntu-24.04", "python-version": "3.11", "toxenv": "mypy"},
{"os": "ubuntu-24.04", "python-version": "3.11", "toxenv": "docs"},
{"os": "ubuntu-24.04", "python-version": "3.11", "toxenv": "py311-llfuse"},
{"os": "ubuntu-24.04", "python-version": "3.12", "toxenv": "py312-pyfuse3"},
{"os": "ubuntu-24.04", "python-version": "3.14", "toxenv": "py314-mfusepy"}
{"os": "ubuntu-26.04", "python-version": "3.14", "toxenv": "mypy"},
{"os": "ubuntu-26.04", "python-version": "3.14", "toxenv": "docs"},
{"os": "ubuntu-26.04", "python-version": "3.14", "toxenv": "py314-mfusepy"}
{"os": "macos-15", "python-version": "3.14", "toxenv": "py314-none", "binary": "borg-macos-15-arm64-gh"},
]
}' || '{
"include": [
{"os": "ubuntu-24.04", "python-version": "3.11", "toxenv": "py311-llfuse"},
{"os": "ubuntu-24.04", "python-version": "3.12", "toxenv": "py312-pyfuse3"},
{"os": "ubuntu-24.04", "python-version": "3.13", "toxenv": "py313-mfusepy"},
{"os": "ubuntu-24.04", "python-version": "3.14", "toxenv": "py314-pyfuse3", "binary": "borg-linux-glibc239-x86_64-gh"},
{"os": "ubuntu-24.04-arm", "python-version": "3.14", "toxenv": "py314-pyfuse3", "binary": "borg-linux-glibc239-arm64-gh"},
{"os": "macos-15", "python-version": "3.14", "toxenv": "py314-none", "binary": "borg-macos-15-arm64-gh"},
{"os": "macos-15-intel", "python-version": "3.14", "toxenv": "py314-none", "binary": "borg-macos-15-x86_64-gh"}
]
}'
) }}
Expand Down Expand Up @@ -392,16 +384,11 @@ jobs:
matrix:
include:
- os: freebsd
version: '14.3'
version: '15.0'
display_name: FreeBSD
# Controls binary build and provenance attestation on tags
do_binaries: true
artifact_prefix: borg-freebsd-14-x86_64-gh

- os: netbsd
version: '10.1'
display_name: NetBSD
do_binaries: false
artifact_prefix: borg-freebsd-15-x86_64-gh

- os: openbsd
version: '7.8'
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ concurrency:
jobs:
analyze:
name: Analyze
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 20
permissions:
actions: read
Expand All @@ -53,7 +53,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: 3.11
python-version: 3.14
- name: Cache pip
uses: actions/cache@v5
with:
Expand Down
1 change: 1 addition & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ above.

New features:

- monitor: access encrypted/signed monitoring data in the repository, #9788
- repo-create: split ``--encryption`` into orthogonal options. ``--encryption`` now
selects only the cipher / AE algorithm (``none``, ``authenticated``, ``aes256-ocb``
or ``chacha20-poly1305``), the new ``--id-hash`` selects the id hash function
Expand Down
1 change: 1 addition & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Usage
usage/repo-list
usage/repo-info
usage/repo-delete
usage/monitor
usage/serve
usage/version
usage/compact
Expand Down
7 changes: 7 additions & 0 deletions docs/usage/general/environment.rst.inc
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,13 @@ Directories and files:
- the key file must either exist (for most commands) or will be created (``borg repo-create``).
- you need to give a different path for different repositories.
- you need to point to the correct key file matching the repository the command will operate on.
BORG_MONITORING_KEY
Used by ``borg monitor`` on the monitoring host to verify and decrypt
the reports backup clients published into the repository.
You can get the correct key value by running ``borg monitor --key`` on
a host that has access to the borg key.
This key can not be used to create reports, nor does it grant access
to backup data.
TMPDIR
This is where temporary files are stored (might need a lot of temporary space for some
operations), see tempfile_ for details.
Expand Down
1 change: 1 addition & 0 deletions docs/usage/monitor.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.. include:: monitor.rst.inc
88 changes: 88 additions & 0 deletions docs/usage/monitor.rst.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!

.. _borg_monitor:

borg monitor
------------
.. code-block:: none

borg [common options] monitor [options]

.. only:: html

.. class:: borg-options-table

+-------------------------------------------------------+-----------------------+-----------------------------------------------------------------------------------------------+
| **options** |
+-------------------------------------------------------+-----------------------+-----------------------------------------------------------------------------------------------+
| | ``--key`` | derive and print BORG_MONITORING_KEY for this repository (needs the borg key) |
+-------------------------------------------------------+-----------------------+-----------------------------------------------------------------------------------------------+
| | ``--name SERIES`` | only report on the given archive series (e.g. the name used with borg create) |
+-------------------------------------------------------+-----------------------+-----------------------------------------------------------------------------------------------+
| | ``--command COMMAND`` | only report on the given command, e.g. create or prune (default: all commands) |
+-------------------------------------------------------+-----------------------+-----------------------------------------------------------------------------------------------+
| | ``--host HOSTNAME`` | only report on backups made from the given host |
+-------------------------------------------------------+-----------------------+-----------------------------------------------------------------------------------------------+
| | ``--user USERNAME`` | only report on backups made by the given user |
+-------------------------------------------------------+-----------------------+-----------------------------------------------------------------------------------------------+
| | ``--max-age SECONDS`` | freshness window in seconds; older reports count as stale (default: 90000) |
+-------------------------------------------------------+-----------------------+-----------------------------------------------------------------------------------------------+
| | ``--keep N`` | after reading, delete all but the N newest report objects (0 = do not clean up; default: 500) |
+-------------------------------------------------------+-----------------------+-----------------------------------------------------------------------------------------------+
| | ``--json`` | format output as JSON |
+-------------------------------------------------------+-----------------------+-----------------------------------------------------------------------------------------------+
| .. class:: borg-common-opt-ref |
| |
| :ref:`common_options` |
+-------------------------------------------------------+-----------------------+-----------------------------------------------------------------------------------------------+

.. raw:: html

<script type='text/javascript'>
$(document).ready(function () {
$('.borg-options-table colgroup').remove();
})
</script>

.. only:: latex



options
--key derive and print BORG_MONITORING_KEY for this repository (needs the borg key)
--name SERIES only report on the given archive series (e.g. the name used with borg create)
--command COMMAND only report on the given command, e.g. create or prune (default: all commands)
--host HOSTNAME only report on backups made from the given host
--user USERNAME only report on backups made by the given user
--max-age SECONDS freshness window in seconds; older reports count as stale (default: 90000)
--keep N after reading, delete all but the N newest report objects (0 = do not clean up; default: 500)
--json format output as JSON


:ref:`common_options`
|

Description
~~~~~~~~~~~

Read trusted monitoring state of a repository.

Borg client commands publish a signed-and-encrypted state report into the
repository after each run. Only borg monitor can read these reports using
the monitoring key.

Setup (once, on a host that has the borg key)::

borg monitor --key # this outputs the monitoring key

Then, on the monitoring host::

BORG_MONITORING_KEY=<that key> borg monitor

This verifies and decrypts the reports and prints, for each distinct backup job
(host, user, command, archive series), the latest status and its age. It exits with
a non-zero code (warning or error) if any job is missing, stale (older than
--max-age), unsigned, or did not indicate success - so it can drive alerting like a
dead man's switch. Use --name, --command, --host and --user to restrict the output.
Reports accumulate over time; --keep=N (default 500) deletes all but the N newest
after reading.
3 changes: 3 additions & 0 deletions src/borg/archiver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
from .recreate_cmd import RecreateMixIn
from .rename_cmd import RenameMixIn
from .repo_create_cmd import RepoCreateMixIn
from .monitor_cmd import MonitorMixIn
from .repo_info_cmd import RepoInfoMixIn
from .repo_delete_cmd import RepoDeleteMixIn
from .repo_list_cmd import RepoListMixIn
Expand Down Expand Up @@ -112,6 +113,7 @@ class Archiver(
KeysMixIn,
ListMixIn,
LocksMixIn,
MonitorMixIn,
MountMixIn,
PruneMixIn,
RecreateMixIn,
Expand Down Expand Up @@ -301,6 +303,7 @@ def build_parser(self):
self.build_parser_keys(subparsers, common_parser, mid_common_parser)
self.build_parser_list(subparsers, common_parser, mid_common_parser)
self.build_parser_locks(subparsers, common_parser, mid_common_parser)
self.build_parser_monitor(subparsers, common_parser, mid_common_parser)
self.build_parser_mount_umount(subparsers, common_parser, mid_common_parser)
self.build_parser_prune(subparsers, common_parser, mid_common_parser)
self.build_parser_repo_create(subparsers, common_parser, mid_common_parser)
Expand Down
14 changes: 13 additions & 1 deletion src/borg/archiver/create_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from ._common import with_repository, Highlander
from .. import helpers
from .. import monitoring
from ..archive import Archive, is_special, SF_DATALESS
from ..archive import BackupError, BackupOSError, BackupItemExcluded, backup_io, OsOpen, stat_update_check
from ..archive import FilesystemObjectProcessors, MetadataCollector, ChunksProcessor
Expand Down Expand Up @@ -287,7 +288,18 @@ def create_inner(archive, cache, fso):
files_changed=args.files_changed,
)
create_inner(archive, cache, fso)
else:

monitoring.publish_command_report(
repository,
manifest.key,
"create",
hostname=archive.hostname,
username=archive.username,
archive=archive.name,
archive_id=archive.id,
stats=archive.stats.as_dict(),
)
else: # dry-run
create_inner(None, None, None)

def _process_any(self, *, path, parent_fd, name, st, fso, cache, read_special, dry_run, strip_prefix):
Expand Down
14 changes: 11 additions & 3 deletions src/borg/archiver/delete_cmd.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging

from ._common import with_repository
from .. import monitoring
from ..constants import * # NOQA
from ..helpers import format_archive, CommandError, bin_to_hex, archivename_validator
from ..helpers.argparsing import ArgumentParser
Expand Down Expand Up @@ -32,7 +33,7 @@ def do_delete(self, args, repository):
"or just delete the whole repository (might be much faster)."
)

deleted = False
deleted_count = 0
logger_list = logging.getLogger("borg.output.list")
for i, archive_info in enumerate(archive_infos, 1):
name, id, hex_id = archive_info.name, archive_info.id, bin_to_hex(archive_info.id)
Expand All @@ -46,17 +47,24 @@ def do_delete(self, args, repository):
except KeyError:
self.print_warning(f"Archive {name} {hex_id} not found ({i}/{count}).")
else:
deleted = True
deleted_count += 1
if self.output_list:
msg = "Would delete: {} ({}/{})" if dry_run else "Deleted archive: {} ({}/{})"
logger_list.info(msg.format(archive_formatted, i, count))
if dry_run:
logger.info("Finished dry-run.")
elif deleted:
elif deleted_count:
manifest.write()
self.print_warning('Done. Run "borg compact" to free space.', wc=None)
else:
self.print_warning("Aborted.", wc=None)
if not dry_run:
monitoring.publish_command_report(
repository,
manifest.key,
"delete",
stats={"archives_deleted": deleted_count, "archives_considered": count},
)
return

def build_parser_delete(self, subparsers, common_parser, mid_common_parser):
Expand Down
Loading
Loading