bin/pvlbackup-rsync-wrapper
changeset 43 2911d4dd5a47
parent 42 43e27a3e9efe
child 44 7069af6b7025
equal deleted inserted replaced
42:43e27a3e9efe 43:2911d4dd5a47
     1 #!/usr/bin/python
       
     2 
       
     3 """
       
     4     SSH authorized_keys command="..." wrapper for rsync.
       
     5 
       
     6     Testing goes something like:
       
     7         sudo PYTHONPATH=. ./bin/pvlbackup-rsync-wrapper --command 'rsync --server --sender -ax . lvm:asdf:test' -vD
       
     8 
       
     9         sudo sh -c "PYTHONPATH=. rsync -e './bin/pvlbackup-rsync-wrapper --debug -C --' -ax testing:lvm:asdf:test test/tmp"
       
    10 """
       
    11 
       
    12 from pvl.backup import __version__
       
    13 from pvl.backup.rsync import RSyncCommandFormatError
       
    14 from pvl.backup.invoke import InvokeError
       
    15 from pvl.backup import rsync, lvm
       
    16 
       
    17 import optparse
       
    18 import shlex
       
    19 import os
       
    20 import logging
       
    21 
       
    22 log = logging.getLogger()
       
    23 
       
    24 # command-line options
       
    25 options = None
       
    26 
       
    27 def parse_options (argv) :
       
    28     """
       
    29         Parse command-line arguments.
       
    30     """
       
    31 
       
    32 #    import sys; sys.stderr.write("%s\n" % (argv, ))
       
    33 
       
    34     parser = optparse.OptionParser(
       
    35             prog        = argv[0],
       
    36 
       
    37             # module docstring
       
    38             description = __doc__,
       
    39             version     = __version__,
       
    40     )
       
    41 
       
    42     # logging
       
    43     general = optparse.OptionGroup(parser, "General Options")
       
    44 
       
    45     general.add_option('-q', '--quiet',      dest='loglevel', action='store_const', const=logging.WARNING, help="Less output")
       
    46     general.add_option('-v', '--verbose',    dest='loglevel', action='store_const', const=logging.INFO,  help="More output")
       
    47     general.add_option('-D', '--debug',      dest='loglevel', action='store_const', const=logging.DEBUG, help="Even more output")
       
    48     general.add_option('--debug-for',        action='append', metavar='MODULE', help="Enable logging for the given logger/module name")
       
    49 
       
    50     parser.add_option_group(general)
       
    51 
       
    52     #
       
    53     parser.add_option('-c', '--command',    metavar='CMD', default=os.environ.get('SSH_ORIGINAL_COMMAND'),
       
    54             help="rsync command to execute")
       
    55 
       
    56     parser.add_option('-C', '--given-command', action='store_true', default=False,
       
    57             help="use given command in `rsync -e %prog` format")
       
    58 
       
    59     parser.add_option('-R', '--readonly',   action='store_true', default=False,
       
    60             help="restrict to read/source mode")
       
    61 
       
    62     parser.add_option('-P', '--restrict-path', metavar='PATH', default=False,
       
    63             help="restrict to given path prefix")
       
    64 
       
    65     # lvm options
       
    66     parser.add_option('-L', '--snapshot-size', metavar='SIZE', default=lvm.LVM_SNAPSHOT_SIZE,
       
    67             help="create snapshot with given LV size (used to store writes during backup)")
       
    68 
       
    69     parser.add_option('--snapshot-wait', metavar='SECONDS', default=lvm.LVM_SNAPSHOT_WAIT, type='float',
       
    70             help="wait for snapshot to settle after unmounting")
       
    71 
       
    72     # defaults
       
    73     parser.set_defaults(
       
    74         debug_for   = [],
       
    75         loglevel    = logging.INFO,
       
    76     )
       
    77 
       
    78     # parse
       
    79     options, args = parser.parse_args(argv[1:])
       
    80 
       
    81     # configure
       
    82     logging.basicConfig(
       
    83         format  = '%(levelname)6s %(name)s : %(funcName)s : %(message)s',
       
    84         level   = options.loglevel,
       
    85     )
       
    86 
       
    87     # enable debugging for specific targets
       
    88     for target in options.debug_for :
       
    89         logging.getLogger(target).setLevel(logging.DEBUG)
       
    90 
       
    91     return options, args
       
    92 
       
    93 def rsync_wrapper (command, restrict='lvm:') :
       
    94     """
       
    95         Wrap given rsync command.
       
    96 
       
    97         Parses the command, the source path, and then executes rsync within the source path (which may be a special
       
    98         pseudo-path with additional handling).
       
    99     """
       
   100 
       
   101     try :
       
   102         # parse the rsync command sent by the client
       
   103         rsync_cmd, rsync_options, source_path, dest_path = rsync.parse_command(command, 
       
   104                 restrict_readonly   = options.readonly,
       
   105             )
       
   106 
       
   107     except RSyncCommandFormatError, e:
       
   108         log.error("invalid rsync command: %r: %s", command, e)
       
   109         return 2
       
   110 
       
   111     # XXX: the real path is always given second..
       
   112     path = dest_path
       
   113 
       
   114     try :
       
   115         # parse the source path as given by the client, may be a real path or pseudo-path
       
   116         source = rsync.parse_source(path,
       
   117                 restrict_path       = options.restrict_path,
       
   118                 lvm_opts            = dict(size = options.snapshot_size, wait = options.snapshot_wait),
       
   119             )
       
   120 
       
   121     except RSyncCommandFormatError, e:
       
   122         log.error("invalid rsync source: %r: %s", path, e)
       
   123         return 2
       
   124 
       
   125     try :
       
   126         # run rsync within the source (may perform additional stuff like snapshotting...)
       
   127         source.execute(rsync_options)
       
   128 
       
   129     except InvokeError, e:
       
   130         log.error("%s failed: %d", e.cmd, e.exit)
       
   131         return e.exit
       
   132 
       
   133     # ok
       
   134     return 0
       
   135 
       
   136 def main (argv) :
       
   137     global options
       
   138 
       
   139     # global options + args
       
   140     options, args = parse_options(argv)
       
   141 
       
   142     # command required
       
   143     if options.given_command :
       
   144         # from args (as given by `rsync -e pvlbackup-rsync-wrapper`) -> 'pvlbackup-rsync-wrapper <host> (<command> ...)'
       
   145         host = args.pop(0)
       
   146         command_parts = args
       
   147 
       
   148         log.debug("host=%r, using command from args: %r", host, command_parts)
       
   149 
       
   150     # args
       
   151     elif args :
       
   152         log.error("No arguments are handled")
       
   153         return 2
       
   154 
       
   155     elif options.command:
       
   156         # as given
       
   157         command_parts = shlex.split(options.command)
       
   158 
       
   159     else :
       
   160         log.error("SSH_ORIGINAL_COMMAND not given")
       
   161         return 2
       
   162 
       
   163 
       
   164     # run
       
   165     try :
       
   166         return rsync_wrapper(command_parts)
       
   167 
       
   168     except Exception, e:
       
   169         log.error("Internal error:", exc_info=e)
       
   170         return 3
       
   171 
       
   172     # ok
       
   173     return 0
       
   174 
       
   175 if __name__ == '__main__' :
       
   176     import sys
       
   177 
       
   178     sys.exit(main(sys.argv))
       
   179