Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
de54228
first stab at an attic-borg converter
anarcat Oct 1, 2015
9ab1e19
keyfile conversion code
anarcat Oct 1, 2015
e88a994
reshuffle and document
anarcat Oct 1, 2015
2d19881
some debugging code
anarcat Oct 1, 2015
c7af4c7
more debug
anarcat Oct 1, 2015
312c3cf
rewrite converter to avoid using attic code
anarcat Oct 1, 2015
aa25a21
move conversion code to a separate class for clarity
anarcat Oct 1, 2015
5a16803
remove needless use of self
anarcat Oct 1, 2015
c30df4e
move converter code out of test suite
anarcat Oct 1, 2015
77ed6de
skip converter tests if attic isn't installed
anarcat Oct 1, 2015
e554365
remove unused import
anarcat Oct 1, 2015
f35e8e1
add dry run support to converter
anarcat Oct 1, 2015
a5f32b0
add convert command
anarcat Oct 1, 2015
1b29699
cosmetic: reorder
anarcat Oct 1, 2015
1ba856d
refactor: group test repo subroutine
anarcat Oct 1, 2015
bcd94b9
split up keyfile, segments and overall testing in converter
anarcat Oct 1, 2015
c990829
add attic dependency for build as a separate factor
anarcat Oct 1, 2015
a81755f
use triple-double-quoted instead of single-double-quoted
anarcat Oct 1, 2015
efbad39
help text review: magic s/number/string/, s/can/must/
anarcat Oct 1, 2015
c2913f5
style: don't use continue for nothing
anarcat Oct 1, 2015
dbd4ac7
add missing colon
anarcat Oct 1, 2015
5b8cb63
remove duplicate code with the unit test
anarcat Oct 1, 2015
ef0ed40
fix typo
anarcat Oct 1, 2015
d665163
use builtin NotImplementedError instead of writing our own
anarcat Oct 1, 2015
d5198c5
split out depends in imports
anarcat Oct 1, 2015
5f6eb87
much nicer validation checking
anarcat Oct 1, 2015
4a85f2d
fix most pep8 warnings
anarcat Oct 1, 2015
b9c474d
pep8: put pytest skip marker after imports
anarcat Oct 1, 2015
79d9aeb
use permanently instead of irrevocably, which is less common
anarcat Oct 1, 2015
57801a2
keep tests simple by always adding attic depends
anarcat Oct 1, 2015
58815bc
fix commandline dispatch for converter
anarcat Oct 1, 2015
98e4e6b
lock repository when converting segments
anarcat Oct 1, 2015
f5cb0f4
rewrite convert tests with pytest fixtures
anarcat Oct 1, 2015
a08bcb2
refactor common code
anarcat Oct 1, 2015
7f6fd1f
add docs for all converter test code
anarcat Oct 1, 2015
6c318a0
re-pep8
anarcat Oct 1, 2015
946aca9
avoid flooding the console
anarcat Oct 1, 2015
0d457bc
clarify what to do about the cache warning
anarcat Oct 1, 2015
3bb3bd4
add percentage progress
anarcat Oct 1, 2015
6a72252
release lock properly if segment conversion crashes
anarcat Oct 1, 2015
180dfcb
remove needless indentation
anarcat Oct 1, 2015
35b2195
only write magic num if necessary
anarcat Oct 1, 2015
a7902e5
cosmetic: show 100% when done, not n-1/n%
anarcat Oct 1, 2015
7c32f55
repository index conversion
anarcat Oct 1, 2015
022de5b
untested file/chunks cache conversion
anarcat Oct 1, 2015
4f9a411
remove unneeded fixture decorator
anarcat Oct 1, 2015
28a033d
remove debug output that clobbers segment spinner
anarcat Oct 1, 2015
55f79b4
complete cache conversion code
anarcat Oct 1, 2015
8022e56
don't clobber existing borg cache
anarcat Oct 1, 2015
3e7fa0d
also copy the cache config file to workaround #234
anarcat Oct 1, 2015
081b91b
remove needless paren
anarcat Oct 2, 2015
41e9942
follow naming of tested module
anarcat Oct 2, 2015
d4d1b41
remove needless autouse
anarcat Oct 2, 2015
6904058
update docs to reflect that cache is converted
anarcat Oct 2, 2015
ad85f64
whitespace
anarcat Oct 2, 2015
ea5d004
also document the cache locations
anarcat Oct 2, 2015
2c66e7c
make percentage a real percentage
anarcat Oct 3, 2015
3773681
rewire cache copy mechanisms
anarcat Oct 3, 2015
6905412
style fixes (pep8, append, file builtin)
anarcat Oct 3, 2015
48b7c8c
avoid checking for non-existent files
anarcat Oct 3, 2015
c91c5d0
rename convert command to upgrade
anarcat Oct 3, 2015
fded221
mention borg delete borg
anarcat Oct 3, 2015
5409cba
also copy files cache verbatim
anarcat Oct 3, 2015
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
66 changes: 66 additions & 0 deletions borg/archiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from . import __version__
from .archive import Archive, ArchiveChecker, CHUNKER_PARAMS
from .compress import Compressor, COMPR_BUFFER
from .upgrader import AtticRepositoryUpgrader
from .repository import Repository
from .cache import Cache
from .key import key_creator
Expand Down Expand Up @@ -462,6 +463,24 @@ def do_prune(self, args):
stats.print_('Deleted data:', cache)
return self.exit_code

def do_upgrade(self, args):
"""upgrade a repository from a previous version"""
# XXX: currently only upgrades from Attic repositories, but may
# eventually be extended to deal with major upgrades for borg
# itself.
#
# in this case, it should auto-detect the current repository
# format and fire up necessary upgrade mechanism. this remains
# to be implemented.

# XXX: should auto-detect if it is an attic repository here
repo = AtticRepositoryUpgrader(args.repository.path, create=False)
try:
repo.upgrade(args.dry_run)
except NotImplementedError as e:
print("warning: %s" % e)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just as a reminder to change this (and other similar places) to logging.warning (or .error) after we have reasonable logging in place.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure. can't wait. :)

return self.exit_code

helptext = {}
helptext['patterns'] = '''
Exclude patterns use a variant of shell pattern syntax, with '*' matching any
Expand Down Expand Up @@ -896,6 +915,53 @@ def run(self, args=None):
type=location_validator(archive=False),
help='repository to prune')

upgrade_epilog = textwrap.dedent("""
upgrade an existing Borg repository in place. this currently
only support converting an Attic repository, but may
eventually be extended to cover major Borg upgrades as well.

it will change the magic strings in the repository's segments
to match the new Borg magic strings. the keyfiles found in
$ATTIC_KEYS_DIR or ~/.attic/keys/ will also be converted and
copied to $BORG_KEYS_DIR or ~/.borg/keys.

the cache files are converted, from $ATTIC_CACHE_DIR or
~/.cache/attic to $BORG_CACHE_DIR or ~/.cache/borg, but the
cache layout between Borg and Attic changed, so it is possible
the first backup after the conversion takes longer than expected
due to the cache resync.

it is recommended you run this on a copy of the Attic
repository, in case something goes wrong, for example:

cp -a attic borg
borg upgrade -n borg
borg upgrade borg

upgrade should be able to resume if interrupted, although it
will still iterate over all segments. if you want to start
from scratch, use `borg delete` over the copied repository to
make sure the cache files are also removed:

borg delete borg

the conversion can PERMANENTLY DAMAGE YOUR REPOSITORY! Attic
will also NOT BE ABLE TO READ THE BORG REPOSITORY ANYMORE, as
the magic strings will have changed.

you have been warned.""")
subparser = subparsers.add_parser('upgrade', parents=[common_parser],
description=self.do_upgrade.__doc__,
epilog=upgrade_epilog,
formatter_class=argparse.RawDescriptionHelpFormatter)
subparser.set_defaults(func=self.do_upgrade)
subparser.add_argument('-n', '--dry-run', dest='dry_run',
default=False, action='store_true',
help='do not change repository')
subparser.add_argument('repository', metavar='REPOSITORY', nargs='?', default='',
type=location_validator(archive=False),
help='path to the repository to be upgraded')

subparser = subparsers.add_parser('help', parents=[common_parser],
description='Extra help')
subparser.add_argument('--epilog-only', dest='epilog_only',
Expand Down
163 changes: 163 additions & 0 deletions borg/testsuite/upgrader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import os
import shutil
import tempfile

import pytest

try:
import attic.repository
import attic.key
import attic.helpers
except ImportError:
attic = None

from ..upgrader import AtticRepositoryUpgrader, AtticKeyfileKey
from ..helpers import get_keys_dir
from ..key import KeyfileKey
from ..repository import Repository, MAGIC

pytestmark = pytest.mark.skipif(attic is None,
reason='cannot find an attic install')


def repo_valid(path):
"""
utility function to check if borg can open a repository

:param path: the path to the repository
:returns: if borg can check the repository
"""
repository = Repository(str(path), create=False)
# can't check raises() because check() handles the error
state = repository.check()
repository.close()
return state


def key_valid(path):
"""
check that the new keyfile is alright

:param path: the path to the key file
:returns: if the file starts with the borg magic string
"""
keyfile = os.path.join(get_keys_dir(),
os.path.basename(path))
with open(keyfile, 'r') as f:
return f.read().startswith(KeyfileKey.FILE_ID)


@pytest.fixture()
def attic_repo(tmpdir):
"""
create an attic repo with some stuff in it

:param tmpdir: path to the repository to be created
:returns: a attic.repository.Repository object
"""
attic_repo = attic.repository.Repository(str(tmpdir), create=True)
# throw some stuff in that repo, copied from `RepositoryTestCase.test1`
for x in range(100):
attic_repo.put(('%-32d' % x).encode('ascii'), b'SOMEDATA')
attic_repo.commit()
attic_repo.close()
return attic_repo


def test_convert_segments(tmpdir, attic_repo):
"""test segment conversion

this will load the given attic repository, list all the segments
then convert them one at a time. we need to close the repo before
conversion otherwise we have errors from borg

:param tmpdir: a temporary directory to run the test in (builtin
fixture)
:param attic_repo: a populated attic repository (fixture)
"""
# check should fail because of magic number
assert not repo_valid(tmpdir)
print("opening attic repository with borg and converting")
repo = AtticRepositoryUpgrader(str(tmpdir), create=False)
segments = [filename for i, filename in repo.io.segment_iterator()]
repo.close()
repo.convert_segments(segments, dryrun=False)
repo.convert_cache(dryrun=False)
assert repo_valid(tmpdir)


class MockArgs:
"""
mock attic location

this is used to simulate a key location with a properly loaded
repository object to create a key file
"""
def __init__(self, path):
self.repository = attic.helpers.Location(path)


@pytest.fixture()
def attic_key_file(attic_repo, tmpdir):
"""
create an attic key file from the given repo, in the keys
subdirectory of the given tmpdir

:param attic_repo: an attic.repository.Repository object (fixture
define above)
:param tmpdir: a temporary directory (a builtin fixture)
:returns: the KeyfileKey object as returned by
attic.key.KeyfileKey.create()
"""
keys_dir = str(tmpdir.mkdir('keys'))

# we use the repo dir for the created keyfile, because we do
# not want to clutter existing keyfiles
os.environ['ATTIC_KEYS_DIR'] = keys_dir

# we use the same directory for the converted files, which
# will clutter the previously created one, which we don't care
# about anyways. in real runs, the original key will be retained.
os.environ['BORG_KEYS_DIR'] = keys_dir
os.environ['ATTIC_PASSPHRASE'] = 'test'
return attic.key.KeyfileKey.create(attic_repo,
MockArgs(keys_dir))


def test_keys(tmpdir, attic_repo, attic_key_file):
"""test key conversion

test that we can convert the given key to a properly formatted
borg key. assumes that the ATTIC_KEYS_DIR and BORG_KEYS_DIR have
been properly populated by the attic_key_file fixture.

:param tmpdir: a temporary directory (a builtin fixture)
:param attic_repo: an attic.repository.Repository object (fixture
define above)
:param attic_key_file: an attic.key.KeyfileKey (fixture created above)
"""
repository = AtticRepositoryUpgrader(str(tmpdir), create=False)
keyfile = AtticKeyfileKey.find_key_file(repository)
AtticRepositoryUpgrader.convert_keyfiles(keyfile, dryrun=False)
assert key_valid(attic_key_file.path)


def test_convert_all(tmpdir, attic_repo, attic_key_file):
"""test all conversion steps

this runs everything. mostly redundant test, since everything is
done above. yet we expect a NotImplementedError because we do not
convert caches yet.

:param tmpdir: a temporary directory (a builtin fixture)
:param attic_repo: an attic.repository.Repository object (fixture
define above)
:param attic_key_file: an attic.key.KeyfileKey (fixture created above)
"""
# check should fail because of magic number
assert not repo_valid(tmpdir)
print("opening attic repository with borg and converting")
repo = AtticRepositoryUpgrader(str(tmpdir), create=False)
repo.upgrade(dryrun=False)
assert key_valid(attic_key_file.path)
assert repo_valid(tmpdir)
Loading