def test_unsupported_platform_with_force(self):
     """Test that UnsupportedPlatformError is raised as expected."""
     with MockedProgram('uname'):
         program = RsyncSystemBackup(destination='/some/random/directory',
                                     force=True)
         # Avoid making an actual backup.
         program.execute_helper = MagicMock()
         program.execute()
         assert program.execute_helper.called
 def test_destination_context(self):
     """Test destination context creation."""
     # Make sure DestinationContextUnavailable is raised when the
     # destination is an rsync daemon module.
     program = RsyncSystemBackup(destination='server::backups/system')
     self.assertRaises(DestinationContextUnavailable,
                       lambda: program.destination_context)
     # Make sure the SSH alias and user are copied from the destination
     # expression to the destination context.
     program = RsyncSystemBackup(
         destination='backup-user@backup-server:backups/system')
     assert program.destination_context.ssh_alias == 'backup-server'
     assert program.destination_context.ssh_user == 'backup-user'
 def test_backup_failure(self):
     """Test that an exception is raised when ``rsync`` fails."""
     program = RsyncSystemBackup(
         destination='0.0.0.0::module/directory',
         sudo_enabled=False,
     )
     self.assertRaises(ExternalCommandFailed, program.execute)
 def test_notifications(self):
     """Test the desktop notification functionality."""
     program = RsyncSystemBackup(destination='/backups/system')
     # Right now we just make sure the Python code doesn't contain any silly
     # mistakes. It would be nice to have a more thorough test though, e.g.
     # make sure that `notify-send' is called and make sure that we don't
     # fail when `notify-send' does fail.
     program.notify_starting()
     program.notify_finished(Timer())
     program.notify_failed(Timer())
 def test_rsync_module_path_as_destination(self):
     """Test that destination defaults to ``$RSYNC_MODULE_PATH``."""
     with TemporaryDirectory() as temporary_directory:
         try:
             os.environ['RSYNC_MODULE_PATH'] = temporary_directory
             program = RsyncSystemBackup()
             assert program.destination.directory == temporary_directory
             assert not program.destination.hostname
             assert not program.destination.username
             assert not program.destination.module
         finally:
             os.environ.pop('RSYNC_MODULE_PATH')
 def test_invalid_destination_directory(self):
     """Test that InvalidDestinationDirectory is raised as expected."""
     if not os.path.isdir(MOUNT_POINT):
         return self.skipTest("Skipping test because %s doesn't exist!",
                              MOUNT_POINT)
     with prepared_image_file():
         program = RsyncSystemBackup(
             crypto_device=CRYPTO_NAME,
             destination='/some/random/directory',
             mount_point=MOUNT_POINT,
             notifications_enabled=False,
         )
         self.assertRaises(InvalidDestinationDirectory,
                           program.transfer_changes)
 def test_mount_failure(self):
     """Test that FailedToMountError is raised as expected."""
     with prepared_image_file(create_filesystem=False):
         program = RsyncSystemBackup(
             crypto_device=CRYPTO_NAME,
             destination=os.path.join(MOUNT_POINT, 'latest'),
             mount_point=MOUNT_POINT,
         )
         # When `mount' fails it should exit with a nonzero exit code,
         # thereby causing executor to raise an ExternalCommandFailed
         # exception that obscures the FailedToMountError exception that
         # we're interested in. The check=False option enables our
         # `last resort error handling' code path to be reached.
         program.destination_context.options['check'] = False
         self.assertRaises(FailedToMountError, program.execute)
 def test_missing_crypto_device(self):
     """Test that MissingBackupDiskError is raised as expected."""
     if not os.path.isdir(MOUNT_POINT):
         return self.skipTest("Skipping test because %s doesn't exist!",
                              MOUNT_POINT)
     # Make sure the image file doesn't exist.
     if os.path.exists(IMAGE_FILE):
         os.unlink(IMAGE_FILE)
     # Ask rsync-system-backup to use the encrypted filesystem on the image
     # file anyway, because we know it will fail and that's exactly what
     # we're interested in :-).
     program = RsyncSystemBackup(
         crypto_device=CRYPTO_NAME,
         destination=os.path.join(MOUNT_POINT, 'latest'),
         mount_point=MOUNT_POINT,
         notifications_enabled=False,
     )
     self.assertRaises(MissingBackupDiskError, program.execute)
 def test_unlock_failure(self):
     """Test that FailedToUnlockError is raised as expected."""
     # Make sure the image file doesn't exist.
     if os.path.exists(IMAGE_FILE):
         os.unlink(IMAGE_FILE)
     # Ask rsync-system-backup to use the encrypted filesystem on the image
     # file anyway, because we know it will fail and that's exactly what
     # we're interested in :-).
     program = RsyncSystemBackup(
         crypto_device=CRYPTO_NAME,
         destination=os.path.join(MOUNT_POINT, 'latest'),
         mount_point=MOUNT_POINT,
     )
     # When `cryptdisks_start' fails it should exit with a nonzero exit
     # code, thereby causing executor to raise an ExternalCommandFailed
     # exception that obscures the FailedToUnlockError exception that we're
     # interested in. The check=False option enables our `last resort error
     # handling' code path to be reached.
     program.destination_context.options['check'] = False
     self.assertRaises(FailedToUnlockError, program.execute)
 def test_unsupported_platform_error(self):
     """Test that UnsupportedPlatformError is raised as expected."""
     with MockedProgram('uname'):
         program = RsyncSystemBackup(destination='/some/random/directory')
         self.assertRaises(UnsupportedPlatformError, program.execute)
 def test_notifications(self):
     """Test the desktop notification functionality."""
     timer = Timer()
     program = RsyncSystemBackup(destination='/backups/system')
     # The happy path.
     with MockedProgram('notify-send', returncode=0):
         program.notify_starting()
         program.notify_finished(timer)
         program.notify_failed(timer)
     # The sad path (should not raise exceptions).
     with MockedProgram('notify-send', returncode=1):
         program.notify_starting()
         program.notify_finished(timer)
         program.notify_failed(timer)
