pvl/backup/rsync.py
author Tero Marttila <terom@paivola.fi>
Thu, 16 Feb 2012 16:32:22 +0200
changeset 24 6bf4f5cc4479
parent 12 fbfdde7326f4
child 28 82bcde9e21c4
permissions -rw-r--r--
rsync: fix RSyncLVMServer to mount with readonly=True
"""
    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 shlex
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

        # XXX: generate
        path = '/mnt'

        # snapshot
        log.info("Open snapshot...")

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

            # mount
            log.info("Mounting snapshot: %s -> %s", snapshot, path)

            with mount(snapshot.dev_path, path, readonly=True) as mountpoint:
                log.info("Mounted snapshot: %s", mountpoint)
                
                # rsync!
                log.info("Running rsync: ...")

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

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

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

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

    # split
    parts = shlex.split(command)

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

    # parse
    for part in 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")