rsync: pass command as list, moving the shlex.split to main; support `rsync -e "rsync-wrapper -C --"` style execution with the rsync command given as arguments
"""
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
# 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_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")