Beispiel #12
0
def main():
    """Command line interface for the ``rsync-system-backup`` program."""
    # Initialize logging to the terminal and system log.
    coloredlogs.install(syslog=True)
    # Parse the command line arguments.
    context_opts = dict()
    program_opts = dict()
    dest_opts = dict()
    try:
        options, arguments = getopt.gnu_getopt(sys.argv[1:], 'bsrm:c:t:i:unx:fvqh', [
            'backup', 'snapshot', 'rotate', 'mount=', 'crypto=', 'tunnel=',
            'ionice=', 'no-sudo', 'dry-run', 'multi-fs', 'exclude=', 'force',
            'disable-notifications', 'verbose', 'quiet', 'help',
        ])
        for option, value in options:
            if option in ('-b', '--backup'):
                enable_explicit_action(program_opts, 'backup_enabled')
            elif option in ('-s', '--snapshot'):
                enable_explicit_action(program_opts, 'snapshot_enabled')
            elif option in ('-r', '--rotate'):
                enable_explicit_action(program_opts, 'rotate_enabled')
            elif option in ('-m', '--mount'):
                program_opts['mount_point'] = value
            elif option in ('-c', '--crypto'):
                program_opts['crypto_device'] = value
            elif option in ('-t', '--tunnel'):
                ssh_user, _, value = value.rpartition('@')
                ssh_alias, _, port_number = value.partition(':')
                tunnel_opts = dict(
                    ssh_alias=ssh_alias,
                    ssh_user=ssh_user,
                    # The port number of the rsync daemon.
                    remote_port=RSYNCD_PORT,
                )
                if port_number:
                    # The port number of the SSH server.
                    tunnel_opts['port'] = int(port_number)
                dest_opts['ssh_tunnel'] = SecureTunnel(**tunnel_opts)
            elif option in ('-i', '--ionice'):
                value = value.lower().strip()
                validate_ionice_class(value)
                program_opts['ionice'] = value
            elif option in ('-u', '--no-sudo'):
                program_opts['sudo_enabled'] = False
            elif option in ('-n', '--dry-run'):
                logger.info("Performing a dry run (because of %s option) ..", option)
                program_opts['dry_run'] = True
            elif option in ('-f', '--force'):
                program_opts['force'] = True
            elif option in ('-x', '--exclude'):
                program_opts.setdefault('exclude_list', [])
                program_opts['exclude_list'].append(value)
            elif option == '--multi-fs':
                program_opts['multi_fs'] = True
            elif option == '--disable-notifications':
                program_opts['notifications_enabled'] = False
            elif option in ('-v', '--verbose'):
                coloredlogs.increase_verbosity()
            elif option in ('-q', '--quiet'):
                coloredlogs.decrease_verbosity()
            elif option in ('-h', '--help'):
                usage(__doc__)
                return
            else:
                raise Exception("Unhandled option! (programming error)")
        if len(arguments) > 2:
            msg = "Expected one or two positional arguments! (got %i)"
            raise Exception(msg % len(arguments))
        if len(arguments) == 2:
            # Get the source from the first of two arguments.
            program_opts['source'] = arguments.pop(0)
        if arguments:
            # Get the destination from the second (or only) argument.
            dest_opts['expression'] = arguments[0]
            program_opts['destination'] = Destination(**dest_opts)
        elif not os.environ.get('RSYNC_MODULE_PATH'):
            # Show a usage message when no destination is given.
            usage(__doc__)
            return
    except Exception as e:
        warning("Error: %s", e)
        sys.exit(1)
    try:
        # Inject the source context into the program options.
        program_opts['source_context'] = create_context(**context_opts)
        # Initialize the program with the command line
        # options and execute the requested action(s).
        RsyncSystemBackup(**program_opts).execute()
    except Exception as e:
        if isinstance(e, RsyncSystemBackupError):
            # Special handling when the backup disk isn't available.
            if isinstance(e, MissingBackupDiskError):
                # Check if we're connected to a terminal to decide whether the
                # error should be propagated or silenced, the idea being that
                # rsync-system-backup should keep quiet when it's being run
                # from cron and the backup disk isn't available.
                if not connected_to_terminal():
                    logger.info("Skipping backup: %s", e)
                    sys.exit(0)
            # Known problems shouldn't produce
            # an intimidating traceback to users.
            logger.error("Aborting due to error: %s", e)
        else:
            # Unhandled exceptions do get a traceback,
            # because it may help fix programming errors.
            logger.exception("Aborting due to unhandled exception!")
        sys.exit(1)
