Esempio n. 1
0
    def do_cleanup(
        self,
        dry_run: bool,
        preserve_scratch: bool,
    ) -> None:
        """ Ensure that the backup store gets cleaned up appropriately before we shut down; this
        can be called as a signal handler, hence the first two arguments.  Otherwise this should
        be called whenever we lock the store.

        :param signum: if called as a signal handler, the signal num; otherwise None
        :param frame: if called as a signal handler, the stack trace; otherwise None
        :param dry_run: whether to actually save any data or not
        :param preserve_scratch: whether to clean up the scratch directory before we exit; mainly
            used for debugging purposes
        """
        if not self._manifest:
            return

        if not self._manifest.changed:  # test_m1_crash_before_save
            logger.info('No changes detected; nothing to do')
        elif not dry_run:
            lock_manifest(
                self._manifest,
                self.config.read('private_key_filename', default=''),
                self._save,
                self._load,
                self.options,
            )
            self.rotate_manifests()

        if not preserve_scratch:
            rmtree(get_scratch_dir(), ignore_errors=True)
        self._manifest = None  # test_m1_crash_after_save
Esempio n. 2
0
def unlock_manifest(
    manifest_filename: str,
    private_key_filename: str,
    load: Callable[[str, IOIter], IOIter],
    options: OptionsDict,
) -> Manifest:
    """ Load a manifest into local storage and unencrypt it

    :param manifest_filename: the name of the manifest to unlock
    :param private_key_filename: the private key file in PEM format used to encrypt the
        manifest's keypair
    :param load: the _load function from the backup store
    :param options: backup store options
    :returns: the requested Manifest
    """
    local_manifest_filename = path_join(get_scratch_dir(), manifest_filename)
    logger.debug(f'Unlocking manifest at {local_manifest_filename}')

    # First use the private key to read the AES key and nonce used to encrypt the manifest
    key_pair = b''
    if options['use_encryption']:
        key_pair = get_manifest_keypair(manifest_filename,
                                        private_key_filename, load)

    # Now use the key and nonce to decrypt the manifest
    with IOIter() as encrypted_local_manifest, \
            IOIter(local_manifest_filename, check_mtime=False) as local_manifest:
        load(manifest_filename, encrypted_local_manifest)
        decrypt_and_unpack(encrypted_local_manifest, local_manifest, key_pair,
                           options)

    return Manifest(local_manifest_filename)
Esempio n. 3
0
    def unlock(self, *, dry_run=False, preserve_scratch=False) -> Iterator:
        """
        Unlock the backup store and prep for work

        The backup store is responsible for the manifest in the store; unfortunately, since
        sqlite3 doesn't accept an open file descriptor when opening a DB connection, we have to
        circumvent some of the IOIter functionality and do it ourselves.  We wrap this in a
        context manager so this can be abstracted away and still ensure that proper cleanup happens.

        :param dry_run: whether to actually save any data or not
        :param preserve_scratch: whether to clean up the scratch directory before we exit; mainly
            used for debugging purposes
        """
        # we have to create the scratch dir regardless of whether --dry-run is enabled
        # because we still need to be able to figure out what's changed and what we should do
        rmtree(get_scratch_dir(), ignore_errors=True)
        os.makedirs(get_scratch_dir(), exist_ok=True)

        try:
            manifests = sorted(self._query(MANIFEST_PREFIX))
            if not manifests:
                logger.warning('''
                    ********************************************************************
                    This looks like a new backup location; if you are not expecting this
                    message, someone may be tampering with your backup!
                    ********************************************************************
                    ''')
                self._manifest = Manifest(
                    os.path.join(
                        get_scratch_dir(),
                        MANIFEST_FILE.format(ts=time.time()),
                    ))
            else:
                self._manifest = unlock_manifest(
                    manifests[-1],
                    self.config.read('private_key_filename', default=''),
                    self._load,
                    self.options,
                )
            _register_unlocked_store(self, dry_run, preserve_scratch)

            yield

        finally:
            self.do_cleanup(dry_run, preserve_scratch)
            _unregister_store()
Esempio n. 4
0
def test_unlock(fs, backup_store, manifest_exists):
    os.makedirs(get_scratch_dir())
    with mock.patch('backuppy.stores.backup_store.Manifest') as mock_manifest, \
            mock.patch('backuppy.stores.backup_store.unlock_manifest') as mock_unlock_manifest, \
            mock.patch('backuppy.stores.backup_store.rmtree') as mock_remove, \
            mock.patch('backuppy.stores.backup_store._register_unlocked_store') as mock_register, \
            mock.patch('backuppy.stores.backup_store._unregister_store') as mock_unregister:
        backup_store.do_cleanup = mock.Mock()
        mock_unlock_manifest.return_value = mock_manifest.return_value
        if manifest_exists:
            backup_store._query.return_value = ['manifest.1234123']
        with backup_store.unlock():
            pass
        assert mock_unlock_manifest.call_count == manifest_exists
        assert mock_remove.call_count == 1
        assert backup_store.do_cleanup.call_args == mock.call(False, False)
        assert mock_register.call_count == 1
        assert mock_unregister.call_count == 1
Esempio n. 5
0
    def save(self, src: IOIter, dest: str, key_pair: bytes) -> bytes:
        """ Wrapper around the _save function that converts the SHA to a path and does encryption

        :param src: the file to save
        :param dest: the name of the file to write to in the store
        :param key_pair: an AES key + nonce to use to encrypt the file
        :returns: the HMAC of the saved file
        """
        dest = sha_to_path(dest)

        # We compress and encrypt the file on the local file system, and then pass the encrypted
        # file to the backup store to handle atomically
        filename = path_join(get_scratch_dir(), dest)

        with IOIter(filename) as encrypted_save_file:
            signature = compress_and_encrypt(src, encrypted_save_file,
                                             key_pair, self.options)
            self._save(encrypted_save_file, dest)  # test_f1_crash_file_save
        os.remove(filename)
        return signature