Browse Source

Add borg config command (fixes #3304)

This command works similarly to "git config" - it parses repo and
cache configs to get, set, and delete values. It only works on local
repos so a malicious client can't e.g. override their storage quota
or reset the append_only flag.

Add tests for borg config

Add documentation for borg config

Change manual config edits -> borg config

There were a couple places in the documentation where it was advised
to edit the repository or cache config file, a process that is stream-
lined by borg config.
Milkey Mouse 7 years ago
parent
commit
5b47cf6fa5

+ 2 - 3
docs/faq.rst

@@ -502,8 +502,7 @@ space for chunks.archive.d (see :issue:`235` for details):
 ::
 
     # this assumes you are working with the same user as the backup.
-    # you can get the REPOID from the "config" file inside the repository.
-    cd ~/.cache/borg/<REPOID>
+    cd ~/.cache/borg/$(borg config /path/to/repo id)
     rm -rf chunks.archive.d ; touch chunks.archive.d
 
 This deletes all the cached archive chunk indexes and replaces the directory
@@ -808,7 +807,7 @@ There are some caveats:
 - If the repository is in "keyfile" encryption mode, the keyfile must
   exist locally or it must be manually moved after performing the upgrade:
 
-  1. Locate the repository ID, contained in the ``config`` file in the repository.
+  1. Get the repository ID with ``borg config /path/to/repo id``.
   2. Locate the attic key file at ``~/.attic/keys/``. The correct key for the
      repository starts with the line ``ATTIC_KEY <repository id>``.
   3. Copy the attic key file to ``~/.config/borg/keys/``

+ 5 - 4
docs/quickstart.rst

@@ -22,10 +22,11 @@ a good amount of free space on the filesystem that has your backup repository
 repositories. See also :ref:`cache-memory-usage`.
 
 Borg doesn't use space reserved for root on repository disks (even when run as root),
-on file systems which do not support this mechanism (e.g. XFS) we recommend to
-reserve some space in Borg itself just to be safe by adjusting the
-``additional_free_space`` setting in the ``[repository]`` section of a repositories
-``config`` file. A good starting point is ``2G``.
+on file systems which do not support this mechanism (e.g. XFS) we recommend to reserve
+some space in Borg itself just to be safe by adjusting the ``additional_free_space``
+setting (a good starting point is ``2G``)::
+
+    borg config /path/to/repo additional_free_space 2G
 
 If Borg runs out of disk space, it tries to free as much space as it
 can while aborting the current operation safely, which allows the user to free more space

+ 1 - 0
docs/usage.rst

@@ -51,6 +51,7 @@ Usage
    usage/serve
    usage/lock
    usage/benchmark
+   usage/config
 
    usage/help
    usage/debug

+ 22 - 0
docs/usage/config.rst

@@ -0,0 +1,22 @@
+.. include:: config.rst.inc
+
+.. note::
+
+   The repository & cache config files are some of the only directly manipulable
+   parts of a repository that aren't versioned or backed up, so be careful when
+   making changes\!
+
+Examples
+~~~~~~~~
+::
+
+    # find cache directory
+    $ cd ~/.cache/borg/$(borg config /path/to/repo id)
+
+    # reserve some space
+    $ borg config /path/to/repo additional_free_space 2G
+
+    # make a repo append-only
+    $ borg config /path/to/repo append_only 1
+
+

+ 3 - 2
docs/usage/notes.rst

@@ -149,8 +149,9 @@ reject to delete the repository completely). This is useful for scenarios where
 backup client machine backups remotely to a backup server using ``borg serve``, since
 a hacked client machine cannot delete backups on the server permanently.
 
-To activate append-only mode, edit the repository ``config`` file and add a line
-``append_only=1`` to the ``[repository]`` section (or edit the line if it exists).
+To activate append-only mode, set ``append_only`` to 1 in the repository config::
+
+    borg config /path/to/repo append_only 1
 
 In append-only mode Borg will create a transaction log in the ``transactions`` file,
 where each line is a transaction and a UTC timestamp.

+ 67 - 0
src/borg/archiver.py

@@ -1,5 +1,6 @@
 import argparse
 import collections
+import configparser
 import faulthandler
 import functools
 import hashlib
@@ -1517,6 +1518,41 @@ class Archiver:
             # see issue #1867.
             repository.commit()
 
+    @with_repository(exclusive=True, cache=True, compatibility=(Manifest.Operation.WRITE,))
+    def do_config(self, args, repository, manifest, key, cache):
+        """get, set, and delete values in a repository or cache config file"""
+        try:
+            section, name = args.name.split('.')
+        except ValueError:
+            section = args.cache and "cache" or "repository"
+            name = args.name
+
+        if args.cache:
+            cache.cache_config.load()
+            config = cache.cache_config._config
+            save = cache.cache_config.save
+        else:
+            config = repository.config
+            save = lambda: repository.save_config(repository.path, repository.config)
+
+        if args.delete:
+            config.remove_option(section, name)
+            if len(config.options(section)) == 0:
+                config.remove_section(section)
+            save()
+        elif args.value:
+            if section not in config.sections():
+                config.add_section(section)
+            config.set(section, name, args.value)
+            save()
+        else:
+            try:
+                print(config.get(section, name))
+            except (configparser.NoOptionError, configparser.NoSectionError) as e:
+                print(e, file=sys.stderr)
+                return EXIT_WARNING
+        return EXIT_SUCCESS
+
     def do_debug_info(self, args):
         """display system information for debugging / bug reports"""
         print(sysinfo())
