]> git.michaelhowe.org Git - packages/b/bup.git/commitdiff
rbackup-cmd: we can now backup a *remote* machine to a *local* server.
authorAvery Pennarun <apenwarr@gmail.com>
Sun, 21 Mar 2010 04:41:52 +0000 (00:41 -0400)
committerAvery Pennarun <apenwarr@gmail.com>
Sun, 21 Mar 2010 05:53:58 +0000 (01:53 -0400)
The -r option to split and save allowed you to backup from a local machine
to a remote server, but that doesn't always work; sometimes the machine you
want to backup is out on the Internet, and the backup repo is safe behind a
firewall.  In that case, you can ssh *out* from the secure backup machine to
the public server, but not vice versa, and you were out of luck.  Some
people have apparently been doing this:

    ssh publicserver tar -c / | bup split -n publicserver

(ie. running tar remotely, piped to a local bup split) but that isn't
efficient, because it sends *all* the data from the remote server over the
network before deduplicating it locally.  Now you can do instead:

    bup rbackup publicserver index -vux /
    bup rbackup publicserver save -n publicserver /

And get all the usual advantages of 'bup save -r', except the server runs
locally and the client runs remotely.

cmd/rbackup-cmd.py [new file with mode: 0755]
cmd/rbackup-server-cmd.py [new file with mode: 0755]
cmd/save-cmd.py
cmd/split-cmd.py
lib/bup/client.py

diff --git a/cmd/rbackup-cmd.py b/cmd/rbackup-cmd.py
new file mode 100755 (executable)
index 0000000..dcb5ecb
--- /dev/null
@@ -0,0 +1,57 @@
+#!/usr/bin/env python
+import sys, os, struct, getopt, subprocess, signal
+from bup import options, ssh
+from bup.helpers import *
+
+optspec = """
+bup rbackup <hostname> index ...
+bup rbackup <hostname> save ...
+bup rbackup <hostname> split ...
+"""
+o = options.Options('bup rbackup', optspec, optfunc=getopt.getopt)
+(opt, flags, extra) = o.parse(sys.argv[1:])
+if len(extra) < 2:
+    o.fatal('arguments expected')
+
+class SigException(Exception):
+    def __init__(self, signum):
+        self.signum = signum
+        Exception.__init__(self, 'signal %d received' % signum)
+def handler(signum, frame):
+    raise SigException(signum)
+
+signal.signal(signal.SIGTERM, handler)
+signal.signal(signal.SIGINT, handler)
+
+sp = None
+p = None
+ret = 99
+
+try:
+    hostname = extra[0]
+    argv = extra[1:]
+    p = ssh.connect(hostname, 'rbackup-server')
+
+    argvs = '\0'.join(['bup'] + argv)
+    p.stdin.write(struct.pack('!I', len(argvs)) + argvs)
+    p.stdin.flush()
+
+    main_exe = os.environ.get('BUP_MAIN_EXE') or sys.argv[0]
+    sp = subprocess.Popen([main_exe, 'server'], stdin=p.stdout, stdout=p.stdin)
+
+    p.stdin.close()
+    p.stdout.close()
+
+finally:
+    while 1:
+        # if we get a signal while waiting, we have to keep waiting, just
+        # in case our child doesn't die.
+        try:
+            ret = p.wait()
+            sp.wait()
+            break
+        except SigException, e:
+            log('\nbup rbackup: %s\n' % e)
+            os.kill(p.pid, e.signum)
+            ret = 84
+sys.exit(ret)
diff --git a/cmd/rbackup-server-cmd.py b/cmd/rbackup-server-cmd.py
new file mode 100755 (executable)
index 0000000..9c2b0d8
--- /dev/null
@@ -0,0 +1,50 @@
+#!/usr/bin/env python
+import sys, os, struct
+from bup import options, helpers
+
+optspec = """
+bup rbackup-server
+--
+    This command is not intended to be run manually.
+"""
+o = options.Options('bup rbackup-server', optspec)
+(opt, flags, extra) = o.parse(sys.argv[1:])
+if extra:
+    o.fatal('no arguments expected')
+
+# get the subcommand's argv.
+# Normally we could just pass this on the command line, but since we'll often
+# be getting called on the other end of an ssh pipe, which tends to mangle
+# argv (by sending it via the shell), this way is much safer.
+buf = sys.stdin.read(4)
+sz = struct.unpack('!I', buf)[0]
+assert(sz > 0)
+assert(sz < 1000000)
+buf = sys.stdin.read(sz)
+assert(len(buf) == sz)
+argv = buf.split('\0')
+
+# stdin/stdout are supposedly connected to 'bup server' that the caller
+# started for us (often on the other end of an ssh tunnel), so we don't want
+# to misuse them.  Move them out of the way, then replace stdout with
+# a pointer to stderr in case our subcommand wants to do something with it.
+#
+# It might be nice to do the same with stdin, but my experiments showed that
+# ssh seems to make its child's stderr a readable-but-never-reads-anything
+# socket.  They really should have used shutdown(SHUT_WR) on the other end
+# of it, but probably didn't.  Anyway, it's too messy, so let's just make sure
+# anyone reading from stdin is disappointed.
+#
+# (You can't just leave stdin/stdout "not open" by closing the file
+# descriptors.  Then the next file that opens is automatically assigned 0 or 1,
+# and people *trying* to read/write stdin/stdout get screwed.)
+os.dup2(0, 3)
+os.dup2(1, 4)
+os.dup2(2, 1)
+fd = os.open('/dev/null', os.O_RDONLY)
+os.dup2(fd, 0)
+os.close(fd)
+
+os.environ['BUP_SERVER_REVERSE'] = helpers.hostname()
+os.execvp(argv[0], argv)
+sys.exit(99)
index 411215b3a5fd495dea0d9ca9bb6b3c6cb6a606af..edb712a1001435562f2dbaaf4fe1fdd0116d4f34 100755 (executable)
@@ -27,8 +27,12 @@ if not extra:
 opt.progress = (istty and not opt.quiet)
 opt.smaller = parse_num(opt.smaller or 0)
 