def main():
    """Command line interface for the ``rsync-system-backup`` program."""
    # Initialize logging to the terminal and system log.
    coloredlogs.install(syslog=True)
    # Parse the command line arguments.
    context_opts = dict()
    program_opts = dict()
    try:
        options, arguments = getopt.getopt(sys.argv[1:], 'bsrm:c:i:unvqh', [
            'backup',
            'snapshot',
            'rotate',
            'mount=',
            'crypto=',
            'ionice=',
            'no-sudo',
            'dry-run',
            'disable-notifications',
            'verbose',
            'quiet',
            'help',
        ])
        for option, value in options:
            if option in ('-b', '--backup'):
                enable_explicit_action(program_opts, 'backup_enabled')
            elif option in ('-s', '--snapshot'):
                enable_explicit_action(program_opts, 'snapshot_enabled')
            elif option in ('-r', '--rotate'):
                enable_explicit_action(program_opts, 'rotate_enabled')
            elif option in ('-m', '--mount'):
                program_opts['mount_point'] = value
            elif option in ('-c', '--crypto'):
                program_opts['crypto_device'] = value
            elif option in ('-i', '--ionice'):
                value = value.lower().strip()
                validate_ionice_class(value)
                program_opts['ionice'] = value
            elif option in ('-u', '--no-sudo'):
                program_opts['sudo_enabled'] = False
            elif option in ('-n', '--dry-run'):
                logger.info("Performing a dry run (because of %s option) ..",
                            option)
                program_opts['dry_run'] = True
            elif option == '--disable-notifications':
                program_opts['notifications_enabled'] = False
            elif option in ('-v', '--verbose'):
                coloredlogs.increase_verbosity()
            elif option in ('-q', '--quiet'):
                coloredlogs.decrease_verbosity()
            elif option in ('-h', '--help'):
                usage(__doc__)
                return
            else:
                raise Exception("Unhandled option! (programming error)")
        if len(arguments) > 2:
            msg = "Expected one or two positional arguments! (got %i)"
            raise Exception(msg % len(arguments))
        if len(arguments) == 2:
            # Get the source from the first of two arguments.
            program_opts['source'] = arguments.pop(0)
        if arguments:
            # Get the destination from the second (or only) argument.
            program_opts['destination'] = arguments[0]
        elif not os.environ.get('RSYNC_MODULE_PATH'):
            # Show a usage message when no destination is given.
            usage(__doc__)
            return
    except Exception as e:
        warning("Error: %s", e)
        sys.exit(1)
    try:
        # Inject the source context into the program options.
        program_opts['source_context'] = create_context(**context_opts)
        # Initialize the program with the command line
        # options and execute the requested action(s).
        RsyncSystemBackup(**program_opts).execute()
    except Exception as e:
        if isinstance(e, RsyncSystemBackupError):
            # Known problems shouldn't produce
            # an intimidating traceback to users.
            logger.error("Aborting due to error: %s", e)
        else:
            # Unhandled exceptions do get a traceback,
            # because it may help fix programming errors.
            logger.exception("Aborting due to unhandled exception!")
        sys.exit(1)