Browse Source

Split out example configuration into different pages of reference documentation (#942).

Dan Helfman 3 weeks ago
parent
commit
efc4316a45
30 changed files with 488 additions and 143 deletions
  1. 13 6
      borgmatic/actions/config/generate.py
  2. 7 2
      borgmatic/commands/arguments.py
  3. 40 9
      borgmatic/config/generate.py
  4. 8 8
      borgmatic/config/schema.yaml
  5. 2 2
      docs/Dockerfile
  6. 2 1
      docs/_includes/header.njk
  7. 4 5
      docs/docker-compose.yaml
  8. 11 70
      docs/how-to/backup-your-databases.md
  9. 0 2
      docs/how-to/extract-a-backup.md
  10. 1 1
      docs/reference/configuration/credentials/container.md
  11. 10 0
      docs/reference/configuration/data-sources/btrfs.md
  12. 27 0
      docs/reference/configuration/data-sources/index.md
  13. 10 0
      docs/reference/configuration/data-sources/lvm.md
  14. 23 0
      docs/reference/configuration/data-sources/mariadb.md
  15. 21 0
      docs/reference/configuration/data-sources/mongodb.md
  16. 21 0
      docs/reference/configuration/data-sources/mysql.md
  17. 21 0
      docs/reference/configuration/data-sources/postgresql.md
  18. 22 0
      docs/reference/configuration/data-sources/sqlite.md
  19. 10 0
      docs/reference/configuration/data-sources/zfs.md
  20. 7 0
      docs/reference/configuration/monitoring/apprise.md
  21. 7 0
      docs/reference/configuration/monitoring/healthchecks.md
  22. 12 15
      docs/reference/configuration/monitoring/ntfy.md
  23. 0 2
      docs/reference/configuration/monitoring/pagerduty.md
  24. 8 0
      docs/reference/configuration/monitoring/pushover.md
  25. 2 2
      docs/reference/configuration/monitoring/sentry.md
  26. 7 0
      docs/reference/configuration/monitoring/uptime-kuma.md
  27. 8 9
      docs/reference/configuration/monitoring/zabbix.md
  28. 41 0
      docs/reference/configuration/runtime-directory.md
  29. 124 6
      tests/integration/config/test_generate.py
  30. 19 3
      tests/unit/actions/config/test_generate.py

+ 13 - 6
borgmatic/actions/config/generate.py

@@ -19,26 +19,33 @@ def run_generate(generate_arguments, global_arguments):
     dry_run_label = ' (dry run; not actually writing anything)' if global_arguments.dry_run else ''
 
     logger.answer(
-        f'Generating a configuration file at: {generate_arguments.destination_filename}{dry_run_label}',
+        f'Generating configuration files within: {generate_arguments.destination_path}{dry_run_label}'
+        if generate_arguments.split
+        else f'Generating a configuration file at: {generate_arguments.destination_path}{dry_run_label}'
     )
 
     borgmatic.config.generate.generate_sample_configuration(
         global_arguments.dry_run,
         generate_arguments.source_filename,
-        generate_arguments.destination_filename,
+        generate_arguments.destination_path,
         borgmatic.config.validate.schema_filename(),
         overwrite=generate_arguments.overwrite,
+        split=generate_arguments.split,
     )
 
     if generate_arguments.source_filename:
         logger.answer(
             f'''
-Merged in the contents of configuration file at: {generate_arguments.source_filename}
-To review the changes made, run:
-
-    diff --unified {generate_arguments.source_filename} {generate_arguments.destination_filename}''',
+Merged in the contents of configuration file at: {generate_arguments.source_filename}'''
         )
 
+        if not generate_arguments.split:
+            logger.answer(
+                '''To review the changes made, run:
+
+    diff --unified {generate_arguments.source_filename} {generate_arguments.destination_path}''',
+            )
+
     logger.answer(
         '''
 This includes all available configuration options with example values, the few

+ 7 - 2
borgmatic/commands/arguments.py

@@ -1213,9 +1213,9 @@ def make_parsers(schema, unparsed_arguments):  # noqa: PLR0915
     config_generate_group.add_argument(
         '-d',
         '--destination',
-        dest='destination_filename',
+        dest='destination_path',
         default=config_paths[0],
-        help=f'Destination configuration file, default: {unexpanded_config_paths[0]}',
+        help=f'Destination configuration file (or directory if using --split), default: {unexpanded_config_paths[0]}',
     )
     config_generate_group.add_argument(
         '--overwrite',
@@ -1223,6 +1223,11 @@ def make_parsers(schema, unparsed_arguments):  # noqa: PLR0915
         action='store_true',
         help='Whether to overwrite any existing destination file, defaults to false',
     )
+    config_generate_group.add_argument(
+        '--split',
+        action='store_true',
+        help='Assuming the destination is a directory instead of a file, split the configuration into separate files within it, one per option, useful for documentation',
+    )
     config_generate_group.add_argument(
         '-h',
         '--help',

+ 40 - 9
borgmatic/config/generate.py

@@ -107,7 +107,7 @@ def comment_out_line(line):
     return '# '.join((indent_spaces, line[count_indent_spaces:]))
 
 
-def comment_out_optional_configuration(rendered_config):
+def transform_optional_configuration(rendered_config, comment_out=True):
     '''
     Post-process a rendered configuration string to comment out optional key/values, as determined
     by a sentinel in the comment before each key.
@@ -117,6 +117,9 @@ def comment_out_optional_configuration(rendered_config):
 
     Ideally ruamel.yaml would support commenting out keys during configuration generation, but it's
     not terribly easy to accomplish that way.
+
+    If comment_out is False, then just strip the comment sentinel without actually commenting
+    anything out.
     '''
     lines = []
     optional = False
@@ -129,6 +132,9 @@ def comment_out_optional_configuration(rendered_config):
         # Upon encountering an optional configuration option, comment out lines until the next blank
         # line.
         if line.strip().startswith(f'# {COMMENTED_OUT_SENTINEL}'):
+            if comment_out is False:
+                continue
+
             optional = True
             indent_characters_at_sentinel = indent_characters
             continue
@@ -313,16 +319,18 @@ def merge_source_configuration_into_destination(destination_config, source_confi
 def generate_sample_configuration(
     dry_run,
     source_filename,
-    destination_filename,
+    destination_path,
     schema_filename,
     overwrite=False,
+    split=False,
 ):
     '''
-    Given an optional source configuration filename, and a required destination configuration
-    filename, the path to a schema filename in a YAML rendition of the JSON Schema format, and
-    whether to overwrite a destination file, write out a sample configuration file based on that
-    schema. If a source filename is provided, merge the parsed contents of that configuration into
-    the generated configuration.
+    Given an optional source configuration filename, a required destination configuration path, the
+    path to a schema filename in a YAML rendition of the JSON Schema format, whether to overwrite a
+    destination file, and whether to split the configuration into multiple files (one per option) in
+    the assumed destination directory, write out sample configuration file(s) based on that schema.
+    If a source filename is provided, merge the parsed contents of that configuration into the
+    generated configuration.
     '''
     schema = ruamel.yaml.YAML(typ='safe').load(open(schema_filename, encoding='utf-8'))
     source_config = None
@@ -345,8 +353,31 @@ def generate_sample_configuration(
     if dry_run:
         return
 
+    if split:
+        if os.path.exists(destination_path) and not os.path.isdir(destination_path):
+            raise ValueError('With the --split flag, the destination path must be a directory')
+
+        os.makedirs(destination_path, exist_ok=True)
+
+        for option_name, option_config in destination_config.items():
+            write_configuration(
+                os.path.join(destination_path, f'{option_name}.yaml'),
+                transform_optional_configuration(
+                    render_configuration({option_name: option_config}),
+                    comment_out=False,
+                ),
+                overwrite=overwrite,
+            )
+
+        return
+
+    if os.path.exists(destination_path) and not os.path.isfile(destination_path):
+        raise ValueError('Without the --split flag, the destination path must be a file')
+
     write_configuration(
-        destination_filename,
-        comment_out_optional_configuration(render_configuration(destination_config)),
+        destination_path,
+        transform_optional_configuration(
+            render_configuration(destination_config), comment_out=True
+        ),
         overwrite=overwrite,
     )

+ 8 - 8
borgmatic/config/schema.yaml

@@ -2139,17 +2139,17 @@ properties:
                         type: string
                         description: |
                             The message body to publish.
-                        example: Your backups have failed.
+                        example: Your backups have started.
                     priority:
                         type: string
                         description: |
                             The priority to set.
-                        example: urgent
+                        example: min
                     tags:
                         type: string
                         description: |
                             Tags to attach to the message.
-                        example: incoming_envelope
+                        example: borgmatic
             finish:
                 type: object
                 additionalProperties: false
@@ -2163,17 +2163,17 @@ properties:
                         type: string
                         description: |
                             The message body to publish.
-                        example: Your backups have failed.
+                        example: Your backups have finished.
                     priority:
                         type: string
                         description: |
                             The priority to set.
-                        example: urgent
+                        example: min
                     tags:
                         type: string
                         description: |
                             Tags to attach to the message.
-                        example: incoming_envelope
+                        example: borgmatic,+1
             fail:
                 type: object
                 additionalProperties: false
@@ -2192,12 +2192,12 @@ properties:
                         type: string
                         description: |
                             The priority to set.
-                        example: urgent
+                        example: max
                     tags:
                         type: string
                         description: |
                             Tags to attach to the message.
-                        example: incoming_envelope
+                        example: borgmatic,-1,skull
             states:
                 type: array
                 items:

+ 2 - 2
docs/Dockerfile

@@ -2,7 +2,7 @@ FROM docker.io/alpine:3.20.1 AS borgmatic
 
 COPY . /app
 RUN apk add --no-cache py3-pip py3-ruamel.yaml py3-ruamel.yaml.clib
-RUN pip install --break-system-packages --no-cache /app && borgmatic config generate && chmod +r /etc/borgmatic/config.yaml
+RUN pip install --break-system-packages --no-cache /app && borgmatic config generate && borgmatic config generate --destination /etc/borgmatic --split && chmod +r /etc/borgmatic/*.yaml
 RUN borgmatic --help > /command-line.txt \
     && for action in repo-create transfer create prune compact check delete extract config "config bootstrap" "config generate" "config validate" export-tar mount umount repo-delete restore repo-list list repo-info info break-lock "key export" "key import" "key change-passphrase" recreate borg; do \
            echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \
@@ -23,7 +23,7 @@ RUN npm install @11ty/eleventy \
     markdown-it \
     markdown-it-anchor \
     markdown-it-replace-link
-COPY --from=borgmatic /etc/borgmatic/config.yaml /source/docs/_includes/borgmatic/config.yaml
+COPY --from=borgmatic /etc/borgmatic/* /source/docs/_includes/borgmatic/
 COPY --from=borgmatic /command-line.txt /source/docs/_includes/borgmatic/command-line.txt
 COPY --from=borgmatic /contributors.html /source/docs/_includes/borgmatic/contributors.html
 COPY . /source

+ 2 - 1
docs/_includes/header.njk

@@ -2,7 +2,8 @@
     {% if page.url != '/' %}<h3><a href="https://torsion.org/borgmatic/">borgmatic</a></h3>{% endif %}
     <div class="container" id="breadcrumb">
         {% set breadcrumb = collections.all | eleventyNavigationBreadcrumb(eleventyNavigation.key, {allowMissing: true}) %}
-        {{ breadcrumb | eleventyNavigationToHtml | safe }}
+        {# The replace() is a work-around for https://github.com/11ty/eleventy-navigation/issues/56 #}
+        {{ breadcrumb | eleventyNavigationToHtml | replace('href="/reference/', 'href="/borgmatic/reference/') | safe }}
     </div>
     <h1 class="elv-hed">{{ title | safe }}</h1>
     {% if page.url == '/' %}<h3>It's your data. Keep it that way.</h3>{% endif %}

+ 4 - 5
docs/docker-compose.yaml

@@ -23,14 +23,13 @@ services:
     labels:
       - "traefik.enable=true"
       - "traefik.http.routers.borgmatic-docs.rule=PathPrefix(`/borgmatic`)"
-#      - "traefik.http.routers.borgmatic-docs.middlewares=borgmatic-trailing-slash-redirectregex,borgmatic-docs-redirectregex,borgmatic-stripprefix"
-      - "traefik.http.routers.borgmatic-docs.middlewares=borgmatic-trailing-slash-redirectregex,borgmatic-stripprefix"
+      - "traefik.http.routers.borgmatic-docs.middlewares=borgmatic-trailing-slash-redirectregex,borgmatic-docs-redirectregex,borgmatic-stripprefix"
       - "traefik.http.middlewares.borgmatic-trailing-slash-redirectregex.redirectregex.regex=^(.*)/borgmatic$$"
       - "traefik.http.middlewares.borgmatic-trailing-slash-redirectregex.redirectregex.replacement=$${1}/borgmatic/"
       - "traefik.http.middlewares.borgmatic-trailing-slash-redirectregex.redirectregex.permanent=true"
-#      - "traefik.http.middlewares.borgmatic-docs-redirectregex.redirectregex.regex=^(.*)/borgmatic/docs/(.*)$$"
-#      - "traefik.http.middlewares.borgmatic-docs-redirectregex.redirectregex.replacement=$${1}/borgmatic/$${2}"
-#      - "traefik.http.middlewares.borgmatic-docs-redirectregex.redirectregex.permanent=true"
+      - "traefik.http.middlewares.borgmatic-docs-redirectregex.redirectregex.regex=^(.*)/borgmatic/docs/(.*)$$"
+      - "traefik.http.middlewares.borgmatic-docs-redirectregex.redirectregex.replacement=$${1}/borgmatic/$${2}"
+      - "traefik.http.middlewares.borgmatic-docs-redirectregex.redirectregex.permanent=true"
       - "traefik.http.middlewares.borgmatic-stripprefix.stripprefix.prefixes=/borgmatic"
       - "traefik.http.routers.borgmatic-docs.entrypoints=web"
     build:

+ 11 - 70
docs/how-to/backup-your-databases.md

@@ -27,33 +27,6 @@ mysql_databases:
 these and other database options in the `hooks:` section of your
 configuration.
 
-<span class="minilink minilink-addedin">New in version 1.5.22</span> You can
-also dump MongoDB databases. For example:
-
-```yaml
-mongodb_databases:
-    - name: messages
-```
-
-<span class="minilink minilink-addedin">New in version 1.7.9</span>
-Additionally, you can dump SQLite databases. For example:
-
-```yaml
-sqlite_databases:
-    - name: mydb
-      path: /var/lib/sqlite3/mydb.sqlite
-```
-
-<span class="minilink minilink-addedin">New in version 1.8.2</span> If you're
-using MariaDB, use the MariaDB database hook instead of `mysql_databases:` as
-the MariaDB hook calls native MariaDB commands instead of the deprecated MySQL
-ones. For instance:
-
-```yaml
-mariadb_databases:
-    - name: comments
-```
-
 As part of each backup, borgmatic streams a database dump for each configured
 database directly to Borg, so it's included in the backup without consuming
 additional disk space. (The exceptions are the PostgreSQL/MongoDB `directory`
@@ -107,47 +80,17 @@ sqlite_databases:
       path: /var/lib/sqlite3/mydb.sqlite
 ```
 
-See your [borgmatic configuration
-file](https://torsion.org/borgmatic/reference/configuration/) for
-additional customization of the options passed to database commands (when
-listing databases, restoring databases, etc.).
-
-
-### Runtime directory
-
-<span class="minilink minilink-addedin">New in version 1.9.0</span> To support
-streaming database dumps to Borg, borgmatic uses a runtime directory for
-temporary file storage, probing the following locations (in order) to find it:
-
- 1. The `user_runtime_directory` borgmatic configuration option.
- 2. The `XDG_RUNTIME_DIR` environment variable, usually `/run/user/$UID`
-    (where `$UID` is the current user's ID), automatically set by PAM on Linux
-    for a user with a session.
- 3. <span class="minilink minilink-addedin">New in version 1.9.2</span>The
-    `RUNTIME_DIRECTORY` environment variable, set by systemd if
-    `RuntimeDirectory=borgmatic` is added to borgmatic's systemd service file.
- 4. <span class="minilink minilink-addedin">New in version 1.9.1</span>The
-    `TMPDIR` environment variable, set on macOS for a user with a session,
-    among other operating systems.
- 5. <span class="minilink minilink-addedin">New in version 1.9.1</span>The
-    `TEMP` environment variable, set on various systems.
- 6. <span class="minilink minilink-addedin">New in version 1.9.2</span>
-    Hard-coded `/tmp`. <span class="minilink minilink-addedin">Prior to
-    version 1.9.2</span>This was instead hard-coded to `/run/user/$UID`.
-
-You can see the runtime directory path that borgmatic selects by running with
-`--verbosity 2` and looking for "Using runtime directory" in the output.
-
-Regardless of the runtime directory selected, borgmatic stores its files
-within a `borgmatic` subdirectory of the runtime directory. Additionally, in
-the case of `TMPDIR`, `TEMP`, and the hard-coded `/tmp`, borgmatic creates a
-randomly named subdirectory in an effort to reduce path collisions in shared
-system temporary directories.
-
-<span class="minilink minilink-addedin">Prior to version 1.9.0</span>
-borgmatic created temporary streaming database dumps within the `~/.borgmatic`
-directory by default. At that time, the path was configurable by the
-`borgmatic_source_directory` configuration option (now deprecated).
+See the [data sources
+documentation](https://torsion.org/borgmatic/reference/configuration/data-sources/)
+for details on additional options, including customizing the flags passed to
+database commands when listing databases, restoring databases, etc.
+
+<a id="runtime-directory"></a>
+
+To support streaming database dumps to Borg, borgmatic uses a runtime directory
+for temporary file storage. See the [runtime directory
+documentation](https://torsion.org/borgmatic/reference/configuration/runtime-directory/)
+for details.
 
 
 ### All databases
@@ -412,8 +355,6 @@ most up-to-date files and therefore the latest timestamp, run a command like:
 borgmatic restore --archive host-2023-01-02T04:06:07.080910
 ```
 
-(No borgmatic `restore` action? Upgrade borgmatic!)
-
 Or you can simplify this to:
 
 ```bash

+ 0 - 2
docs/how-to/extract-a-backup.md

@@ -30,8 +30,6 @@ and therefore the latest timestamp, run a command like:
 borgmatic extract --archive host-2023-01-02T04:06:07.080910
 ```
 
-(No borgmatic `extract` action? Upgrade borgmatic!)
-
 Or simplify this to:
 
 ```bash

+ 1 - 1
docs/reference/configuration/credentials/container.md

@@ -1,5 +1,5 @@
 ---
-title: Container secerts
+title: Container secrets
 eleventyNavigation:
   key: • Container
   parent: 🔒 Credentials

+ 10 - 0
docs/reference/configuration/data-sources/btrfs.md

@@ -0,0 +1,10 @@
+---
+title: Btrfs
+eleventyNavigation:
+  key: • Btrfs
+  parent: 🗄️ Data sources
+---
+
+```yaml
+{% include borgmatic/btrfs.yaml %}
+```

+ 27 - 0
docs/reference/configuration/data-sources/index.md

@@ -0,0 +1,27 @@
+---
+title: Data sources
+eleventyNavigation:
+  key: 🗄️ Data sources
+  parent: ⚙️  Configuration
+---
+Data sources are built-in borgmatic integrations that, instead of backing up
+plain filesystem data, can pull data directly from database servers and
+filesystem snapshots.
+
+In the case of supported database systems, borgmatic dumps your configured
+databases, streaming them directly to Borg when creating a backup. Here are the
+supported databases and how to configure their borgmatic integrations:
+
+ * [MariaDB](https://torsion.org/borgmatic/reference/configuration/data-sources/mariadb/)
+ * [MongoDB](https://torsion.org/borgmatic/reference/configuration/data-sources/mongodb/)
+ * [MySQL](https://torsion.org/borgmatic/reference/configuration/data-sources/mysql/)
+ * [PostgreSQL](https://torsion.org/borgmatic/reference/configuration/data-sources/postgresql/)
+ * [SQLite](https://torsion.org/borgmatic/reference/configuration/data-sources/sqlite/)
+
+For supported filesystems, borgmatic takes on-demand snapshots of configured
+source directories and feeds them to Borg. Here are the supported filesystems /
+volume managers and how to configure their borgmatic integrations:
+
+ * [Btrfs](https://torsion.org/borgmatic/reference/configuration/data-sources/btrfs/)
+ * [LVM](https://torsion.org/borgmatic/reference/configuration/data-sources/lvm/)
+ * [ZFS](https://torsion.org/borgmatic/reference/configuration/data-sources/zfs/)

+ 10 - 0
docs/reference/configuration/data-sources/lvm.md

@@ -0,0 +1,10 @@
+---
+title: LVM
+eleventyNavigation:
+  key: • LVM
+  parent: 🗄️ Data sources
+---
+
+```yaml
+{% include borgmatic/lvm.yaml %}
+```

+ 23 - 0
docs/reference/configuration/data-sources/mariadb.md

@@ -0,0 +1,23 @@
+---
+title: MariaDB
+eleventyNavigation:
+  key: • MariaDB
+  parent: 🗄️ Data sources
+---
+
+<span class="minilink minilink-addedin">New in version 1.8.2</span> To backup
+MariaDB with borgmatic, use the `mariadb_databases:` hook instead of
+`mysql_databases:` as the MariaDB hook calls native MariaDB commands instead of
+the deprecated MySQL ones. For instance:
+
+```yaml
+mariadb_databases:
+    - name: comments
+```
+
+
+### Full configuration
+
+```yaml
+{% include borgmatic/mariadb_databases.yaml %}
+```

+ 21 - 0
docs/reference/configuration/data-sources/mongodb.md

@@ -0,0 +1,21 @@
+---
+title: MongoDB
+eleventyNavigation:
+  key: • MongoDB
+  parent: 🗄️ Data sources
+---
+
+<span class="minilink minilink-addedin">New in version 1.5.22</span> To backup
+MongoDB with borgmatic, use the `mongodb_databases:` hook.  For example:
+
+```yaml
+mongodb_databases:
+    - name: messages
+```
+
+
+### Full configuration
+
+```yaml
+{% include borgmatic/mongodb_databases.yaml %}
+```

+ 21 - 0
docs/reference/configuration/data-sources/mysql.md

@@ -0,0 +1,21 @@
+---
+title: MySQL
+eleventyNavigation:
+  key: • MySQL
+  parent: 🗄️ Data sources
+---
+
+<span class="minilink minilink-addedin">New in version 1.4.9</span> To backup
+MySQL with borgmatic, use the `mysql_databases:` hook. For instance:
+
+```yaml
+mysql_databases:
+    - name: posts
+```
+
+
+## Full configuration
+
+```yaml
+{% include borgmatic/mysql_databases.yaml %}
+```

+ 21 - 0
docs/reference/configuration/data-sources/postgresql.md

@@ -0,0 +1,21 @@
+---
+title: PostgreSQL
+eleventyNavigation:
+  key: • PostgreSQL
+  parent: 🗄️ Data sources
+---
+
+<span class="minilink minilink-addedin">New in version 1.4.0</span> To backup
+PostgreSQL with borgmatic, use the `postgresql_databases:` hook. For instance:
+
+```yaml
+postgresql_databases:
+    - name: users
+```
+
+
+## Full configuration
+
+```yaml
+{% include borgmatic/postgresql_databases.yaml %}
+```

+ 22 - 0
docs/reference/configuration/data-sources/sqlite.md

@@ -0,0 +1,22 @@
+---
+title: SQLite
+eleventyNavigation:
+  key: • SQLite
+  parent: 🗄️ Data sources
+---
+<span class="minilink minilink-addedin">New in version 1.7.9</span> To backup
+SQLite with borgmatic, use the `sqlite_databases:` hook. For example:
+
+
+```yaml
+sqlite_databases:
+    - name: mydb
+      path: /var/lib/sqlite3/mydb.sqlite
+```
+
+
+## Full configuration
+
+```yaml
+{% include borgmatic/sqlite_databases.yaml %}
+```

+ 10 - 0
docs/reference/configuration/data-sources/zfs.md

@@ -0,0 +1,10 @@
+---
+title: ZFS
+eleventyNavigation:
+  key: • ZFS
+  parent: 🗄️ Data sources
+---
+
+```yaml
+{% include borgmatic/zfs.yaml %}
+```

+ 7 - 0
docs/reference/configuration/monitoring/apprise.md

@@ -104,3 +104,10 @@ This may be necessary for some services that reject large requests.
 See the [configuration
 reference](https://torsion.org/borgmatic/reference/configuration/) for
 details.
+
+
+### Full configuration
+
+```yaml
+{% include borgmatic/apprise.yaml %}
+```

+ 7 - 0
docs/reference/configuration/monitoring/healthchecks.md

@@ -48,3 +48,10 @@ defaults for these flags in your borgmatic configuration via the
 You can configure Healthchecks to notify you by a [variety of
 mechanisms](https://healthchecks.io/#welcome-integrations) when backups fail
 or it doesn't hear from borgmatic for a certain period of time.
+
+
+### Full configuration
+
+```yaml
+{% include borgmatic/healthchecks.yaml %}
+```

+ 12 - 15
docs/reference/configuration/monitoring/ntfy.md

@@ -17,9 +17,7 @@ to issues. The `states` list can override this. Each state can have its own
 custom messages, priorities and tags or, if none are provided, will use the
 default.
 
-An example configuration is shown here with all the available options,
-including [priorities](https://ntfy.sh/docs/publish/#message-priority) and
-[tags](https://ntfy.sh/docs/publish/#tags-emojis):
+Here's a basic configuration that notifies on failure:
 
 ```yaml
 ntfy:
@@ -28,24 +26,12 @@ ntfy:
     username: myuser
     password: secret
 
-    start:
-        title: A borgmatic backup started
-        message: Watch this space...
-        tags: borgmatic
-        priority: min
-    finish:
-        title: A borgmatic backup completed successfully
-        message: Nice!
-        tags: borgmatic,+1
-        priority: min
     fail:
         title: A borgmatic backup failed
         message: You should probably fix it
         tags: borgmatic,-1,skull
         priority: max
     states:
-        - start
-        - finish
         - fail
 ```
 
@@ -62,3 +48,14 @@ ntfy:
     server: https://ntfy.my-domain.com
     access_token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
 ````
+
+
+### Full configuration
+
+Here's an example configuration with all the available options,
+including [priorities](https://ntfy.sh/docs/publish/#message-priority) and
+[tags](https://ntfy.sh/docs/publish/#tags-emojis):
+
+```yaml
+{% include borgmatic/ntfy.yaml %}
+```

+ 0 - 2
docs/reference/configuration/monitoring/pagerduty.md

@@ -65,5 +65,3 @@ pagerduty:
     integration_key: a177cad45bd374409f78906a810a3074
     send_logs: false
 ```
-
-

+ 8 - 0
docs/reference/configuration/monitoring/pushover.md

@@ -52,3 +52,11 @@ pushover:
         expire: 600  # Used only for priority 2. Default is 600 seconds.
         retry: 30  # Used only for priority 2. Default is 30 seconds.
         device: "pixel8"
+```
+
+
+### Full configuration
+
+```yaml
+{% include borgmatic/pushover.yaml %}
+```

+ 2 - 2
docs/reference/configuration/monitoring/sentry.md

@@ -17,7 +17,7 @@ displayed
 environment variable into borgmatic's Sentry `data_source_name_url`
 configuration option. For example:
 
-```
+```yaml
 sentry:
     data_source_name_url: https://5f80ec@o294220.ingest.us.sentry.io/203069
     monitor_slug: mymonitor
@@ -32,7 +32,7 @@ finishes, or fails, but only when any of the `create`, `prune`, `compact`, or
 behavior with the `states` configuration option. For instance, to only ping
 Sentry on failure:
 
-```
+```yaml
 sentry:
     data_source_name_url: https://5f80ec@o294220.ingest.us.sentry.io/203069
     monitor_slug: mymonitor

+ 7 - 0
docs/reference/configuration/monitoring/uptime-kuma.md

@@ -57,3 +57,10 @@ Heartbeat Retry = 360          # = 10 minutes
 # is sent each time.
 Resend Notification every X times = 1
 ```
+
+
+### Full configuration
+
+```yaml
+{% include borgmatic/uptime_kuma.yaml %}
+```

+ 8 - 9
docs/reference/configuration/monitoring/zabbix.md

@@ -16,7 +16,7 @@ states will trigger the hook. The value defined in the configuration of each
 state is used to populate the data of the configured Zabbix item. If none are
 provided, it defaults to a lower-case string of the state.
 
-An example configuration is shown here with all the available options.
+Here's an example configuration:
 
 ```yaml
 zabbix:
@@ -24,21 +24,13 @@ zabbix:
     
     username: myuser
     password: secret
-    api_key: b2ecba64d8beb47fc161ae48b164cfd7104a79e8e48e6074ef5b141d8a0aeeca
 
     host: "borg-server"
     key: borg.status
-    itemid: 55105
 
-    start:
-        value: "STARTED"
-    finish:
-        value: "OK"
     fail:
         value: "ERROR"
     states:
-        - start
-        - finish
         - fail
 ```
 
@@ -69,3 +61,10 @@ is used.
 
 Keep in mind that `host` refers to the "Host name" on the Zabbix server and not
 the "Visual name".
+
+
+### Full configuration
+
+```yaml
+{% include borgmatic/zabbix.yaml %}
+```

+ 41 - 0
docs/reference/configuration/runtime-directory.md

@@ -0,0 +1,41 @@
+---
+title: Runtime directory
+eleventyNavigation:
+  key: 📁 Runtime directory
+  parent: ⚙️  Configuration
+---
+<span class="minilink minilink-addedin">New in version 1.9.0</span> borgmatic
+uses a runtime directory for temporary file storage, such as for streaming
+database dumps to Borg, creating filesystem snapshots, saving bootstrap
+metadata, and so on. To determine the path for this runtime directory, borgmatic
+probes the following values:
+
+ 1. The `user_runtime_directory` borgmatic configuration option.
+ 2. The `XDG_RUNTIME_DIR` environment variable, usually `/run/user/$UID`
+    (where `$UID` is the current user's ID), automatically set by PAM on Linux
+    for a user with a session.
+ 3. <span class="minilink minilink-addedin">New in version 1.9.2</span>The
+    `RUNTIME_DIRECTORY` environment variable, set by systemd if
+    `RuntimeDirectory=borgmatic` is added to borgmatic's systemd service file.
+ 4. <span class="minilink minilink-addedin">New in version 1.9.1</span>The
+    `TMPDIR` environment variable, set on macOS for a user with a session,
+    among other operating systems.
+ 5. <span class="minilink minilink-addedin">New in version 1.9.1</span>The
+    `TEMP` environment variable, set on various systems.
+ 6. <span class="minilink minilink-addedin">New in version 1.9.2</span>
+    Hard-coded `/tmp`. <span class="minilink minilink-addedin">Prior to
+    version 1.9.2</span>This was instead hard-coded to `/run/user/$UID`.
+
+You can see the runtime directory path that borgmatic selects by running with
+`--verbosity 2` and looking for `Using runtime directory` in the output.
+
+Regardless of the runtime directory selected, borgmatic stores its files
+within a `borgmatic` subdirectory of the runtime directory. Additionally, in
+the case of `TMPDIR`, `TEMP`, and the hard-coded `/tmp`, borgmatic creates a
+randomly named subdirectory in an effort to reduce path collisions in shared
+system temporary directories.
+
+<span class="minilink minilink-addedin">Prior to version 1.9.0</span>
+borgmatic created temporary streaming database dumps within the `~/.borgmatic`
+directory by default. At that time, the path was configurable by the
+`borgmatic_source_directory` configuration option (now deprecated).

+ 124 - 6
tests/integration/config/test_generate.py

@@ -195,7 +195,7 @@ def test_comment_out_line_comments_twice_indented_option():
     assert module.comment_out_line(line) == '        # - item'
 
 
-def test_comment_out_optional_configuration_comments_optional_config_only():
+def test_transform_optional_configuration_comments_optional_config_only():
     # The "# COMMENT_OUT" comment is a sentinel used to express that the following key is optional.
     # It's stripped out of the final output.
     flexmock(module).comment_out_line = lambda line: '# ' + line
@@ -236,7 +236,54 @@ repositories:
 # other: thing
     '''
 
-    assert module.comment_out_optional_configuration(config.strip()) == expected_config.strip()
+    assert module.transform_optional_configuration(config.strip()) == expected_config.strip()
+
+
+def test_transform_optional_configuration_with_comment_out_false_leaves_in_optional_config():
+    # The "# COMMENT_OUT" comment is a sentinel used to express that the following key is optional.
+    # It's stripped out of the final output.
+    flexmock(module).comment_out_line = lambda line: '# ' + line
+    config = '''
+# COMMENT_OUT
+foo:
+    # COMMENT_OUT
+    bar:
+        - baz
+        - quux
+
+repositories:
+    - path: foo
+      # COMMENT_OUT
+      label: bar
+    - path: baz
+      label: quux
+
+# This comment should be kept.
+# COMMENT_OUT
+other: thing
+    '''
+
+    # flake8: noqa
+    expected_config = '''
+foo:
+    bar:
+        - baz
+        - quux
+
+repositories:
+    - path: foo
+      label: bar
+    - path: baz
+      label: quux
+
+# This comment should be kept.
+other: thing
+    '''
+
+    assert (
+        module.transform_optional_configuration(config.strip(), comment_out=False)
+        == expected_config.strip()
+    )
 
 
 def test_render_configuration_converts_configuration_to_yaml_string():
@@ -377,13 +424,32 @@ def test_generate_sample_configuration_does_not_raise():
     )
     flexmock(module).should_receive('schema_to_sample_configuration')
     flexmock(module).should_receive('merge_source_configuration_into_destination')
+    flexmock(module.os.path).should_receive('exists').and_return(False)
     flexmock(module).should_receive('render_configuration')
-    flexmock(module).should_receive('comment_out_optional_configuration')
+    flexmock(module).should_receive('transform_optional_configuration')
     flexmock(module).should_receive('write_configuration')
 
     module.generate_sample_configuration(False, None, 'dest.yaml', 'schema.yaml')
 
 
+def test_generate_sample_configuration_with_destination_directory_error():
+    builtins = flexmock(sys.modules['builtins'])
+    builtins.should_receive('open').with_args('schema.yaml', encoding='utf-8').and_return('')
+    flexmock(module.ruamel.yaml).should_receive('YAML').and_return(
+        flexmock(load=lambda filename: {})
+    )
+    flexmock(module).should_receive('schema_to_sample_configuration')
+    flexmock(module).should_receive('merge_source_configuration_into_destination')
+    flexmock(module.os.path).should_receive('exists').and_return(True)
+    flexmock(module.os.path).should_receive('isfile').and_return(False)
+    flexmock(module).should_receive('render_configuration').never()
+    flexmock(module).should_receive('transform_optional_configuration').never()
+    flexmock(module).should_receive('write_configuration').never()
+
+    with pytest.raises(ValueError):
+        module.generate_sample_configuration(False, None, 'dest.yaml', 'schema.yaml')
+
+
 def test_generate_sample_configuration_with_source_filename_omits_empty_bootstrap_field():
     builtins = flexmock(sys.modules['builtins'])
     builtins.should_receive('open').with_args('schema.yaml', encoding='utf-8').and_return('')
@@ -398,8 +464,9 @@ def test_generate_sample_configuration_with_source_filename_omits_empty_bootstra
         object, {'foo': 'bar'}
     ).once()
     flexmock(module).should_receive('merge_source_configuration_into_destination')
+    flexmock(module.os.path).should_receive('exists').and_return(False)
     flexmock(module).should_receive('render_configuration')
-    flexmock(module).should_receive('comment_out_optional_configuration')
+    flexmock(module).should_receive('transform_optional_configuration')
     flexmock(module).should_receive('write_configuration')
 
     module.generate_sample_configuration(False, 'source.yaml', 'dest.yaml', 'schema.yaml')
@@ -418,8 +485,9 @@ def test_generate_sample_configuration_with_source_filename_keeps_non_empty_boot
         object, source_config
     ).once()
     flexmock(module).should_receive('merge_source_configuration_into_destination')
+    flexmock(module.os.path).should_receive('exists').and_return(False)
     flexmock(module).should_receive('render_configuration')
-    flexmock(module).should_receive('comment_out_optional_configuration')
+    flexmock(module).should_receive('transform_optional_configuration')
     flexmock(module).should_receive('write_configuration')
 
     module.generate_sample_configuration(False, 'source.yaml', 'dest.yaml', 'schema.yaml')
@@ -433,8 +501,58 @@ def test_generate_sample_configuration_with_dry_run_does_not_write_file():
     )
     flexmock(module).should_receive('schema_to_sample_configuration')
     flexmock(module).should_receive('merge_source_configuration_into_destination')
+    flexmock(module.os.path).should_receive('exists').and_return(False)
     flexmock(module).should_receive('render_configuration')
-    flexmock(module).should_receive('comment_out_optional_configuration')
+    flexmock(module).should_receive('transform_optional_configuration')
     flexmock(module).should_receive('write_configuration').never()
 
     module.generate_sample_configuration(True, None, 'dest.yaml', 'schema.yaml')
+
+
+def test_generate_sample_configuration_with_split_writes_each_option_to_file():
+    builtins = flexmock(sys.modules['builtins'])
+    builtins.should_receive('open').with_args('schema.yaml', encoding='utf-8').and_return('')
+    flexmock(module.ruamel.yaml).should_receive('YAML').and_return(
+        flexmock(load=lambda filename: {})
+    )
+    flexmock(module).should_receive('schema_to_sample_configuration')
+    flexmock(module).should_receive('merge_source_configuration_into_destination').and_return(
+        {'foo': 1, 'bar': 2}
+    )
+    flexmock(module.os.path).should_receive('exists').and_return(False)
+    flexmock(module).should_receive('render_configuration')
+    flexmock(module).should_receive('transform_optional_configuration')
+    flexmock(module.os).should_receive('makedirs')
+    flexmock(module).should_receive('write_configuration').with_args(
+        'dest/foo.yaml',
+        None,
+        overwrite=False,
+    ).once()
+    flexmock(module).should_receive('write_configuration').with_args(
+        'dest/bar.yaml',
+        None,
+        overwrite=False,
+    ).once()
+
+    module.generate_sample_configuration(False, None, 'dest', 'schema.yaml', split=True)
+
+
+def test_generate_sample_configuration_with_split_and_file_destination_errors():
+    builtins = flexmock(sys.modules['builtins'])
+    builtins.should_receive('open').with_args('schema.yaml', encoding='utf-8').and_return('')
+    flexmock(module.ruamel.yaml).should_receive('YAML').and_return(
+        flexmock(load=lambda filename: {})
+    )
+    flexmock(module).should_receive('schema_to_sample_configuration')
+    flexmock(module).should_receive('merge_source_configuration_into_destination').and_return(
+        {'foo': 1, 'bar': 2}
+    )
+    flexmock(module.os.path).should_receive('exists').and_return(True)
+    flexmock(module.os.path).should_receive('isdir').and_return(False)
+    flexmock(module).should_receive('render_configuration').never()
+    flexmock(module).should_receive('transform_optional_configuration').never()
+    flexmock(module.os).should_receive('makedirs').never()
+    flexmock(module).should_receive('write_configuration').never()
+
+    with pytest.raises(ValueError):
+        module.generate_sample_configuration(False, None, 'dest', 'schema.yaml', split=True)

+ 19 - 3
tests/unit/actions/config/test_generate.py

@@ -6,8 +6,9 @@ from borgmatic.actions.config import generate as module
 def test_run_generate_does_not_raise():
     generate_arguments = flexmock(
         source_filename=None,
-        destination_filename='destination.yaml',
+        destination_path='destination.yaml',
         overwrite=False,
+        split=False,
     )
     global_arguments = flexmock(dry_run=False)
     flexmock(module.borgmatic.config.generate).should_receive('generate_sample_configuration')
@@ -18,8 +19,9 @@ def test_run_generate_does_not_raise():
 def test_run_generate_with_dry_run_does_not_raise():
     generate_arguments = flexmock(
         source_filename=None,
-        destination_filename='destination.yaml',
+        destination_path='destination.yaml',
         overwrite=False,
+        split=False,
     )
     global_arguments = flexmock(dry_run=True)
     flexmock(module.borgmatic.config.generate).should_receive('generate_sample_configuration')
@@ -30,8 +32,22 @@ def test_run_generate_with_dry_run_does_not_raise():
 def test_run_generate_with_source_filename_does_not_raise():
     generate_arguments = flexmock(
         source_filename='source.yaml',
-        destination_filename='destination.yaml',
+        destination_path='destination.yaml',
         overwrite=False,
+        split=False,
+    )
+    global_arguments = flexmock(dry_run=False)
+    flexmock(module.borgmatic.config.generate).should_receive('generate_sample_configuration')
+
+    module.run_generate(generate_arguments, global_arguments)
+
+
+def test_run_generate_with_split_does_not_raise():
+    generate_arguments = flexmock(
+        source_filename=None,
+        destination_path='destination.yaml',
+        overwrite=False,
+        split=True,
     )
     global_arguments = flexmock(dry_run=False)
     flexmock(module.borgmatic.config.generate).should_receive('generate_sample_configuration')