+is_reverse = os.environ.get('BUP_SERVER_REVERSE')
+if is_reverse and opt.remote:
+    o.fatal("don't use -r in reverse mode; it's automatic")
+
 refname = opt.name and 'refs/heads/%s' % opt.name or None
-if opt.remote:
+if opt.remote or is_reverse:
     cli = client.Client(opt.remote)
     oldref = refname and cli.read_ref(refname) or None
     w = cli.new_packwriter()
index 0f8408c7071077b0fccbdaef7757509dbbc92378..e8df4d30c66551b2b0984967ddf5c619641a4167 100755 (executable)
@@ -45,12 +45,15 @@ if opt.fanout:
 if opt.blobs:
     hashsplit.fanout = 0
 
+is_reverse = os.environ.get('BUP_SERVER_REVERSE')
+if is_reverse and opt.remote:
+    o.fatal("don't use -r in reverse mode; it's automatic")
 start_time = time.time()
 
 refname = opt.name and 'refs/heads/%s' % opt.name or None
 if opt.noop or opt.copy:
     cli = w = oldref = None
-elif opt.remote:
+elif opt.remote or is_reverse:
     cli = client.Client(opt.remote)
     oldref = refname and cli.read_ref(refname) or None
     w = cli.new_packwriter()
index 8a83e38e40a3fef958bbadf07f8687a7abfc4120..32e0e0d1f319132a00f17b1b7b354b5edaf66821 100644 (file)
@@ -9,7 +9,11 @@ class ClientError(Exception):
 
 class Client:
     def __init__(self, remote, create=False):
-        self._busy = self.conn = self.p = None
+        self._busy = self.conn = self.p = self.pout = self.pin = None
+        is_reverse = os.environ.get('BUP_SERVER_REVERSE')
+        if is_reverse:
+            assert(not remote)
+            remote = '%s:' % is_reverse
         rs = remote.split(':', 1)
         if len(rs) == 1:
             (host, dir) = (None, remote)
@@ -20,10 +24,16 @@ class Client:
                                  % re.sub(r'[^@\w]', '_', 
                                           "%s:%s" % (host, dir)))
         try:
-            self.p = ssh.connect(host, 'server')
+            if is_reverse:
+                self.pout = os.fdopen(3, 'rb')
+                self.pin = os.fdopen(4, 'wb')
+            else:
+                self.p = ssh.connect(host, 'server')
+                self.pout = self.p.stdout
+                self.pin = self.p.stdin
         except OSError, e:
             raise ClientError, 'exec %r: %s' % (argv[0], e), sys.exc_info()[2]
-        self.conn = Conn(self.p.stdout, self.p.stdin)
+        self.conn = Conn(self.pout, self.pin)
         if dir:
             dir = re.sub(r'[\r\n]', ' ', dir)
             if create:
@@ -45,22 +55,25 @@ class Client:
     def close(self):
         if self.conn and not self._busy:
             self.conn.write('quit\n')
-        if self.p:
-            self.p.stdin.close()
-            while self.p.stdout.read(65536):
+        if self.pin and self.pout:
+            self.pin.close()
+            while self.pout.read(65536):
                 pass
-            self.p.stdout.close()
+            self.pout.close()
+        if self.p:
             self.p.wait()
             rv = self.p.wait()
             if rv:
                 raise ClientError('server tunnel returned exit code %d' % rv)
         self.conn = None
-        self.p = None
+        self.p = self.pin = self.pout = None
 
     def check_ok(self):
-        rv = self.p.poll()
-        if rv != None:
-            raise ClientError('server exited unexpectedly with code %r' % rv)
+        if self.p:
+            rv = self.p.poll()
+            if rv != None:
+                raise ClientError('server exited unexpectedly with code %r'
+                                  % rv)
         try:
             return self.conn.check_ok()
         except Exception, e: