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
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)
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()
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
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