@@ -3469,6 +3505,37 @@ class Archiver:
         subparser.add_argument('args', metavar='ARGS', nargs=argparse.REMAINDER,
                                help='command arguments')
 
+        config_epilog = process_epilog("""
+        This command gets and sets options in a local repository or cache config file.
+        For security reasons, this command only works on local repositories.
+
+        To delete a config value entirely, use ``--delete``. To get an existing key, pass
+        only the key name. To set a key, pass both the key name and the new value. Keys
+        can be specified in the format "section.name" or simply "name"; the section will
+        default to "repository" and "cache" for the repo and cache configs, respectively.
+
+        By default, borg config manipulates the repository config file. Using ``--cache``
+        edits the repository cache's config file instead.
+        """)
+        subparser = subparsers.add_parser('config', parents=[common_parser], add_help=False,
+                                          description=self.do_config.__doc__,
+                                          epilog=config_epilog,
+                                          formatter_class=argparse.RawDescriptionHelpFormatter,
+                                          help='get and set repository config options')
+        subparser.set_defaults(func=self.do_config)
+        subparser.add_argument('-c', '--cache', dest='cache', action='store_true',
+                               help='get and set values from the repo cache')
+        subparser.add_argument('-d', '--delete', dest='delete', action='store_true',
+                               help='delete the key from the config file')
+
+        subparser.add_argument('location', metavar='REPOSITORY',
+                               type=location_validator(archive=False, proto='file'),
+                               help='repository to configure')
+        subparser.add_argument('name', metavar='NAME',
+                               help='name of config key')
+        subparser.add_argument('value', metavar='VALUE', nargs='?',
+                               help='new value for key')
+
         subparser = subparsers.add_parser('help', parents=[common_parser], add_help=False,
                                           description='Extra help')
         subparser.add_argument('--epilog-only', dest='epilog_only', action='store_true')

+ 7 - 2
src/borg/helpers/parseformat.py

@@ -447,7 +447,7 @@ class Location:
                                            path)
 
 
-def location_validator(archive=None):
+def location_validator(archive=None, proto=None):
     def validator(text):
         try:
             loc = Location(text)
@@ -456,7 +456,12 @@ def location_validator(archive=None):
         if archive is True and not loc.archive:
             raise argparse.ArgumentTypeError('"%s": No archive specified' % text)
         elif archive is False and loc.archive:
-            raise argparse.ArgumentTypeError('"%s" No archive can be specified' % text)
+            raise argparse.ArgumentTypeError('"%s": No archive can be specified' % text)
+        if proto is not None and loc.proto != proto:
+            if proto == 'file':
+                raise argparse.ArgumentTypeError('"%s": Repository must be local' % text)
+            else:
+                raise argparse.ArgumentTypeError('"%s": Repository must be remote' % text)
         return loc
     return validator
 

+ 17 - 0
src/borg/testsuite/archiver.py

@@ -2709,6 +2709,19 @@ id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02
         with environment_variable(_BORG_BENCHMARK_CRUD_TEST='YES'):
             self.cmd('benchmark', 'crud', self.repository_location, self.input_path)
 
+    def test_config(self):
+        self.create_test_files()
+        os.unlink('input/flagfile')
+        self.cmd('init', '--encryption=repokey', self.repository_location)
+        for flags in [[], ['--cache']]:
+            for key in {'testkey', 'testsection.testkey'}:
+                self.cmd('config', self.repository_location, *flags, key, exit_code=1)
+                self.cmd('config', self.repository_location, *flags, key, 'testcontents')
+                output = self.cmd('config', self.repository_location, *flags, key)
+                assert output == 'testcontents\n'
+                self.cmd('config', self.repository_location, *flags, '--delete', key)
+                self.cmd('config', self.repository_location, *flags, key, exit_code=1)
+
     requires_gnutar = pytest.mark.skipif(not have_gnutar(), reason='GNU tar must be installed for this test.')
     requires_gzip = pytest.mark.skipif(not shutil.which('gzip'), reason='gzip must be installed for this test.')
 
@@ -3191,6 +3204,10 @@ class RemoteArchiverTestCase(ArchiverTestCase):
     def test_debug_put_get_delete_obj(self):
         pass
 
+    @unittest.skip('only works locally')
+    def test_config(self):
+        pass
+
     def test_strip_components_doesnt_leak(self):
         self.cmd('init', '--encryption=repokey', self.repository_location)
         self.create_regular_file('dir/file', contents=b"test file contents 1")