pvl/backup/rsync.py
author Tero Marttila <terom@paivola.fi>
Fri, 02 Mar 2012 16:46:38 +0200
changeset 34 8c23dc925ed7
parent 33 2de09042414f
child 42 43e27a3e9efe
permissions -rw-r--r--
rsync-wrapper: default to logging.INFO; cleanup pvl.backup.rsync info output
"""
    rsync handling.

    Apologies for the 'RSync' nomenclature
"""

from pvl.backup.lvm import LVM, LVMVolume, LVMSnapshot
from pvl.backup.mount import mount
from pvl.backup import invoke

import os.path

import logging

log = logging.getLogger('pvl.backup.rsync')

# Path to rsync binary
RSYNC = '/usr/bin/rsync'

def rsync (source, dest, **opts) :
    """
        Run rsync.
    """

    invoke.command(RSYNC, source, dest, **opts)

class RSyncCommandFormatError (Exception) :
    """
        Improper rsync command
    """

    pass

class RSyncServer (object) :
    """
        rsync server-mode execution.
    """

    def _execute (self, options, path) :
        """
            Underlying rsync just reads from filesystem.
        """
        
        # invoke directly, no option-handling, nor stdin/out redirection
        invoke.invoke(RSYNC, options + ['.', path], data=False)

class RSyncFSServer (RSyncServer) :
    """
        Normal filesystem backup.
    """

    def __init__ (self, path) :
        RSyncServer.__init__(self)

        self.path = path

    def execute (self, options) :
        return self._execute(options, self.path)

class RSyncLVMServer (RSyncServer) :
    """
        Backup LVM LV by snapshotting + mounting it.
    """

    def __init__ (self, volume) :
        RSyncServer.__init__(self)

        self.volume = volume
 
    def execute (self, options) :
        """
            Snapshot, mount, execute
        """
        
        # backup target from LVM command
        lvm = self.volume.lvm
        volume = self.volume

        # snapshot
        log.info("Open snapshot: %s", volume)

        # XXX: generate snapshot nametag to be unique?
        with lvm.snapshot(volume, tag='backup') as snapshot:
            # mount
            log.info("Mounting snapshot: %s", snapshot)

            with mount(snapshot.dev_path, name_hint=('lvm_' + snapshot.name + '_'), readonly=True) as mountpoint:
                # rsync!
                log.info("Running rsync: %s", mountpoint)

                # with trailing slash
                return self._execute(options, mountpoint.path + '/')

            # cleanup
        # cleanup
 
def parse_command (command_parts, restrict_server=True, restrict_readonly=True) :
    """
        Parse given rsync server command into bits. 

            command_parts       - the command-list sent by rsync
            restrict_server     - restrict to server-mode
            restrict_readonly   - restrict to read/send-mode
        
        Returns:

            (cmd, options, source, dest)
    """

    cmd = None
    options = []
    source = None
    dest = None

    # parse
    for part in command_parts :
        if cmd is None :
            cmd = part

        elif part.startswith('-') :
            options.append(part)

        elif source is None :
            source = part

        elif dest is None :
            dest = part

    # options
    have_server = ('--server' in options)
    have_sender = ('--sender' in options)

    # verify
    if not have_server :
        raise RSyncCommandFormatError("Missing --server")

    if restrict_readonly and not have_sender :
        raise RSyncCommandFormatError("Missing --sender for readonly")

    # parse path
    if have_sender :
        # read
        # XXX: which way does the dot go?
        if source != '.' :
            raise RSyncCommandFormatError("Invalid dest for sender")
        
        path = dest

    else :
        # write
        if source != '.' :
            raise RSyncCommandFormatError("Invalid source for reciever")

        path = dest

    if not path :
        raise RSyncCommandFormatError("Missing path")

    # ok
    return cmd, options, source, dest

      
def parse_source (path, restrict_path=False) :
    """
        Figure out source to rsync from, based on pseudo-path given in rsync command.
    """
        
    # normalize
    path = os.path.normpath(path)

    # verify path
    if restrict_path :
        if not path.startswith(restrict_path) :
            raise RSyncCommandFormatError("Restricted path ({restrict})".format(restrict=restrict_path))

    if path.startswith('/') :
        # direct filesystem path
        # XXX: how to handle=
        log.info("filesystem: %s", path)

        return RSyncFSServer(path)

    elif path.startswith('lvm:') :
        # LVM LV
        try :
            lvm, vg, lv = path.split(':')

        except ValueError, e:
            raise RSyncCommandFormatError("Invalid lvm pseudo-path: {error}".format(error=e))
        
        # XXX: validate

        log.info("LVM: %s/%s", vg, lv)

        # open
        lvm = LVM(vg)
        volume = lvm.volume(lv)

        return RSyncLVMServer(volume)
       
    else :
        # invalid
        raise RSyncCommandFormatError("Unrecognized backup path")