Skip to content

Commit

Permalink
Merge pull request #65 from bartfeenstra/assert-paths
Browse files Browse the repository at this point in the history
Increase coverage for asserting backed up and restored data is not corrupt
  • Loading branch information
bartfeenstra committed Jun 5, 2018
2 parents f99b6e2 + 9cead08 commit 513b3c7
Show file tree
Hide file tree
Showing 10 changed files with 376 additions and 260 deletions.
50 changes: 0 additions & 50 deletions backuppy/__init__.py
@@ -1,50 +0,0 @@
import os


def assert_path(test, actual_path, expected_path):
"""Assert two actual and expected directory paths are identical.
:param test: unittest.TestCase
:param actual_path: str
:param expected_path: str
:raise: AssertionError
"""
_assert_path(test, actual_path, expected_path)
_assert_path(test, expected_path, actual_path)


def _assert_path(test, actual_path, expected_path):
"""Assert the contents of an actual directory appear in another.
:param test: unittest.TestCase
:param actual_path: str
:param expected_path: str
:raise: AssertionError
"""
actual_path = actual_path.rstrip('/') + '/'
expected_path = expected_path.rstrip('/') + '/'
try:
for expected_dir_path, child_dir_names, child_file_names in os.walk(expected_path):
actual_dir_path = os.path.join(
actual_path, expected_dir_path[len(expected_path):])
for child_file_name in child_file_names:
with open(os.path.join(expected_dir_path, child_file_name)) as expected_f:
with open(os.path.join(actual_dir_path, child_file_name)) as actual_f:
assert_file(test, actual_f, expected_f)
except Exception:
raise AssertionError(
'The actual contents under the path `%s` are not equal to the expected contents under `%s`.' % (
actual_path, expected_path))


def assert_file(test, source_f, target_f):
"""Assert two source and target files are identical.
:param test: unittest.TestCase
:param source_f: File
:param target_f: File
:raise: AssertionError
"""
source_f.seek(0)
target_f.seek(0)
test.assertEquals(source_f.read(), target_f.read())
18 changes: 16 additions & 2 deletions backuppy/config.py
Expand Up @@ -6,12 +6,12 @@

import yaml

from backuppy.location import Source, Target
from backuppy.location import Source, Target, SshOptionsProvider
from backuppy.notifier import GroupedNotifiers, Notifier, QuietNotifier
from backuppy.plugin import new_source, new_target, new_notifier


class Configuration(object):
class Configuration(SshOptionsProvider):
"""Provides back-up configuration."""

def __init__(self, name, working_directory=None, verbose=False, interactive=False):
Expand Down Expand Up @@ -116,6 +116,9 @@ def source(self, source):
assert isinstance(source, Source)
if self._source is not None:
raise AttributeError('A source has already been set.')
if isinstance(source, SshOptionsProvider) and isinstance(self._target, SshOptionsProvider):
raise AttributeError(
'The source and target cannot both be SHH locations.')
self._source = source

@property
Expand All @@ -138,6 +141,9 @@ def target(self, target):
assert isinstance(target, Target)
if self._target is not None:
raise AttributeError('A target has already been set.')
if isinstance(target, SshOptionsProvider) and isinstance(self._source, SshOptionsProvider):
raise AttributeError(
'The target and source cannot both be SHH locations.')
self._target = target

@property
Expand All @@ -148,6 +154,14 @@ def logger(self):
"""
return self._logger

def ssh_options(self):
"""Build the SSH options for this configuration."""
ssh_location = self._source if isinstance(self._source, SshOptionsProvider) else self._target if isinstance(
self._target, SshOptionsProvider) else None
if ssh_location is None:
return {}
return ssh_location.ssh_options()


def from_configuration_data(configuration_file_path, data, verbose=None, interactive=None):
"""Parse configuration from raw, built-in types such as dictionaries, lists, and scalars.
Expand Down
28 changes: 26 additions & 2 deletions backuppy/location.py
Expand Up @@ -42,6 +42,19 @@ def _new_snapshot_args(name):
]


class SshOptionsProvider(object):
"""Provide SSH options."""

def ssh_options(self):
"""Build SSH options.
:return: Dict[str, str]
"""
return {
'StrictHostKeyChecking': 'yes',
}


class Path(six.with_metaclass(ABCMeta), object):
"""Define a back-up path."""

Expand Down Expand Up @@ -223,7 +236,7 @@ def missing_host_key(self, client, hostname, key):
RejectPolicy.missing_host_key(self, client, hostname, key)


class SshTarget(Target):
class SshTarget(Target, SshOptionsProvider):
"""Provide a target over SSH."""

def __init__(self, notifier, user, host, path, port=22, identity=None, host_keys=None, interactive=False):
Expand Down Expand Up @@ -334,7 +347,18 @@ def to_rsync(self, path=None):
parts = [self.path, 'latest/']
if path:
parts.append(str(path))
return '%s@%s:%d%s' % (self.user, self.host, self.port, os.path.join(*parts))
return '%s@%s:%s' % (self.user, self.host, os.path.join(*parts))

def ssh_options(self):
"""Build SSH options.
:return: Dict[str, str]
"""
options = SshOptionsProvider.ssh_options(self)
options['Port'] = str(self.port)
options['UserKnownHostsFile'] = self._host_keys
options['IdentityFile'] = self._identity
return options


class FirstAvailableTarget(Target):
Expand Down
48 changes: 24 additions & 24 deletions backuppy/task.py
Expand Up @@ -7,6 +7,28 @@
from backuppy.location import new_snapshot_name, Path, FilePath, DirectoryPath


def rsync_args(configuration, origin, destination, path=None):
"""Build the arguments to an rsync transfer."""
args = ['rsync', '-ar', '--numeric-ids']
ssh_options = configuration.ssh_options()
if ssh_options:
ssh_args = []
for option, value in ssh_options.items():
ssh_args.append('-o %s=%s' % (option, value))
args.append('-e')
args.append('ssh %s' % ' '.join(ssh_args))
if configuration.verbose:
args.append('--verbose')
args.append('--progress')
args.append(origin.to_rsync(path))
if isinstance(path, FilePath):
args.append(destination.to_rsync(DirectoryPath(
os.path.dirname(str(path)) + '/')))
else:
args.append(destination.to_rsync(path))
return args


def backup(configuration, path=None):
"""Start a new back-up.
Expand Down Expand Up @@ -34,18 +56,7 @@ def backup(configuration, path=None):
snapshot_name = new_snapshot_name()
target.snapshot(snapshot_name)

args = ['rsync', '-ar', '--numeric-ids',
'-e', 'ssh -o StrictHostKeyChecking=yes']
if configuration.verbose:
args.append('--verbose')
args.append('--progress')
args.append(source.to_rsync(path))
if isinstance(path, FilePath):
args.append(target.to_rsync(DirectoryPath(
os.path.dirname(str(path)) + '/')))
else:
args.append(target.to_rsync(path))
subprocess.call(args)
subprocess.call(rsync_args(configuration, source, target, path))

notifier.confirm('Back-up %s complete.' % configuration.name)

Expand Down Expand Up @@ -77,18 +88,7 @@ def restore(configuration, path=None):

notifier.inform('Restoring %s...' % configuration.name)

args = ['rsync', '-ar', '--numeric-ids',
'-e', 'ssh -o StrictHostKeyChecking=yes']
if configuration.verbose:
args.append('--verbose')
args.append('--progress')
args.append(target.to_rsync(path))
if isinstance(path, FilePath):
args.append(source.to_rsync(DirectoryPath(
os.path.dirname(str(path)) + '/')))
else:
args.append(source.to_rsync(path))
subprocess.call(args)
subprocess.call(rsync_args(configuration, target, source, path))

notifier.confirm('Restoration of back-up %s complete.' %
configuration.name)
Expand Down

0 comments on commit 513b3c7

Please sign in to comment.