From 29b5136da754b4f5959bc8771f44895b5195a605 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Sun, 25 Sep 2016 15:05:39 +0200 Subject: [PATCH 1/3] archiver: Move key management commands to new key subcommand. --- borg/archiver.py | 12 ++++++++++-- borg/keymanager.py | 2 +- borg/testsuite/archiver.py | 18 +++++++++--------- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/borg/archiver.py b/borg/archiver.py index 6ef1d0fb8..8a07c5c6e 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -1110,7 +1110,15 @@ class Archiver: subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', type=location_validator(archive=False)) - subparser = subparsers.add_parser('key-export', parents=[common_parser], + subparser = subparsers.add_parser('key', + description="Manage a keyfile or repokey of a repository", + epilog="", + formatter_class=argparse.RawDescriptionHelpFormatter, + help='manage repository key') + + key_parsers = subparser.add_subparsers(title='required arguments', metavar='') + + subparser = key_parsers.add_parser('export', parents=[common_parser], description=self.do_key_export.__doc__, epilog="", formatter_class=argparse.RawDescriptionHelpFormatter, @@ -1124,7 +1132,7 @@ class Archiver: default=False, help='Create an export suitable for printing and later type-in') - subparser = subparsers.add_parser('key-import', parents=[common_parser], + subparser = key_parsers.add_parser('import', parents=[common_parser], description=self.do_key_import.__doc__, epilog="", formatter_class=argparse.RawDescriptionHelpFormatter, diff --git a/borg/keymanager.py b/borg/keymanager.py index 244e16c69..8eef581da 100644 --- a/borg/keymanager.py +++ b/borg/keymanager.py @@ -98,7 +98,7 @@ class KeyManager: i += 1 return ret - export = 'To restore key use borg key-import --paper /path/to/repo\n\n' + export = 'To restore key use borg key import --paper /path/to/repo\n\n' binary = a2b_base64(self.keyblob) export += 'BORG PAPER KEY v1\n' diff --git a/borg/testsuite/archiver.py b/borg/testsuite/archiver.py index d5896c038..8fb9eddf3 100644 --- a/borg/testsuite/archiver.py +++ b/borg/testsuite/archiver.py @@ -1200,7 +1200,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): export_file = self.output_path + '/exported' self.cmd('init', self.repository_location, '--encryption', 'keyfile') repo_id = self._extract_repository_id(self.repository_path) - self.cmd('key-export', self.repository_location, export_file) + self.cmd('key', 'export', self.repository_location, export_file) with open(export_file, 'r') as fd: export_contents = fd.read() @@ -1216,7 +1216,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): os.unlink(key_file) - self.cmd('key-import', self.repository_location, export_file) + self.cmd('key', 'import', self.repository_location, export_file) with open(key_file, 'r') as fd: key_contents2 = fd.read() @@ -1227,7 +1227,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): export_file = self.output_path + '/exported' self.cmd('init', self.repository_location, '--encryption', 'repokey') repo_id = self._extract_repository_id(self.repository_path) - self.cmd('key-export', self.repository_location, export_file) + self.cmd('key', 'export', self.repository_location, export_file) with open(export_file, 'r') as fd: export_contents = fd.read() @@ -1246,7 +1246,7 @@ class ArchiverTestCase(ArchiverTestCaseBase): with Repository(self.repository_path) as repository: repository.save_key(b'') - self.cmd('key-import', self.repository_location, export_file) + self.cmd('key', 'import', self.repository_location, export_file) with Repository(self.repository_path) as repository: repo_key2 = RepoKey(repository) @@ -1258,17 +1258,17 @@ class ArchiverTestCase(ArchiverTestCaseBase): export_file = self.output_path + '/exported' self.cmd('init', self.repository_location, '--encryption', 'keyfile') - self.cmd('key-import', self.repository_location, export_file, exit_code=EXIT_ERROR) + self.cmd('key', 'import', self.repository_location, export_file, exit_code=EXIT_ERROR) with open(export_file, 'w') as fd: fd.write('something not a key\n') - self.assert_raises(NotABorgKeyFile, lambda: self.cmd('key-import', self.repository_location, export_file)) + self.assert_raises(NotABorgKeyFile, lambda: self.cmd('key', 'import', self.repository_location, export_file)) with open(export_file, 'w') as fd: fd.write('BORG_KEY a0a0a0\n') - self.assert_raises(RepoIdMismatch, lambda: self.cmd('key-import', self.repository_location, export_file)) + self.assert_raises(RepoIdMismatch, lambda: self.cmd('key', 'import', self.repository_location, export_file)) def test_key_export_paperkey(self): repo_id = 'e294423506da4e1ea76e8dcdf1a3919624ae3ae496fddf905610c351d3f09239' @@ -1283,12 +1283,12 @@ class ArchiverTestCase(ArchiverTestCaseBase): fd.write(KeyfileKey.FILE_ID + ' ' + repo_id + '\n') fd.write(b2a_base64(b'abcdefghijklmnopqrstu').decode()) - self.cmd('key-export', '--paper', self.repository_location, export_file) + self.cmd('key', 'export', '--paper', self.repository_location, export_file) with open(export_file, 'r') as fd: export_contents = fd.read() - assert export_contents == """To restore key use borg key-import --paper /path/to/repo + assert export_contents == """To restore key use borg key import --paper /path/to/repo BORG PAPER KEY v1 id: 2 / e29442 3506da 4e1ea7 / 25f62a 5a3d41 - 02 From 5c2424831e8a68783e6912ceb854ccb474a7689a Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Sun, 25 Sep 2016 15:25:02 +0200 Subject: [PATCH 2/3] archiver: Create a subcommmand debug for all debug-* commands The debug commands all should subcommands of a common debug command. This commit adds this command but keeps the old command names for 1.0.x. The plan is to remove them in 1.1.0. --- borg/archiver.py | 82 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/borg/archiver.py b/borg/archiver.py index 8a07c5c6e..024910608 100644 --- a/borg/archiver.py +++ b/borg/archiver.py @@ -1585,6 +1585,22 @@ class Archiver: subparser.add_argument('topic', metavar='TOPIC', type=str, nargs='?', help='additional help on TOPIC') + debug_epilog = textwrap.dedent(""" + These commands are not intended for normal use and potentially very + dangerous if used incorrectly. + + They exist to improve debugging capabilities without direct system access, e.g. + in case you ever run into some severe malfunction. Use them only if you know + what you are doing or if a trusted developer tells you what to do.""") + + subparser = subparsers.add_parser('debug', + description='debugging command (not intended for normal use)', + epilog=debug_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='debugging command (not intended for normal use)') + + debug_parsers = subparser.add_subparsers(title='required arguments', metavar='') + debug_info_epilog = textwrap.dedent(""" This command displays some system information that might be useful for bug reports and debugging problems. If a traceback happens, this information is @@ -1597,6 +1613,13 @@ class Archiver: help='show system infos for debugging / bug reports (debug)') subparser.set_defaults(func=self.do_debug_info) + subparser = debug_parsers.add_parser('info', parents=[common_parser], + description=self.do_debug_info.__doc__, + epilog=debug_info_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='show system infos for debugging / bug reports (debug)') + subparser.set_defaults(func=self.do_debug_info) + debug_dump_archive_items_epilog = textwrap.dedent(""" This command dumps raw (but decrypted and decompressed) archive items (only metadata) to files. """) @@ -1610,6 +1633,16 @@ class Archiver: type=location_validator(archive=True), help='archive to dump') + subparser = debug_parsers.add_parser('dump-archive-items', parents=[common_parser], + description=self.do_debug_dump_archive_items.__doc__, + epilog=debug_dump_archive_items_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='dump archive items (metadata) (debug)') + subparser.set_defaults(func=self.do_debug_dump_archive_items) + subparser.add_argument('location', metavar='ARCHIVE', + type=location_validator(archive=True), + help='archive to dump') + debug_dump_repo_objs_epilog = textwrap.dedent(""" This command dumps raw (but decrypted and decompressed) repo objects to files. """) @@ -1623,6 +1656,16 @@ class Archiver: type=location_validator(archive=False), help='repo to dump') + subparser = debug_parsers.add_parser('dump-repo-objs', parents=[common_parser], + description=self.do_debug_dump_repo_objs.__doc__, + epilog=debug_dump_repo_objs_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='dump repo objects (debug)') + subparser.set_defaults(func=self.do_debug_dump_repo_objs) + subparser.add_argument('location', metavar='REPOSITORY', + type=location_validator(archive=False), + help='repo to dump') + debug_get_obj_epilog = textwrap.dedent(""" This command gets an object from the repository. """) @@ -1640,6 +1683,20 @@ class Archiver: subparser.add_argument('path', metavar='PATH', type=str, help='file to write object data into') + subparser = debug_parsers.add_parser('get-obj', parents=[common_parser], + description=self.do_debug_get_obj.__doc__, + epilog=debug_get_obj_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='get object from repository (debug)') + subparser.set_defaults(func=self.do_debug_get_obj) + subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', + type=location_validator(archive=False), + help='repository to use') + subparser.add_argument('id', metavar='ID', type=str, + help='hex object ID to get from the repo') + subparser.add_argument('path', metavar='PATH', type=str, + help='file to write object data into') + debug_put_obj_epilog = textwrap.dedent(""" This command puts objects into the repository. """) @@ -1655,6 +1712,18 @@ class Archiver: subparser.add_argument('paths', metavar='PATH', nargs='+', type=str, help='file(s) to read and create object(s) from') + subparser = debug_parsers.add_parser('put-obj', parents=[common_parser], + description=self.do_debug_put_obj.__doc__, + epilog=debug_put_obj_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='put object to repository (debug)') + subparser.set_defaults(func=self.do_debug_put_obj) + subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', + type=location_validator(archive=False), + help='repository to use') + subparser.add_argument('paths', metavar='PATH', nargs='+', type=str, + help='file(s) to read and create object(s) from') + debug_delete_obj_epilog = textwrap.dedent(""" This command deletes objects from the repository. """) @@ -1669,6 +1738,19 @@ class Archiver: help='repository to use') subparser.add_argument('ids', metavar='IDs', nargs='+', type=str, help='hex object ID(s) to delete from the repo') + + subparser = debug_parsers.add_parser('delete-obj', parents=[common_parser], + description=self.do_debug_delete_obj.__doc__, + epilog=debug_delete_obj_epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + help='delete object from repository (debug)') + subparser.set_defaults(func=self.do_debug_delete_obj) + subparser.add_argument('location', metavar='REPOSITORY', nargs='?', default='', + type=location_validator(archive=False), + help='repository to use') + subparser.add_argument('ids', metavar='IDs', nargs='+', type=str, + help='hex object ID(s) to delete from the repo') + return parser def get_args(self, argv, cmd): From a11436cfb6e67f114936b32279c27223a611eb91 Mon Sep 17 00:00:00 2001 From: Martin Hostettler Date: Sun, 25 Sep 2016 16:08:22 +0200 Subject: [PATCH 3/3] setup.py: Add subcommand support to build_usage. --- setup.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index b0103e06a..e0a18a47a 100644 --- a/setup.py +++ b/setup.py @@ -150,19 +150,33 @@ class build_usage(Command): def run(self): print('generating usage docs') + if not os.path.exists('docs/usage'): + os.mkdir('docs/usage') # allows us to build docs without the C modules fully loaded during help generation from borg.archiver import Archiver parser = Archiver().build_parser(prog='borg') + + self.generate_level("", parser, Archiver) + + def generate_level(self, prefix, parser, Archiver): + is_subcommand = False choices = {} for action in parser._actions: - if action.choices is not None: - choices.update(action.choices) + if action.choices is not None and 'SubParsersAction' in str(action.__class__): + is_subcommand = True + for cmd, parser in action.choices.items(): + choices[prefix + cmd] = parser + if prefix and not choices: + return print('found commands: %s' % list(choices.keys())) - if not os.path.exists('docs/usage'): - os.mkdir('docs/usage') + for command, parser in choices.items(): print('generating help for %s' % command) - with open('docs/usage/%s.rst.inc' % command, 'w') as doc: + + if self.generate_level(command + " ", parser, Archiver): + return + + with open('docs/usage/%s.rst.inc' % command.replace(" ", "_"), 'w') as doc: doc.write(".. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!\n\n") if command == 'help': for topic in Archiver.helptext: @@ -173,14 +187,16 @@ class build_usage(Command): doc.write(Archiver.helptext[topic]) else: params = {"command": command, + "command_": command.replace(' ', '_'), "underline": '-' * len('borg ' + command)} - doc.write(".. _borg_{command}:\n\n".format(**params)) + doc.write(".. _borg_{command_}:\n\n".format(**params)) doc.write("borg {command}\n{underline}\n::\n\n".format(**params)) epilog = parser.epilog parser.epilog = None doc.write(re.sub("^", " ", parser.format_help(), flags=re.M)) doc.write("\nDescription\n~~~~~~~~~~~\n") doc.write(epilog) + return is_subcommand class build_api(Command):