summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorben <ben@2b77aa54-bcbc-44c9-a7ec-4f6cf2b41109>2002-07-16 05:16:42 +0000
committerben <ben@2b77aa54-bcbc-44c9-a7ec-4f6cf2b41109>2002-07-16 05:16:42 +0000
commit4c8440ee71ba819c7327913870a615186ef8d386 (patch)
tree5d4d811680e1b3fd0a3393de3d49eb9cae116481
parent6efc3610e37994c38a70cf32266e1e495035fbd3 (diff)
downloadrdiff-backup-4c8440ee71ba819c7327913870a615186ef8d386.tar.gz
Various changes to 0.9.3, see CHANGELOG
git-svn-id: http://svn.savannah.nongnu.org/svn/rdiff-backup/trunk@157 2b77aa54-bcbc-44c9-a7ec-4f6cf2b41109
-rw-r--r--rdiff-backup/rdiff_backup/Globals.py45
-rw-r--r--rdiff-backup/rdiff_backup/Main.py75
-rw-r--r--rdiff-backup/rdiff_backup/Security.py171
-rw-r--r--rdiff-backup/rdiff_backup/SetConnections.py14
-rw-r--r--rdiff-backup/rdiff_backup/Time.py28
-rw-r--r--rdiff-backup/rdiff_backup/cmodule.c4
-rw-r--r--rdiff-backup/rdiff_backup/connection.py47
-rw-r--r--rdiff-backup/rdiff_backup/increment.py4
-rw-r--r--rdiff-backup/rdiff_backup/rorpiter.py4
-rw-r--r--rdiff-backup/rdiff_backup/selection.py13
-rw-r--r--rdiff-backup/rdiff_backup/statistics.py49
-rw-r--r--rdiff-backup/src/Globals.py45
-rw-r--r--rdiff-backup/src/Main.py75
-rw-r--r--rdiff-backup/src/Security.py171
-rw-r--r--rdiff-backup/src/SetConnections.py14
-rw-r--r--rdiff-backup/src/Time.py28
-rw-r--r--rdiff-backup/src/cmodule.c4
-rw-r--r--rdiff-backup/src/connection.py47
-rw-r--r--rdiff-backup/src/increment.py4
-rw-r--r--rdiff-backup/src/rorpiter.py4
-rw-r--r--rdiff-backup/src/selection.py13
-rw-r--r--rdiff-backup/src/statistics.py49
-rw-r--r--rdiff-backup/testing/commontest.py16
-rw-r--r--rdiff-backup/testing/connectiontest.py13
-rw-r--r--rdiff-backup/testing/finaltest.py31
-rw-r--r--rdiff-backup/testing/securitytest.py60
-rw-r--r--rdiff-backup/testing/selectiontest.py12
-rw-r--r--rdiff-backup/testing/statisticstest.py1
-rw-r--r--rdiff-backup/testing/timetest.py26
29 files changed, 938 insertions, 129 deletions
diff --git a/rdiff-backup/rdiff_backup/Globals.py b/rdiff-backup/rdiff_backup/Globals.py
index 0fc08f5..bf4a977 100644
--- a/rdiff-backup/rdiff_backup/Globals.py
+++ b/rdiff-backup/rdiff_backup/Globals.py
@@ -85,9 +85,6 @@ isbackup_writer = None
# Connection of the backup writer
backup_writer = None
-# True if this process is the client invoked by the user
-isclient = None
-
# Connection of the client
client_conn = None
@@ -171,6 +168,22 @@ select_source, select_mirror = None, None
# object. Access is provided to increment error counts.
ITRB = None
+# Percentage of time to spend sleeping. None means never sleep.
+sleep_ratio = None
+
+# security_level has 4 values and controls which requests from remote
+# systems will be honored. "all" means anything goes. "read-only"
+# means that the requests must not write to disk. "update-only" means
+# that requests shouldn't destructively update the disk (but normal
+# incremental updates are OK). "minimal" means only listen to a few
+# basic requests.
+security_level = "all"
+
+# If this is set, it indicates that the remote connection should only
+# deal with paths inside of restrict_path.
+restrict_path = None
+
+
def get(name):
"""Return the value of something in this module"""
return globals()[name]
@@ -199,6 +212,32 @@ def set_integer(name, val):
"received %s instead." % (name, val))
set(name, intval)
+def set_float(name, val, min = None, max = None, inclusive = 1):
+ """Like set, but make sure val is float within given bounds"""
+ def error():
+ s = "Variable %s must be set to a float" % (name,)
+ if min is not None and max is not None:
+ s += " between %s and %s " % (min, max)
+ if inclusive: s += "inclusive"
+ else: s += "not inclusive"
+ elif min is not None or max is not None:
+ if inclusive: inclusive_string = "or equal to "
+ else: inclusive_string = ""
+ if min is not None:
+ s += " greater than %s%s" % (inclusive_string, min)
+ else: s+= " less than %s%s" % (inclusive_string, max)
+ Log.FatalError(s)
+
+ try: f = float(val)
+ except ValueError: error()
+ if min is not None:
+ if inclusive and f < min: error()
+ elif not inclusive and f <= min: error()
+ if max is not None:
+ if inclusive and f > max: error()
+ elif not inclusive and f >= max: error()
+ set(name, f)
+
def get_dict_val(name, key):
"""Return val from dictionary in this class"""
return globals()[name][key]
diff --git a/rdiff-backup/rdiff_backup/Main.py b/rdiff-backup/rdiff_backup/Main.py
index 35624c2..06a1285 100644
--- a/rdiff-backup/rdiff_backup/Main.py
+++ b/rdiff-backup/rdiff_backup/Main.py
@@ -44,16 +44,17 @@ def parse_cmdlineoptions(arglist):
"checkpoint-interval=", "current-time=", "exclude=",
"exclude-device-files", "exclude-filelist=",
"exclude-filelist-stdin", "exclude-mirror=",
- "exclude-regexp=", "force", "include=",
- "include-filelist=", "include-filelist-stdin",
+ "exclude-other-filesystems", "exclude-regexp=", "force",
+ "include=", "include-filelist=", "include-filelist-stdin",
"include-regexp=", "list-increments", "mirror-only",
- "no-compression", "no-compression-regexp=",
- "no-hard-links", "no-resume", "null-separator",
- "parsable-output", "print-statistics", "quoting-char=",
- "remote-cmd=", "remote-schema=", "remove-older-than=",
- "restore-as-of=", "resume", "resume-window=", "server",
- "ssh-no-compression", "terminal-verbosity=",
- "test-server", "verbosity", "version", "windows-mode",
+ "no-compression", "no-compression-regexp=", "no-hard-links",
+ "no-resume", "null-separator", "parsable-output",
+ "print-statistics", "quoting-char=", "remote-cmd=",
+ "remote-schema=", "remove-older-than=", "restore-as-of=",
+ "restrict=", "restrict-read-only=", "restrict-update-only=",
+ "resume", "resume-window=", "server", "sleep-ratio=",
+ "ssh-no-compression", "terminal-verbosity=", "test-server",
+ "verbosity", "version", "windows-mode",
"windows-time-format"])
except getopt.error, e:
commandline_error("Bad commandline options: %s" % str(e))
@@ -80,6 +81,8 @@ def parse_cmdlineoptions(arglist):
select_files.append(sys.stdin)
elif opt == "--exclude-mirror":
select_mirror_opts.append(("--exclude", arg))
+ elif opt == "--exclude-other-filesystems":
+ select_opts.append((opt, arg))
elif opt == "--exclude-regexp": select_opts.append((opt, arg))
elif opt == "--force": force = 1
elif opt == "--include": select_opts.append((opt, arg))
@@ -99,23 +102,34 @@ def parse_cmdlineoptions(arglist):
elif opt == "--no-hard-links": Globals.set('preserve_hardlinks', 0)
elif opt == '--no-resume': Globals.resume = 0
elif opt == "--null-separator": Globals.set("null_separator", 1)
- elif opt == "-r" or opt == "--restore-as-of":
- restore_timestr, action = arg, "restore-as-of"
elif opt == "--parsable-output": Globals.set('parsable_output', 1)
elif opt == "--print-statistics":
Globals.set('print_statistics', 1)
elif opt == "--quoting-char":
Globals.set('quoting_char', arg)
Globals.set('quoting_enabled', 1)
+ elif opt == "-r" or opt == "--restore-as-of":
+ restore_timestr, action = arg, "restore-as-of"
elif opt == "--remote-cmd": remote_cmd = arg
elif opt == "--remote-schema": remote_schema = arg
elif opt == "--remove-older-than":
remove_older_than_string = arg
action = "remove-older-than"
+ elif opt == "--restrict": Globals.restrict_path = arg
+ elif opt == "--restrict-read-only":
+ Globals.security_level = "read-only"
+ Globals.restrict_path = arg
+ elif opt == "--restrict-update-only":
+ Globals.security_level = "update-only"
+ Globals.restrict_path = arg
elif opt == '--resume': Globals.resume = 1
elif opt == '--resume-window':
Globals.set_integer('resume_window', arg)
- elif opt == "-s" or opt == "--server": action = "server"
+ elif opt == "-s" or opt == "--server":
+ action = "server"
+ Globals.server = 1
+ elif opt == "--sleep-ratio":
+ Globals.set_float("sleep_ratio", arg, 0, 1, inclusive=0)
elif opt == "--ssh-no-compression":
Globals.set('ssh_compression', None)
elif opt == "--terminal-verbosity": Log.setterm_verbosity(arg)
@@ -176,7 +190,6 @@ def misc_setup(rps):
os.umask(077)
Time.setcurtime(Globals.current_time)
FilenameMapping.set_init_quote_vals()
- Globals.set("isclient", 1)
SetConnections.UpdateGlobal("client_conn", Globals.local_connection)
# This is because I originally didn't think compiled regexps
@@ -209,7 +222,9 @@ def Main(arglist):
"""Start everything up!"""
parse_cmdlineoptions(arglist)
set_action()
- rps = SetConnections.InitRPs(args, remote_schema, remote_cmd)
+ cmdpairs = SetConnections.get_cmd_pairs(args, remote_schema, remote_cmd)
+ Security.initialize(action, cmdpairs)
+ rps = map(SetConnections.cmdpair2rp, cmdpairs)
misc_setup(rps)
take_action(rps)
cleanup()
@@ -222,6 +237,7 @@ def Mirror(src_rp, dest_rp):
# Since no "rdiff-backup-data" dir, use root of destination.
SetConnections.UpdateGlobal('rbdir', dest_rp)
SetConnections.BackupInitConnections(src_rp.conn, dest_rp.conn)
+ backup_init_select(src_rp, dest_rp)
HighLevel.Mirror(src_rp, dest_rp)
def mirror_check_paths(rpin, rpout):
@@ -245,7 +261,7 @@ def Backup(rpin, rpout):
Time.setprevtime(prevtime)
HighLevel.Mirror_and_increment(rpin, rpout, incdir, RSI)
else: HighLevel.Mirror(rpin, rpout, incdir, RSI)
- backup_touch_curmirror(rpin, rpout)
+ rpout.conn.Main.backup_touch_curmirror_local(rpin, rpout)
def backup_init_select(rpin, rpout):
"""Create Select objects on source and dest connections"""
@@ -307,6 +323,7 @@ may need to use the --exclude option.""" % (rpout.path, rpin.path), 2)
def backup_get_mirrorrps():
"""Return list of current_mirror rps"""
+ datadir = Globals.rbdir
if not datadir.isdir(): return []
mirrorrps = [datadir.append(fn) for fn in datadir.listdir()
if fn.startswith("current_mirror.")]
@@ -324,12 +341,14 @@ went wrong during your last backup? Using """ + mirrorrps[-1].path, 2)
timestr = mirrorrps[-1].getinctime()
return Time.stringtotime(timestr)
-def backup_touch_curmirror(rpin, rpout):
+def backup_touch_curmirror_local(rpin, rpout):
"""Make a file like current_mirror.time.data to record time
- Also updates rpout so mod times don't get messed up.
+ Also updates rpout so mod times don't get messed up. This should
+ be run on the destination connection.
"""
+ datadir = Globals.rbdir
map(RPath.delete, backup_get_mirrorrps())
mirrorrp = datadir.append("current_mirror.%s.%s" % (Time.curtimestr,
"data"))
@@ -337,7 +356,6 @@ def backup_touch_curmirror(rpin, rpout):
mirrorrp.touch()
RPath.copy_attribs(rpin, rpout)
-
def restore(src_rp, dest_rp = None):
"""Main restoring function
@@ -474,23 +492,24 @@ def RemoveOlderThan(rootrp):
(datadir.path,))
try: time = Time.genstrtotime(remove_older_than_string)
- except TimeError, exc: Log.FatalError(str(exc))
+ except Time.TimeException, exc: Log.FatalError(str(exc))
timep = Time.timetopretty(time)
Log("Deleting increment(s) before %s" % timep, 4)
- itimes = [Time.stringtopretty(inc.getinctime())
- for inc in Restore.get_inclist(datadir.append("increments"))
- if Time.stringtotime(inc.getinctime()) < time]
-
- if not itimes:
+ times_in_secs = map(lambda inc: Time.stringtotime(inc.getinctime()),
+ Restore.get_inclist(datadir.append("increments")))
+ times_in_secs = filter(lambda t: t < time, times_in_secs)
+ if not times_in_secs:
Log.FatalError("No increments older than %s found" % timep)
- inc_pretty_time = "\n".join(itimes)
- if len(itimes) > 1 and not force:
+
+ times_in_secs.sort()
+ inc_pretty_time = "\n".join(map(Time.timetopretty, times_in_secs))
+ if len(times_in_secs) > 1 and not force:
Log.FatalError("Found %d relevant increments, dated:\n%s"
"\nIf you want to delete multiple increments in this way, "
- "use the --force." % (len(itimes), inc_pretty_time))
+ "use the --force." % (len(times_in_secs), inc_pretty_time))
Log("Deleting increment%sat times:\n%s" %
- (len(itimes) == 1 and " " or "s ", inc_pretty_time), 3)
+ (len(times_in_secs) == 1 and " " or "s ", inc_pretty_time), 3)
Manage.delete_earlier_than(datadir, time)
diff --git a/rdiff-backup/rdiff_backup/Security.py b/rdiff-backup/rdiff_backup/Security.py
new file mode 100644
index 0000000..e4630d7
--- /dev/null
+++ b/rdiff-backup/rdiff_backup/Security.py
@@ -0,0 +1,171 @@
+# Copyright 2002 Ben Escoto
+#
+# This file is part of rdiff-backup.
+#
+# rdiff-backup is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, Inc., 675 Mass Ave, Cambridge MA
+# 02139, USA; either version 2 of the License, or (at your option) any
+# later version; incorporated herein by reference.
+
+"""Functions to make sure remote requests are kosher"""
+
+import sys
+import Globals, tempfile
+from rpath import *
+
+class Violation(Exception):
+ """Exception that indicates an improper request has been received"""
+ pass
+
+
+# This will store the list of functions that will be honored from
+# remote connections.
+allowed_requests = None
+
+# This stores the list of global variables that the client can not
+# set on the server.
+disallowed_server_globals = ["server", "security_level", "restrict_path"]
+
+def initialize(action, cmdpairs):
+ """Initialize allowable request list and chroot"""
+ global allowed_requests
+ set_security_level(action, cmdpairs)
+ set_allowed_requests(Globals.security_level)
+
+def set_security_level(action, cmdpairs):
+ """If running client, set security level and restrict_path
+
+ To find these settings, we must look at the action to see what is
+ supposed to happen, and then look at the cmdpairs to see what end
+ the client is on.
+
+ """
+ def islocal(cmdpair): return not cmdpair[0]
+ def bothlocal(cp1, cp2): return islocal(cp1) and islocal(cp2)
+ def bothremote(cp1, cp2): return not islocal(cp1) and not islocal(cp2)
+ def getpath(cmdpair): return cmdpair[1]
+
+ if Globals.server: return
+ cp1 = cmdpairs[0]
+ if len(cmdpairs) > 1: cp2 = cmdpairs[1]
+
+ if action == "backup":
+ if bothlocal(cp1, cp2) or bothremote(cp1, cp2):
+ sec_level = "minimal"
+ rdir = tempfile.gettempdir()
+ elif islocal(cp1):
+ sec_level = "read-only"
+ rdir = getpath(cp1)
+ else:
+ assert islocal(cp2)
+ sec_level = "update-only"
+ rdir = getpath(cp2)
+ elif action == "restore" or action == "restore-as-of":
+ if len(cmdpairs) == 1 or bothlocal(cp1, cp2) or bothremote(cp1, cp2):
+ sec_level = "minimal"
+ rdir = tempfile.gettempdir()
+ elif islocal(cp1):
+ sec_level = "read-only"
+ else:
+ assert islocal(cp2)
+ sec_level = "all"
+ rdir = getpath(cp2)
+ elif action == "mirror":
+ if bothlocal(cp1, cp2) or bothremote(cp1, cp2):
+ sec_level = "minimal"
+ rdir = tempfile.gettempdir()
+ elif islocal(cp1):
+ sec_level = "read-only"
+ rdir = getpath(cp1)
+ else:
+ assert islocal(cp2)
+ sec_level = "all"
+ rdir = getpath(cp2)
+ elif (action == "test-server" or action == "list-increments" or
+ action == "calculate-average" or action == "remove-older-than"):
+ sec_level = "minimal"
+ rdir = tempfile.gettempdir()
+ else: assert 0, "Unknown action %s" % action
+
+ Globals.security_level = sec_level
+ Globals.restrict_path = rdir
+
+def set_allowed_requests(sec_level):
+ """Set the allowed requests list using the security level"""
+ global allowed_requests
+ if sec_level == "all": return
+ allowed_requests = ["VirtualFile.readfromid", "VirtualFile.closebyid",
+ "Globals.get", "Globals.is_not_None",
+ "Globals.get_dict_val",
+ "Log.open_logfile_allconn",
+ "Log.close_logfile_allconn",
+ "SetConnections.add_redirected_conn",
+ "RedirectedRun"]
+ if sec_level == "minimal": pass
+ elif sec_level == "read-only" or sec_level == "update-only":
+ allowed_requests.extend(["C.make_file_dict",
+ "os.getuid",
+ "os.listdir",
+ "Resume.ResumeCheck",
+ "HLSourceStruct.split_initial_dsiter",
+ "HLSourceStruct.get_diffs_and_finalize"])
+ if sec_level == "update-only":
+ allowed_requests. \
+ extend(["Log.open_logfile_local", "Log.close_logfile_local",
+ "Log.close_logfile_allconn", "Log.log_to_file",
+ "SaveState.init_filenames",
+ "SaveState.touch_last_file",
+ "HLDestinationStruct.get_sigs",
+ "HLDestinationStruct.patch_w_datadir_writes",
+ "HLDestinationStruct.patch_and_finalize",
+ "HLDestinationStruct.patch_increment_and_finalize",
+ "Main.backup_touch_curmirror_local",
+ "Globals.ITRB.increment_stat"])
+ if Globals.server:
+ allowed_requests.extend(["SetConnections.init_connection_remote",
+ "Log.setverbosity",
+ "Log.setterm_verbosity",
+ "Time.setcurtime_local",
+ "Time.setprevtime_local",
+ "FilenameMapping.set_init_quote_vals_local",
+ "Globals.postset_regexp_local",
+ "Globals.set_select",
+ "HLSourceStruct.set_session_info",
+ "HLDestinationStruct.set_session_info"])
+
+def vet_request(request, arglist):
+ """Examine request for security violations"""
+ #if Globals.server: sys.stderr.write(str(request) + "\n")
+ security_level = Globals.security_level
+ if Globals.restrict_path:
+ for arg in arglist:
+ if isinstance(arg, RPath): vet_rpath(arg)
+ if security_level == "all": return
+ if request.function_string in allowed_requests: return
+ if request.function_string == "Globals.set":
+ if Globals.server and arglist[0] not in disallowed_server_globals:
+ return
+ raise Violation("\nWarning Security Violation!\n"
+ "Bad request for function: %s\n"
+ "with arguments: %s\n" % (request.function_string,
+ arglist))
+
+def vet_rpath(rpath):
+ """Require rpath not to step outside retricted directory"""
+ if Globals.restrict_path and rpath.conn is Globals.local_connection:
+ normalized, restrict = rpath.normalize().path, Globals.restrict_path
+ components = normalized.split("/")
+ # 3 cases for restricted dir /usr/foo: /var, /usr/foobar, /usr/foo/..
+ if (not normalized.startswith(restrict) or
+ (len(normalized) > len(restrict) and
+ normalized[len(restrict)] != "/") or
+ ".." in components):
+ raise Violation("\nWarning Security Violation!\n"
+ "Request to handle path %s\n"
+ "which doesn't appear to be within "
+ "restrict path %s.\n" % (normalized, restrict))
+
+
+
+
diff --git a/rdiff-backup/rdiff_backup/SetConnections.py b/rdiff-backup/rdiff_backup/SetConnections.py
index 91edd7d..8d763a1 100644
--- a/rdiff-backup/rdiff_backup/SetConnections.py
+++ b/rdiff-backup/rdiff_backup/SetConnections.py
@@ -28,8 +28,13 @@ __conn_remote_cmds = [None]
class SetConnectionsException(Exception): pass
-def InitRPs(arglist, remote_schema = None, remote_cmd = None):
- """Map the given file descriptions into rpaths and return list"""
+def get_cmd_pairs(arglist, remote_schema = None, remote_cmd = None):
+ """Map the given file descriptions into command pairs
+
+ Command pairs are tuples cmdpair with length 2. cmdpair[0] is
+ None iff it describes a local path, and cmdpair[1] is the path.
+
+ """
global __cmd_schema
if remote_schema: __cmd_schema = remote_schema
elif not Globals.ssh_compression: __cmd_schema = __cmd_schema_no_compress
@@ -44,11 +49,10 @@ def InitRPs(arglist, remote_schema = None, remote_cmd = None):
elif remote_schema:
Log("Remote schema option ignored - no remote file "
"descriptions.", 2)
-
- cmd_pairs = map(desc2cmd_pairs, desc_pairs)
+ cmdpairs = map(desc2cmd_pairs, desc_pairs)
if remote_cmd: # last file description gets remote_cmd
cmd_pairs[-1] = (remote_cmd, cmd_pairs[-1][1])
- return map(cmdpair2rp, cmd_pairs)
+ return cmdpairs
def cmdpair2rp(cmd_pair):
"""Return RPath from cmd_pair (remote_cmd, filename)"""
diff --git a/rdiff-backup/rdiff_backup/Time.py b/rdiff-backup/rdiff_backup/Time.py
index 6220514..90d3ae8 100644
--- a/rdiff-backup/rdiff_backup/Time.py
+++ b/rdiff-backup/rdiff_backup/Time.py
@@ -25,17 +25,18 @@ _genstr_date_regexp1 = re.compile("^(?P<year>[0-9]{4})[-/]"
_genstr_date_regexp2 = re.compile("^(?P<month>[0-9]{1,2})[-/]"
"(?P<day>[0-9]{1,2})[-/](?P<year>[0-9]{4})$")
curtime = curtimestr = None
+been_awake_since = None # stores last time sleep() was run
def setcurtime(curtime = None):
"""Sets the current time in curtime and curtimestr on all systems"""
t = curtime or time.time()
for conn in Globals.connections:
- conn.Time.setcurtime_local(t, timetostring(t))
+ conn.Time.setcurtime_local(t)
-def setcurtime_local(timeinseconds, timestr):
+def setcurtime_local(timeinseconds):
"""Only set the current time locally"""
global curtime, curtimestr
- curtime, curtimestr = timeinseconds, timestr
+ curtime, curtimestr = timeinseconds, timetostring(timeinseconds)
def setprevtime(timeinseconds):
"""Sets the previous inc time in prevtime and prevtimestr"""
@@ -168,6 +169,25 @@ def cmp(time1, time2):
elif time1 == time2: return 0
else: return 1
+
+def sleep(sleep_ratio):
+ """Sleep for period to maintain given sleep_ratio
+
+ On my system sleeping for periods less than 1/20th of a second
+ doesn't seem to work very accurately, so accumulate at least that
+ much time before sleeping.
+
+ """
+ global been_awake_since
+ if been_awake_since is None: # first running
+ been_awake_since = time.time()
+ else:
+ elapsed_time = time.time() - been_awake_since
+ sleep_time = elapsed_time * (sleep_ratio/(1-sleep_ratio))
+ if sleep_time >= 0.05:
+ time.sleep(sleep_time)
+ been_awake_since = time.time()
+
def genstrtotime(timestr, curtime = None):
"""Convert a generic time string to a time in seconds"""
if curtime is None: curtime = globals()['curtime']
@@ -203,5 +223,3 @@ the day).""" % timestr)
t = stringtotime(timestr)
if t: return t
else: error()
-
-
diff --git a/rdiff-backup/rdiff_backup/cmodule.c b/rdiff-backup/rdiff_backup/cmodule.c
index cce87da..6a55ab4 100644
--- a/rdiff-backup/rdiff_backup/cmodule.c
+++ b/rdiff-backup/rdiff_backup/cmodule.c
@@ -49,7 +49,7 @@ static PyObject *c_make_file_dict(self, args)
size = PyLong_FromLongLong((LONG_LONG)sbuf.st_size);
inode = PyLong_FromLongLong((LONG_LONG)sbuf.st_ino);
#else
- size = PyInt_FromLong((long)sbuf.st_size);
+ size = PyInt_FromLong(sbuf.st_size);
inode = PyInt_FromLong((long)sbuf.st_ino);
#endif
mode = (long)sbuf.st_mode;
@@ -64,7 +64,7 @@ static PyObject *c_make_file_dict(self, args)
atime = PyLong_FromLongLong((LONG_LONG)sbuf.st_atime);
#else
mtime = PyInt_FromLong((long)sbuf.st_mtime);
- atime = PyLong_FromLongLong((long)sbuf.st_atime);
+ atime = PyInt_FromLong((long)sbuf.st_atime);
#endif
/* Build return dictionary from stat struct */
diff --git a/rdiff-backup/rdiff_backup/connection.py b/rdiff-backup/rdiff_backup/connection.py
index 91c44fe..20dd3db 100644
--- a/rdiff-backup/rdiff_backup/connection.py
+++ b/rdiff-backup/rdiff_backup/connection.py
@@ -48,11 +48,9 @@ class LocalConnection(Connection):
elif isinstance(__builtins__, dict): return __builtins__[name]
else: return __builtins__.__dict__[name]
- def __setattr__(self, name, value):
- globals()[name] = value
+ def __setattr__(self, name, value): globals()[name] = value
- def __delattr__(self, name):
- del globals()[name]
+ def __delattr__(self, name): del globals()[name]
def __str__(self): return "LocalConnection"
@@ -329,7 +327,9 @@ class PipeConnection(LowLevelPipeConnection):
arg_req_num, arg = self._get()
assert arg_req_num == req_num
argument_list.append(arg)
- try: result = apply(eval(request.function_string), argument_list)
+ try:
+ Security.vet_request(request, argument_list)
+ result = apply(eval(request.function_string), argument_list)
except: result = self.extract_exception()
self._put(result, req_num)
self.unused_request_numbers[req_num] = None
@@ -407,14 +407,31 @@ class RedirectedConnection(Connection):
self.routing_number = routing_number
self.routing_conn = Globals.connection_dict[routing_number]
+ def reval(self, function_string, *args):
+ """Evalution function_string on args on remote connection"""
+ return self.routing_conn.reval("RedirectedRun", self.conn_number,
+ function_string, *args)
+
def __str__(self):
return "RedirectedConnection %d,%d" % (self.conn_number,
self.routing_number)
def __getattr__(self, name):
- return EmulateCallable(self.routing_conn,
- "Globals.get_dict_val('connection_dict', %d).%s"
- % (self.conn_number, name))
+ return EmulateCallableRedirected(self.conn_number, self.routing_conn,
+ name)
+
+def RedirectedRun(conn_number, func, *args):
+ """Run func with args on connection with conn number conn_number
+
+ This function is meant to redirect requests from one connection to
+ another, so conn_number must not be the local connection (and also
+ for security reasons since this function is always made
+ available).
+
+ """
+ conn = Globals.connection_dict[conn_number]
+ assert conn is not Globals.local_connection, conn
+ return conn.reval(func, *args)
class EmulateCallable:
@@ -428,6 +445,18 @@ class EmulateCallable:
return EmulateCallable(self.connection,
"%s.%s" % (self.name, attr_name))
+class EmulateCallableRedirected:
+ """Used by RedirectedConnection in calls like conn.os.chmod(foo)"""
+ def __init__(self, conn_number, routing_conn, name):
+ self.conn_number, self.routing_conn = conn_number, routing_conn
+ self.name = name
+ def __call__(self, *args):
+ return apply(self.routing_conn.reval,
+ ("RedirectedRun", self.conn_number, self.name) + args)
+ def __getattr__(self, attr_name):
+ return EmulateCallableRedirected(self.conn_number, self.routing_conn,
+ "%s.%s" % (self.name, attr_name))
+
class VirtualFile:
"""When the client asks for a file over the connection, it gets this
@@ -499,7 +528,7 @@ class VirtualFile:
# everything has to be available here for remote connection's use, but
# put at bottom to reduce circularities.
-import Globals, Time, Rdiff, Hardlink, FilenameMapping, C
+import Globals, Time, Rdiff, Hardlink, FilenameMapping, C, Security, Main
from static import *
from lazy import *
from log import *
diff --git a/rdiff-backup/rdiff_backup/increment.py b/rdiff-backup/rdiff_backup/increment.py
index e3b7f5a..814322d 100644
--- a/rdiff-backup/rdiff_backup/increment.py
+++ b/rdiff-backup/rdiff_backup/increment.py
@@ -274,6 +274,7 @@ class IncrementITRB(StatsITRB):
def branch_process(self, branch):
"""Update statistics, and the has_changed flag if change in branch"""
+ if Globals.sleep_ratio is not None: Time.sleep(Globals.sleep_ratio)
if branch.changed: self.changed = 1
self.add_file_stats(branch)
@@ -288,7 +289,7 @@ class MirrorITRB(StatsITRB):
StatsITRB.__init__(self)
def start_process(self, index, diff_rorp, mirror_dsrp):
- """Initialize statistics, do actual writing to mirror"""
+ """Initialize statistics and do actual writing to mirror"""
self.start_stats(mirror_dsrp)
if diff_rorp and not diff_rorp.isplaceholder():
RORPIter.patchonce_action(None, mirror_dsrp, diff_rorp).execute()
@@ -312,6 +313,7 @@ class MirrorITRB(StatsITRB):
def branch_process(self, branch):
"""Update statistics with subdirectory results"""
+ if Globals.sleep_ratio is not None: Time.sleep(Globals.sleep_ratio)
self.add_file_stats(branch)
diff --git a/rdiff-backup/rdiff_backup/rorpiter.py b/rdiff-backup/rdiff_backup/rorpiter.py
index cfd2d5f..1e85d55 100644
--- a/rdiff-backup/rdiff_backup/rorpiter.py
+++ b/rdiff-backup/rdiff_backup/rorpiter.py
@@ -242,7 +242,9 @@ class RORPIter:
def init(): Hardlink.link_rp(diff_rorp, tf, basisrp)
return Robust.make_tf_robustaction(init, tf, basisrp)
elif basisrp and basisrp.isreg() and diff_rorp.isreg():
- assert diff_rorp.get_attached_filetype() == 'diff'
+ if diff_rorp.get_attached_filetype() != 'diff':
+ raise RPathException("File %s appears to have changed during"
+ " processing, skipping" % (basisrp.path,))
return Rdiff.patch_with_attribs_action(basisrp, diff_rorp)
else: # Diff contains whole file, just copy it over
if not basisrp: basisrp = base_rp.new_index(diff_rorp.index)
diff --git a/rdiff-backup/rdiff_backup/selection.py b/rdiff-backup/rdiff_backup/selection.py
index 58abdf0..c70857c 100644
--- a/rdiff-backup/rdiff_backup/selection.py
+++ b/rdiff-backup/rdiff_backup/selection.py
@@ -256,6 +256,8 @@ class Select:
self.add_selection_func(self.filelist_get_sf(
filelists[filelists_index], 0, arg))
filelists_index += 1
+ elif opt == "--exclude-other-filesystems":
+ self.add_selection_func(self.other_filesystems_get_sf(0))
elif opt == "--exclude-regexp":
self.add_selection_func(self.regexp_get_sf(arg, 0))
elif opt == "--include":
@@ -416,6 +418,17 @@ probably isn't what you meant.""" %
else: return (None, None) # dsrp greater, not initial sequence
else: assert 0, "Include is %s, should be 0 or 1" % (include,)
+ def other_filesystems_get_sf(self, include):
+ """Return selection function matching files on other filesystems"""
+ assert include == 0 or include == 1
+ root_devloc = self.dsrpath.getdevloc()
+ def sel_func(dsrp):
+ if dsrp.getdevloc() == root_devloc: return None
+ else: return include
+ sel_func.exclude = not include
+ sel_func.name = "Match other filesystems"
+ return sel_func
+
def regexp_get_sf(self, regexp_string, include):
"""Return selection function given by regexp_string"""
assert include == 0 or include == 1
diff --git a/rdiff-backup/rdiff_backup/statistics.py b/rdiff-backup/rdiff_backup/statistics.py
index e9f43dc..83a8858 100644
--- a/rdiff-backup/rdiff_backup/statistics.py
+++ b/rdiff-backup/rdiff_backup/statistics.py
@@ -28,7 +28,7 @@ class StatsObj:
'ChangedFiles',
'ChangedSourceSize', 'ChangedMirrorSize',
'IncrementFiles', 'IncrementFileSize')
- stat_misc_attrs = ('Errors',)
+ stat_misc_attrs = ('Errors', 'TotalDestinationSizeChange')
stat_time_attrs = ('StartTime', 'EndTime', 'ElapsedTime')
stat_attrs = (('Filename',) + stat_time_attrs +
stat_misc_attrs + stat_file_attrs)
@@ -65,6 +65,26 @@ class StatsObj:
"""Add 1 to value of attribute"""
self.__dict__[attr] += 1
+ def get_total_dest_size_change(self):
+ """Return total destination size change
+
+ This represents the total change in the size of the
+ rdiff-backup destination directory.
+
+ """
+ addvals = [self.NewFileSize, self.ChangedSourceSize,
+ self.IncrementFileSize]
+ subtractvals = [self.DeletedFileSize, self.ChangedMirrorSize]
+ for val in addvals + subtractvals:
+ if val is None:
+ result = None
+ break
+ else:
+ def addlist(l): return reduce(lambda x,y: x+y, l)
+ result = addlist(addvals) - addlist(subtractvals)
+ self.TotalDestinationSizeChange = result
+ return result
+
def get_stats_line(self, index, use_repr = 1):
"""Return one line abbreviated version of full stats string"""
file_attrs = map(lambda attr: str(self.get_stat(attr)),
@@ -95,7 +115,9 @@ class StatsObj:
def get_stats_string(self):
"""Return extended string printing out statistics"""
- return self.get_timestats_string() + self.get_filestats_string()
+ return "%s%s%s" % (self.get_timestats_string(),
+ self.get_filestats_string(),
+ self.get_miscstats_string())
def get_timestats_string(self):
"""Return portion of statistics string dealing with time"""
@@ -112,8 +134,6 @@ class StatsObj:
self.ElapsedTime = self.EndTime - self.StartTime
timelist.append("ElapsedTime %.2f (%s)\n" %
(self.ElapsedTime, Time.inttopretty(self.ElapsedTime)))
- if self.Errors is not None:
- timelist.append("Errors %d\n" % self.Errors)
return "".join(timelist)
def get_filestats_string(self):
@@ -130,8 +150,23 @@ class StatsObj:
return "".join(map(fileline, self.stat_file_pairs))
+ def get_miscstats_string(self):
+ """Return portion of extended stat string about misc attributes"""
+ misc_string = ""
+ tdsc = self.get_total_dest_size_change()
+ if tdsc is not None:
+ misc_string += ("TotalDestinationSizeChange %s (%s)\n" %
+ (tdsc, self.get_byte_summary_string(tdsc)))
+ if self.Errors is not None: misc_string += "Errors %d\n" % self.Errors
+ return misc_string
+
def get_byte_summary_string(self, byte_count):
"""Turn byte count into human readable string like "7.23GB" """
+ if byte_count < 0:
+ sign = "-"
+ byte_count = -byte_count
+ else: sign = ""
+
for abbrev_bytes, abbrev_string in self.byte_abbrev_list:
if byte_count >= abbrev_bytes:
# Now get 3 significant figures
@@ -139,11 +174,11 @@ class StatsObj:
if abbrev_count >= 100: precision = 0
elif abbrev_count >= 10: precision = 1
else: precision = 2
- return "%%.%df %s" % (precision, abbrev_string) \
+ return "%s%%.%df %s" % (sign, precision, abbrev_string) \
% (abbrev_count,)
byte_count = round(byte_count)
- if byte_count == 1: return "1 byte"
- else: return "%d bytes" % (byte_count,)
+ if byte_count == 1: return sign + "1 byte"
+ else: return "%s%d bytes" % (sign, byte_count)
def get_stats_logstring(self, title):
"""Like get_stats_string, but add header and footer"""
diff --git a/rdiff-backup/src/Globals.py b/rdiff-backup/src/Globals.py
index 0fc08f5..bf4a977 100644
--- a/rdiff-backup/src/Globals.py
+++ b/rdiff-backup/src/Globals.py
@@ -85,9 +85,6 @@ isbackup_writer = None
# Connection of the backup writer
backup_writer = None
-# True if this process is the client invoked by the user
-isclient = None
-
# Connection of the client
client_conn = None
@@ -171,6 +168,22 @@ select_source, select_mirror = None, None
# object. Access is provided to increment error counts.
ITRB = None
+# Percentage of time to spend sleeping. None means never sleep.
+sleep_ratio = None
+
+# security_level has 4 values and controls which requests from remote
+# systems will be honored. "all" means anything goes. "read-only"
+# means that the requests must not write to disk. "update-only" means
+# that requests shouldn't destructively update the disk (but normal
+# incremental updates are OK). "minimal" means only listen to a few
+# basic requests.
+security_level = "all"
+
+# If this is set, it indicates that the remote connection should only
+# deal with paths inside of restrict_path.
+restrict_path = None
+
+
def get(name):
"""Return the value of something in this module"""
return globals()[name]
@@ -199,6 +212,32 @@ def set_integer(name, val):
"received %s instead." % (name, val))
set(name, intval)
+def set_float(name, val, min = None, max = None, inclusive = 1):
+ """Like set, but make sure val is float within given bounds"""
+ def error():
+ s = "Variable %s must be set to a float" % (name,)
+ if min is not None and max is not None:
+ s += " between %s and %s " % (min, max)
+ if inclusive: s += "inclusive"
+ else: s += "not inclusive"
+ elif min is not None or max is not None:
+ if inclusive: inclusive_string = "or equal to "
+ else: inclusive_string = ""
+ if min is not None:
+ s += " greater than %s%s" % (inclusive_string, min)
+ else: s+= " less than %s%s" % (inclusive_string, max)
+ Log.FatalError(s)
+
+ try: f = float(val)
+ except ValueError: error()
+ if min is not None:
+ if inclusive and f < min: error()
+ elif not inclusive and f <= min: error()
+ if max is not None:
+ if inclusive and f > max: error()
+ elif not inclusive and f >= max: error()
+ set(name, f)
+
def get_dict_val(name, key):
"""Return val from dictionary in this class"""
return globals()[name][key]
diff --git a/rdiff-backup/src/Main.py b/rdiff-backup/src/Main.py
index 35624c2..06a1285 100644
--- a/rdiff-backup/src/Main.py
+++ b/rdiff-backup/src/Main.py
@@ -44,16 +44,17 @@ def parse_cmdlineoptions(arglist):
"checkpoint-interval=", "current-time=", "exclude=",
"exclude-device-files", "exclude-filelist=",
"exclude-filelist-stdin", "exclude-mirror=",
- "exclude-regexp=", "force", "include=",
- "include-filelist=", "include-filelist-stdin",
+ "exclude-other-filesystems", "exclude-regexp=", "force",
+ "include=", "include-filelist=", "include-filelist-stdin",
"include-regexp=", "list-increments", "mirror-only",
- "no-compression", "no-compression-regexp=",
- "no-hard-links", "no-resume", "null-separator",
- "parsable-output", "print-statistics", "quoting-char=",
- "remote-cmd=", "remote-schema=", "remove-older-than=",
- "restore-as-of=", "resume", "resume-window=", "server",
- "ssh-no-compression", "terminal-verbosity=",
- "test-server", "verbosity", "version", "windows-mode",
+ "no-compression", "no-compression-regexp=", "no-hard-links",
+ "no-resume", "null-separator", "parsable-output",
+ "print-statistics", "quoting-char=", "remote-cmd=",
+ "remote-schema=", "remove-older-than=", "restore-as-of=",
+ "restrict=", "restrict-read-only=", "restrict-update-only=",
+ "resume", "resume-window=", "server", "sleep-ratio=",
+ "ssh-no-compression", "terminal-verbosity=", "test-server",
+ "verbosity", "version", "windows-mode",
"windows-time-format"])
except getopt.error, e:
commandline_error("Bad commandline options: %s" % str(e))
@@ -80,6 +81,8 @@ def parse_cmdlineoptions(arglist):
select_files.append(sys.stdin)
elif opt == "--exclude-mirror":
select_mirror_opts.append(("--exclude", arg))
+ elif opt == "--exclude-other-filesystems":
+ select_opts.append((opt, arg))
elif opt == "--exclude-regexp": select_opts.append((opt, arg))
elif opt == "--force": force = 1
elif opt == "--include": select_opts.append((opt, arg))
@@ -99,23 +102,34 @@ def parse_cmdlineoptions(arglist):
elif opt == "--no-hard-links": Globals.set('preserve_hardlinks', 0)
elif opt == '--no-resume': Globals.resume = 0
elif opt == "--null-separator": Globals.set("null_separator", 1)
- elif opt == "-r" or opt == "--restore-as-of":
- restore_timestr, action = arg, "restore-as-of"
elif opt == "--parsable-output": Globals.set('parsable_output', 1)
elif opt == "--print-statistics":
Globals.set('print_statistics', 1)
elif opt == "--quoting-char":
Globals.set('quoting_char', arg)
Globals.set('quoting_enabled', 1)
+ elif opt == "-r" or opt == "--restore-as-of":
+ restore_timestr, action = arg, "restore-as-of"
elif opt == "--remote-cmd": remote_cmd = arg
elif opt == "--remote-schema": remote_schema = arg
elif opt == "--remove-older-than":
remove_older_than_string = arg
action = "remove-older-than"
+ elif opt == "--restrict": Globals.restrict_path = arg
+ elif opt == "--restrict-read-only":
+ Globals.security_level = "read-only"
+ Globals.restrict_path = arg
+ elif opt == "--restrict-update-only":
+ Globals.security_level = "update-only"
+ Globals.restrict_path = arg
elif opt == '--resume': Globals.resume = 1
elif opt == '--resume-window':
Globals.set_integer('resume_window', arg)
- elif opt == "-s" or opt == "--server": action = "server"
+ elif opt == "-s" or opt == "--server":
+ action = "server"
+ Globals.server = 1
+ elif opt == "--sleep-ratio":
+ Globals.set_float("sleep_ratio", arg, 0, 1, inclusive=0)
elif opt == "--ssh-no-compression":
Globals.set('ssh_compression', None)
elif opt == "--terminal-verbosity": Log.setterm_verbosity(arg)
@@ -176,7 +190,6 @@ def misc_setup(rps):
os.umask(077)
Time.setcurtime(Globals.current_time)
FilenameMapping.set_init_quote_vals()
- Globals.set("isclient", 1)
SetConnections.UpdateGlobal("client_conn", Globals.local_connection)
# This is because I originally didn't think compiled regexps
@@ -209,7 +222,9 @@ def Main(arglist):
"""Start everything up!"""
parse_cmdlineoptions(arglist)
set_action()
- rps = SetConnections.InitRPs(args, remote_schema, remote_cmd)
+ cmdpairs = SetConnections.get_cmd_pairs(args, remote_schema, remote_cmd)
+ Security.initialize(action, cmdpairs)
+ rps = map(SetConnections.cmdpair2rp, cmdpairs)
misc_setup(rps)
take_action(rps)
cleanup()
@@ -222,6 +237,7 @@ def Mirror(src_rp, dest_rp):
# Since no "rdiff-backup-data" dir, use root of destination.
SetConnections.UpdateGlobal('rbdir', dest_rp)
SetConnections.BackupInitConnections(src_rp.conn, dest_rp.conn)
+ backup_init_select(src_rp, dest_rp)
HighLevel.Mirror(src_rp, dest_rp)
def mirror_check_paths(rpin, rpout):
@@ -245,7 +261,7 @@ def Backup(rpin, rpout):
Time.setprevtime(prevtime)
HighLevel.Mirror_and_increment(rpin, rpout, incdir, RSI)
else: HighLevel.Mirror(rpin, rpout, incdir, RSI)
- backup_touch_curmirror(rpin, rpout)
+ rpout.conn.Main.backup_touch_curmirror_local(rpin, rpout)
def backup_init_select(rpin, rpout):
"""Create Select objects on source and dest connections"""
@@ -307,6 +323,7 @@ may need to use the --exclude option.""" % (rpout.path, rpin.path), 2)
def backup_get_mirrorrps():
"""Return list of current_mirror rps"""
+ datadir = Globals.rbdir
if not datadir.isdir(): return []
mirrorrps = [datadir.append(fn) for fn in datadir.listdir()
if fn.startswith("current_mirror.")]
@@ -324,12 +341,14 @@ went wrong during your last backup? Using """ + mirrorrps[-1].path, 2)
timestr = mirrorrps[-1].getinctime()
return Time.stringtotime(timestr)
-def backup_touch_curmirror(rpin, rpout):
+def backup_touch_curmirror_local(rpin, rpout):
"""Make a file like current_mirror.time.data to record time
- Also updates rpout so mod times don't get messed up.
+ Also updates rpout so mod times don't get messed up. This should
+ be run on the destination connection.
"""
+ datadir = Globals.rbdir
map(RPath.delete, backup_get_mirrorrps())
mirrorrp = datadir.append("current_mirror.%s.%s" % (Time.curtimestr,
"data"))
@@ -337,7 +356,6 @@ def backup_touch_curmirror(rpin, rpout):
mirrorrp.touch()
RPath.copy_attribs(rpin, rpout)
-
def restore(src_rp, dest_rp = None):
"""Main restoring function
@@ -474,23 +492,24 @@ def RemoveOlderThan(rootrp):
(datadir.path,))
try: time = Time.genstrtotime(remove_older_than_string)
- except TimeError, exc: Log.FatalError(str(exc))
+ except Time.TimeException, exc: Log.FatalError(str(exc))
timep = Time.timetopretty(time)
Log("Deleting increment(s) before %s" % timep, 4)
- itimes = [Time.stringtopretty(inc.getinctime())
- for inc in Restore.get_inclist(datadir.append("increments"))
- if Time.stringtotime(inc.getinctime()) < time]
-
- if not itimes:
+ times_in_secs = map(lambda inc: Time.stringtotime(inc.getinctime()),
+ Restore.get_inclist(datadir.append("increments")))
+ times_in_secs = filter(lambda t: t < time, times_in_secs)
+ if not times_in_secs:
Log.FatalError("No increments older than %s found" % timep)
- inc_pretty_time = "\n".join(itimes)
- if len(itimes) > 1 and not force:
+
+ times_in_secs.sort()
+ inc_pretty_time = "\n".join(map(Time.timetopretty, times_in_secs))
+ if len(times_in_secs) > 1 and not force:
Log.FatalError("Found %d relevant increments, dated:\n%s"
"\nIf you want to delete multiple increments in this way, "
- "use the --force." % (len(itimes), inc_pretty_time))
+ "use the --force." % (len(times_in_secs), inc_pretty_time))
Log("Deleting increment%sat times:\n%s" %
- (len(itimes) == 1 and " " or "s ", inc_pretty_time), 3)
+ (len(times_in_secs) == 1 and " " or "s ", inc_pretty_time), 3)
Manage.delete_earlier_than(datadir, time)
diff --git a/rdiff-backup/src/Security.py b/rdiff-backup/src/Security.py
new file mode 100644
index 0000000..e4630d7
--- /dev/null
+++ b/rdiff-backup/src/Security.py
@@ -0,0 +1,171 @@
+# Copyright 2002 Ben Escoto
+#
+# This file is part of rdiff-backup.
+#
+# rdiff-backup is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, Inc., 675 Mass Ave, Cambridge MA
+# 02139, USA; either version 2 of the License, or (at your option) any
+# later version; incorporated herein by reference.
+
+"""Functions to make sure remote requests are kosher"""
+
+import sys
+import Globals, tempfile
+from rpath import *
+
+class Violation(Exception):
+ """Exception that indicates an improper request has been received"""
+ pass
+
+
+# This will store the list of functions that will be honored from
+# remote connections.
+allowed_requests = None
+
+# This stores the list of global variables that the client can not
+# set on the server.
+disallowed_server_globals = ["server", "security_level", "restrict_path"]
+
+def initialize(action, cmdpairs):
+ """Initialize allowable request list and chroot"""
+ global allowed_requests
+ set_security_level(action, cmdpairs)
+ set_allowed_requests(Globals.security_level)
+
+def set_security_level(action, cmdpairs):
+ """If running client, set security level and restrict_path
+
+ To find these settings, we must look at the action to see what is
+ supposed to happen, and then look at the cmdpairs to see what end
+ the client is on.
+
+ """
+ def islocal(cmdpair): return not cmdpair[0]
+ def bothlocal(cp1, cp2): return islocal(cp1) and islocal(cp2)
+ def bothremote(cp1, cp2): return not islocal(cp1) and not islocal(cp2)
+ def getpath(cmdpair): return cmdpair[1]
+
+ if Globals.server: return
+ cp1 = cmdpairs[0]
+ if len(cmdpairs) > 1: cp2 = cmdpairs[1]
+
+ if action == "backup":
+ if bothlocal(cp1, cp2) or bothremote(cp1, cp2):
+ sec_level = "minimal"
+ rdir = tempfile.gettempdir()
+ elif islocal(cp1):
+ sec_level = "read-only"
+ rdir = getpath(cp1)
+ else:
+ assert islocal(cp2)
+ sec_level = "update-only"
+ rdir = getpath(cp2)
+ elif action == "restore" or action == "restore-as-of":
+ if len(cmdpairs) == 1 or bothlocal(cp1, cp2) or bothremote(cp1, cp2):
+ sec_level = "minimal"
+ rdir = tempfile.gettempdir()
+ elif islocal(cp1):
+ sec_level = "read-only"
+ else:
+ assert islocal(cp2)
+ sec_level = "all"
+ rdir = getpath(cp2)
+ elif action == "mirror":
+ if bothlocal(cp1, cp2) or bothremote(cp1, cp2):
+ sec_level = "minimal"
+ rdir = tempfile.gettempdir()
+ elif islocal(cp1):
+ sec_level = "read-only"
+ rdir = getpath(cp1)
+ else:
+ assert islocal(cp2)
+ sec_level = "all"
+ rdir = getpath(cp2)
+ elif (action == "test-server" or action == "list-increments" or
+ action == "calculate-average" or action == "remove-older-than"):
+ sec_level = "minimal"
+ rdir = tempfile.gettempdir()
+ else: assert 0, "Unknown action %s" % action
+
+ Globals.security_level = sec_level
+ Globals.restrict_path = rdir
+
+def set_allowed_requests(sec_level):
+ """Set the allowed requests list using the security level"""
+ global allowed_requests
+ if sec_level == "all": return
+ allowed_requests = ["VirtualFile.readfromid", "VirtualFile.closebyid",
+ "Globals.get", "Globals.is_not_None",
+ "Globals.get_dict_val",
+ "Log.open_logfile_allconn",
+ "Log.close_logfile_allconn",
+ "SetConnections.add_redirected_conn",
+ "RedirectedRun"]
+ if sec_level == "minimal": pass
+ elif sec_level == "read-only" or sec_level == "update-only":
+ allowed_requests.extend(["C.make_file_dict",
+ "os.getuid",
+ "os.listdir",
+ "Resume.ResumeCheck",
+ "HLSourceStruct.split_initial_dsiter",
+ "HLSourceStruct.get_diffs_and_finalize"])
+ if sec_level == "update-only":
+ allowed_requests. \
+ extend(["Log.open_logfile_local", "Log.close_logfile_local",
+ "Log.close_logfile_allconn", "Log.log_to_file",
+ "SaveState.init_filenames",
+ "SaveState.touch_last_file",
+ "HLDestinationStruct.get_sigs",
+ "HLDestinationStruct.patch_w_datadir_writes",
+ "HLDestinationStruct.patch_and_finalize",
+ "HLDestinationStruct.patch_increment_and_finalize",
+ "Main.backup_touch_curmirror_local",
+ "Globals.ITRB.increment_stat"])
+ if Globals.server:
+ allowed_requests.extend(["SetConnections.init_connection_remote",
+ "Log.setverbosity",
+ "Log.setterm_verbosity",
+ "Time.setcurtime_local",
+ "Time.setprevtime_local",
+ "FilenameMapping.set_init_quote_vals_local",
+ "Globals.postset_regexp_local",
+ "Globals.set_select",
+ "HLSourceStruct.set_session_info",
+ "HLDestinationStruct.set_session_info"])
+
+def vet_request(request, arglist):
+ """Examine request for security violations"""
+ #if Globals.server: sys.stderr.write(str(request) + "\n")
+ security_level = Globals.security_level
+ if Globals.restrict_path:
+ for arg in arglist:
+ if isinstance(arg, RPath): vet_rpath(arg)
+ if security_level == "all": return
+ if request.function_string in allowed_requests: return
+ if request.function_string == "Globals.set":
+ if Globals.server and arglist[0] not in disallowed_server_globals:
+ return
+ raise Violation("\nWarning Security Violation!\n"
+ "Bad request for function: %s\n"
+ "with arguments: %s\n" % (request.function_string,
+ arglist))
+
+def vet_rpath(rpath):
+ """Require rpath not to step outside retricted directory"""
+ if Globals.restrict_path and rpath.conn is Globals.local_connection:
+ normalized, restrict = rpath.normalize().path, Globals.restrict_path
+ components = normalized.split("/")
+ # 3 cases for restricted dir /usr/foo: /var, /usr/foobar, /usr/foo/..
+ if (not normalized.startswith(restrict) or
+ (len(normalized) > len(restrict) and
+ normalized[len(restrict)] != "/") or
+ ".." in components):
+ raise Violation("\nWarning Security Violation!\n"
+ "Request to handle path %s\n"
+ "which doesn't appear to be within "
+ "restrict path %s.\n" % (normalized, restrict))
+
+
+
+
diff --git a/rdiff-backup/src/SetConnections.py b/rdiff-backup/src/SetConnections.py
index 91edd7d..8d763a1 100644
--- a/rdiff-backup/src/SetConnections.py
+++ b/rdiff-backup/src/SetConnections.py
@@ -28,8 +28,13 @@ __conn_remote_cmds = [None]
class SetConnectionsException(Exception): pass
-def InitRPs(arglist, remote_schema = None, remote_cmd = None):
- """Map the given file descriptions into rpaths and return list"""
+def get_cmd_pairs(arglist, remote_schema = None, remote_cmd = None):
+ """Map the given file descriptions into command pairs
+
+ Command pairs are tuples cmdpair with length 2. cmdpair[0] is
+ None iff it describes a local path, and cmdpair[1] is the path.
+
+ """
global __cmd_schema
if remote_schema: __cmd_schema = remote_schema
elif not Globals.ssh_compression: __cmd_schema = __cmd_schema_no_compress
@@ -44,11 +49,10 @@ def InitRPs(arglist, remote_schema = None, remote_cmd = None):
elif remote_schema:
Log("Remote schema option ignored - no remote file "
"descriptions.", 2)
-
- cmd_pairs = map(desc2cmd_pairs, desc_pairs)
+ cmdpairs = map(desc2cmd_pairs, desc_pairs)
if remote_cmd: # last file description gets remote_cmd
cmd_pairs[-1] = (remote_cmd, cmd_pairs[-1][1])
- return map(cmdpair2rp, cmd_pairs)
+ return cmdpairs
def cmdpair2rp(cmd_pair):
"""Return RPath from cmd_pair (remote_cmd, filename)"""
diff --git a/rdiff-backup/src/Time.py b/rdiff-backup/src/Time.py
index 6220514..90d3ae8 100644
--- a/rdiff-backup/src/Time.py
+++ b/rdiff-backup/src/Time.py
@@ -25,17 +25,18 @@ _genstr_date_regexp1 = re.compile("^(?P<year>[0-9]{4})[-/]"
_genstr_date_regexp2 = re.compile("^(?P<month>[0-9]{1,2})[-/]"
"(?P<day>[0-9]{1,2})[-/](?P<year>[0-9]{4})$")
curtime = curtimestr = None
+been_awake_since = None # stores last time sleep() was run
def setcurtime(curtime = None):
"""Sets the current time in curtime and curtimestr on all systems"""
t = curtime or time.time()
for conn in Globals.connections:
- conn.Time.setcurtime_local(t, timetostring(t))
+ conn.Time.setcurtime_local(t)
-def setcurtime_local(timeinseconds, timestr):
+def setcurtime_local(timeinseconds):
"""Only set the current time locally"""
global curtime, curtimestr
- curtime, curtimestr = timeinseconds, timestr
+ curtime, curtimestr = timeinseconds, timetostring(timeinseconds)
def setprevtime(timeinseconds):
"""Sets the previous inc time in prevtime and prevtimestr"""
@@ -168,6 +169,25 @@ def cmp(time1, time2):
elif time1 == time2: return 0
else: return 1
+
+def sleep(sleep_ratio):
+ """Sleep for period to maintain given sleep_ratio
+
+ On my system sleeping for periods less than 1/20th of a second
+ doesn't seem to work very accurately, so accumulate at least that
+ much time before sleeping.
+
+ """
+ global been_awake_since
+ if been_awake_since is None: # first running
+ been_awake_since = time.time()
+ else:
+ elapsed_time = time.time() - been_awake_since
+ sleep_time = elapsed_time * (sleep_ratio/(1-sleep_ratio))
+ if sleep_time >= 0.05:
+ time.sleep(sleep_time)
+ been_awake_since = time.time()
+
def genstrtotime(timestr, curtime = None):
"""Convert a generic time string to a time in seconds"""
if curtime is None: curtime = globals()['curtime']
@@ -203,5 +223,3 @@ the day).""" % timestr)
t = stringtotime(timestr)
if t: return t
else: error()
-
-
diff --git a/rdiff-backup/src/cmodule.c b/rdiff-backup/src/cmodule.c
index cce87da..6a55ab4 100644
--- a/rdiff-backup/src/cmodule.c
+++ b/rdiff-backup/src/cmodule.c
@@ -49,7 +49,7 @@ static PyObject *c_make_file_dict(self, args)
size = PyLong_FromLongLong((LONG_LONG)sbuf.st_size);
inode = PyLong_FromLongLong((LONG_LONG)sbuf.st_ino);
#else
- size = PyInt_FromLong((long)sbuf.st_size);
+ size = PyInt_FromLong(sbuf.st_size);
inode = PyInt_FromLong((long)sbuf.st_ino);
#endif
mode = (long)sbuf.st_mode;
@@ -64,7 +64,7 @@ static PyObject *c_make_file_dict(self, args)
atime = PyLong_FromLongLong((LONG_LONG)sbuf.st_atime);
#else
mtime = PyInt_FromLong((long)sbuf.st_mtime);
- atime = PyLong_FromLongLong((long)sbuf.st_atime);
+ atime = PyInt_FromLong((long)sbuf.st_atime);
#endif
/* Build return dictionary from stat struct */
diff --git a/rdiff-backup/src/connection.py b/rdiff-backup/src/connection.py
index 91c44fe..20dd3db 100644
--- a/rdiff-backup/src/connection.py
+++ b/rdiff-backup/src/connection.py
@@ -48,11 +48,9 @@ class LocalConnection(Connection):
elif isinstance(__builtins__, dict): return __builtins__[name]
else: return __builtins__.__dict__[name]
- def __setattr__(self, name, value):
- globals()[name] = value
+ def __setattr__(self, name, value): globals()[name] = value
- def __delattr__(self, name):
- del globals()[name]
+ def __delattr__(self, name): del globals()[name]
def __str__(self): return "LocalConnection"
@@ -329,7 +327,9 @@ class PipeConnection(LowLevelPipeConnection):
arg_req_num, arg = self._get()
assert arg_req_num == req_num
argument_list.append(arg)
- try: result = apply(eval(request.function_string), argument_list)
+ try:
+ Security.vet_request(request, argument_list)
+ result = apply(eval(request.function_string), argument_list)
except: result = self.extract_exception()
self._put(result, req_num)
self.unused_request_numbers[req_num] = None
@@ -407,14 +407,31 @@ class RedirectedConnection(Connection):
self.routing_number = routing_number
self.routing_conn = Globals.connection_dict[routing_number]
+ def reval(self, function_string, *args):
+ """Evalution function_string on args on remote connection"""
+ return self.routing_conn.reval("RedirectedRun", self.conn_number,
+ function_string, *args)
+
def __str__(self):
return "RedirectedConnection %d,%d" % (self.conn_number,
self.routing_number)
def __getattr__(self, name):
- return EmulateCallable(self.routing_conn,
- "Globals.get_dict_val('connection_dict', %d).%s"
- % (self.conn_number, name))
+ return EmulateCallableRedirected(self.conn_number, self.routing_conn,
+ name)
+
+def RedirectedRun(conn_number, func, *args):
+ """Run func with args on connection with conn number conn_number
+
+ This function is meant to redirect requests from one connection to
+ another, so conn_number must not be the local connection (and also
+ for security reasons since this function is always made
+ available).
+
+ """
+ conn = Globals.connection_dict[conn_number]
+ assert conn is not Globals.local_connection, conn
+ return conn.reval(func, *args)
class EmulateCallable:
@@ -428,6 +445,18 @@ class EmulateCallable:
return EmulateCallable(self.connection,
"%s.%s" % (self.name, attr_name))
+class EmulateCallableRedirected:
+ """Used by RedirectedConnection in calls like conn.os.chmod(foo)"""
+ def __init__(self, conn_number, routing_conn, name):
+ self.conn_number, self.routing_conn = conn_number, routing_conn
+ self.name = name
+ def __call__(self, *args):
+ return apply(self.routing_conn.reval,
+ ("RedirectedRun", self.conn_number, self.name) + args)
+ def __getattr__(self, attr_name):
+ return EmulateCallableRedirected(self.conn_number, self.routing_conn,
+ "%s.%s" % (self.name, attr_name))
+
class VirtualFile:
"""When the client asks for a file over the connection, it gets this
@@ -499,7 +528,7 @@ class VirtualFile:
# everything has to be available here for remote connection's use, but
# put at bottom to reduce circularities.
-import Globals, Time, Rdiff, Hardlink, FilenameMapping, C
+import Globals, Time, Rdiff, Hardlink, FilenameMapping, C, Security, Main
from static import *
from lazy import *
from log import *
diff --git a/rdiff-backup/src/increment.py b/rdiff-backup/src/increment.py
index e3b7f5a..814322d 100644
--- a/rdiff-backup/src/increment.py
+++ b/rdiff-backup/src/increment.py
@@ -274,6 +274,7 @@ class IncrementITRB(StatsITRB):
def branch_process(self, branch):
"""Update statistics, and the has_changed flag if change in branch"""
+ if Globals.sleep_ratio is not None: Time.sleep(Globals.sleep_ratio)
if branch.changed: self.changed = 1
self.add_file_stats(branch)
@@ -288,7 +289,7 @@ class MirrorITRB(StatsITRB):
StatsITRB.__init__(self)
def start_process(self, index, diff_rorp, mirror_dsrp):
- """Initialize statistics, do actual writing to mirror"""
+ """Initialize statistics and do actual writing to mirror"""
self.start_stats(mirror_dsrp)
if diff_rorp and not diff_rorp.isplaceholder():
RORPIter.patchonce_action(None, mirror_dsrp, diff_rorp).execute()
@@ -312,6 +313,7 @@ class MirrorITRB(StatsITRB):
def branch_process(self, branch):
"""Update statistics with subdirectory results"""
+ if Globals.sleep_ratio is not None: Time.sleep(Globals.sleep_ratio)
self.add_file_stats(branch)
diff --git a/rdiff-backup/src/rorpiter.py b/rdiff-backup/src/rorpiter.py
index cfd2d5f..1e85d55 100644
--- a/rdiff-backup/src/rorpiter.py
+++ b/rdiff-backup/src/rorpiter.py
@@ -242,7 +242,9 @@ class RORPIter:
def init(): Hardlink.link_rp(diff_rorp, tf, basisrp)
return Robust.make_tf_robustaction(init, tf, basisrp)
elif basisrp and basisrp.isreg() and diff_rorp.isreg():
- assert diff_rorp.get_attached_filetype() == 'diff'
+ if diff_rorp.get_attached_filetype() != 'diff':
+ raise RPathException("File %s appears to have changed during"
+ " processing, skipping" % (basisrp.path,))
return Rdiff.patch_with_attribs_action(basisrp, diff_rorp)
else: # Diff contains whole file, just copy it over
if not basisrp: basisrp = base_rp.new_index(diff_rorp.index)
diff --git a/rdiff-backup/src/selection.py b/rdiff-backup/src/selection.py
index 58abdf0..c70857c 100644
--- a/rdiff-backup/src/selection.py
+++ b/rdiff-backup/src/selection.py
@@ -256,6 +256,8 @@ class Select:
self.add_selection_func(self.filelist_get_sf(
filelists[filelists_index], 0, arg))
filelists_index += 1
+ elif opt == "--exclude-other-filesystems":
+ self.add_selection_func(self.other_filesystems_get_sf(0))
elif opt == "--exclude-regexp":
self.add_selection_func(self.regexp_get_sf(arg, 0))
elif opt == "--include":
@@ -416,6 +418,17 @@ probably isn't what you meant.""" %
else: return (None, None) # dsrp greater, not initial sequence
else: assert 0, "Include is %s, should be 0 or 1" % (include,)
+ def other_filesystems_get_sf(self, include):
+ """Return selection function matching files on other filesystems"""
+ assert include == 0 or include == 1
+ root_devloc = self.dsrpath.getdevloc()
+ def sel_func(dsrp):
+ if dsrp.getdevloc() == root_devloc: return None
+ else: return include
+ sel_func.exclude = not include
+ sel_func.name = "Match other filesystems"
+ return sel_func
+
def regexp_get_sf(self, regexp_string, include):
"""Return selection function given by regexp_string"""
assert include == 0 or include == 1
diff --git a/rdiff-backup/src/statistics.py b/rdiff-backup/src/statistics.py
index e9f43dc..83a8858 100644
--- a/rdiff-backup/src/statistics.py
+++ b/rdiff-backup/src/statistics.py
@@ -28,7 +28,7 @@ class StatsObj:
'ChangedFiles',
'ChangedSourceSize', 'ChangedMirrorSize',
'IncrementFiles', 'IncrementFileSize')
- stat_misc_attrs = ('Errors',)
+ stat_misc_attrs = ('Errors', 'TotalDestinationSizeChange')
stat_time_attrs = ('StartTime', 'EndTime', 'ElapsedTime')
stat_attrs = (('Filename',) + stat_time_attrs +
stat_misc_attrs + stat_file_attrs)
@@ -65,6 +65,26 @@ class StatsObj:
"""Add 1 to value of attribute"""
self.__dict__[attr] += 1
+ def get_total_dest_size_change(self):
+ """Return total destination size change
+
+ This represents the total change in the size of the
+ rdiff-backup destination directory.
+
+ """
+ addvals = [self.NewFileSize, self.ChangedSourceSize,
+ self.IncrementFileSize]
+ subtractvals = [self.DeletedFileSize, self.ChangedMirrorSize]
+ for val in addvals + subtractvals:
+ if val is None:
+ result = None
+ break
+ else:
+ def addlist(l): return reduce(lambda x,y: x+y, l)
+ result = addlist(addvals) - addlist(subtractvals)
+ self.TotalDestinationSizeChange = result
+ return result
+
def get_stats_line(self, index, use_repr = 1):
"""Return one line abbreviated version of full stats string"""
file_attrs = map(lambda attr: str(self.get_stat(attr)),
@@ -95,7 +115,9 @@ class StatsObj:
def get_stats_string(self):
"""Return extended string printing out statistics"""
- return self.get_timestats_string() + self.get_filestats_string()
+ return "%s%s%s" % (self.get_timestats_string(),
+ self.get_filestats_string(),
+ self.get_miscstats_string())
def get_timestats_string(self):
"""Return portion of statistics string dealing with time"""
@@ -112,8 +134,6 @@ class StatsObj:
self.ElapsedTime = self.EndTime - self.StartTime
timelist.append("ElapsedTime %.2f (%s)\n" %
(self.ElapsedTime, Time.inttopretty(self.ElapsedTime)))
- if self.Errors is not None:
- timelist.append("Errors %d\n" % self.Errors)
return "".join(timelist)
def get_filestats_string(self):
@@ -130,8 +150,23 @@ class StatsObj:
return "".join(map(fileline, self.stat_file_pairs))
+ def get_miscstats_string(self):
+ """Return portion of extended stat string about misc attributes"""
+ misc_string = ""
+ tdsc = self.get_total_dest_size_change()
+ if tdsc is not None:
+ misc_string += ("TotalDestinationSizeChange %s (%s)\n" %
+ (tdsc, self.get_byte_summary_string(tdsc)))
+ if self.Errors is not None: misc_string += "Errors %d\n" % self.Errors
+ return misc_string
+
def get_byte_summary_string(self, byte_count):
"""Turn byte count into human readable string like "7.23GB" """
+ if byte_count < 0:
+ sign = "-"
+ byte_count = -byte_count
+ else: sign = ""
+
for abbrev_bytes, abbrev_string in self.byte_abbrev_list:
if byte_count >= abbrev_bytes:
# Now get 3 significant figures
@@ -139,11 +174,11 @@ class StatsObj:
if abbrev_count >= 100: precision = 0
elif abbrev_count >= 10: precision = 1
else: precision = 2
- return "%%.%df %s" % (precision, abbrev_string) \
+ return "%s%%.%df %s" % (sign, precision, abbrev_string) \
% (abbrev_count,)
byte_count = round(byte_count)
- if byte_count == 1: return "1 byte"
- else: return "%d bytes" % (byte_count,)
+ if byte_count == 1: return sign + "1 byte"
+ else: return "%s%d bytes" % (sign, byte_count)
def get_stats_logstring(self, title):
"""Like get_stats_string, but add header and footer"""
diff --git a/rdiff-backup/testing/commontest.py b/rdiff-backup/testing/commontest.py
index 24eb2cb..dd49394 100644
--- a/rdiff-backup/testing/commontest.py
+++ b/rdiff-backup/testing/commontest.py
@@ -54,6 +54,15 @@ def rdiff_backup(source_local, dest_local, src_dir, dest_dir,
os.system(" ".join(cmdargs))
+def cmd_schemas2rps(schema_list, remote_schema):
+ """Input list of file descriptions and the remote schema, return rps
+
+ File descriptions should be strings of the form 'hostname.net::foo'
+
+ """
+ return map(SetConnections.cmdpair2rp,
+ SetConnections.get_cmd_pairs(schema_list, remote_schema))
+
def InternalBackup(source_local, dest_local, src_dir, dest_dir,
current_time = None):
"""Backup src to dest internally
@@ -75,7 +84,7 @@ def InternalBackup(source_local, dest_local, src_dir, dest_dir,
dest_dir = "cd test2/tmp; python ../../server.py ../../%s::../../%s" \
% (SourceDir, dest_dir)
- rpin, rpout = SetConnections.InitRPs([src_dir, dest_dir], remote_schema)
+ rpin, rpout = cmd_schemas2rps([src_dir, dest_dir], remote_schema)
Main.misc_setup([rpin, rpout])
Main.Backup(rpin, rpout)
Main.cleanup()
@@ -92,7 +101,7 @@ def InternalMirror(source_local, dest_local, src_dir, dest_dir,
dest_dir = "cd test2/tmp; python ../../server.py ../../%s::../../%s" \
% (SourceDir, dest_dir)
- rpin, rpout = SetConnections.InitRPs([src_dir, dest_dir], remote_schema)
+ rpin, rpout = cmd_schemas2rps([src_dir, dest_dir], remote_schema)
Main.misc_setup([rpin, rpout])
Main.backup_init_select(rpin, rpout)
if not rpout.lstat(): rpout.mkdir()
@@ -127,8 +136,7 @@ def InternalRestore(mirror_local, dest_local, mirror_dir, dest_dir, time):
dest_dir = "cd test2/tmp; python ../../server.py ../../%s::../../%s" \
% (SourceDir, dest_dir)
- mirror_rp, dest_rp = SetConnections.InitRPs([mirror_dir, dest_dir],
- remote_schema)
+ mirror_rp, dest_rp = cmd_schemas2rps([mirror_dir, dest_dir], remote_schema)
Time.setcurtime()
inc = get_increment_rp(mirror_rp, time)
if inc: Main.restore(get_increment_rp(mirror_rp, time), dest_rp)
diff --git a/rdiff-backup/testing/connectiontest.py b/rdiff-backup/testing/connectiontest.py
index 1deadbe..ab80256 100644
--- a/rdiff-backup/testing/connectiontest.py
+++ b/rdiff-backup/testing/connectiontest.py
@@ -168,13 +168,22 @@ class RedirectedConnectionTest(unittest.TestCase):
def testBasic(self):
"""Test basic operations with redirection"""
+ self.conna.Globals.set("tmp_val", 1)
+ self.connb.Globals.set("tmp_val", 2)
+ assert self.conna.Globals.get("tmp_val") == 1
+ assert self.connb.Globals.get("tmp_val") == 2
+
self.conna.Globals.set("tmp_connb", self.connb)
self.connb.Globals.set("tmp_conna", self.conna)
assert self.conna.Globals.get("tmp_connb") is self.connb
assert self.connb.Globals.get("tmp_conna") is self.conna
- #self.conna.Test_SetConnGlobals(self.connb, "tmp_settest", 1)
- #assert self.connb.Globals.get("tmp_settest")
+ val = self.conna.reval("Globals.get('tmp_connb').Globals.get",
+ "tmp_val")
+ assert val == 2, val
+ val = self.connb.reval("Globals.get('tmp_conna').Globals.get",
+ "tmp_val")
+ assert val == 1, val
assert self.conna.reval("Globals.get('tmp_connb').pow", 2, 3) == 8
self.conna.reval("Globals.tmp_connb.reval",
diff --git a/rdiff-backup/testing/finaltest.py b/rdiff-backup/testing/finaltest.py
index 0a51485..43daef6 100644
--- a/rdiff-backup/testing/finaltest.py
+++ b/rdiff-backup/testing/finaltest.py
@@ -17,6 +17,7 @@ class Local:
def get_local_rp(extension):
return RPath(Globals.local_connection, "testfiles/" + extension)
+ vftrp = get_local_rp('various_file_types')
inc1rp = get_local_rp('increment1')
inc2rp = get_local_rp('increment2')
inc3rp = get_local_rp('increment3')
@@ -71,6 +72,19 @@ class PathSetter(unittest.TestCase):
print "executing " + cmdstr
assert not os.system(cmdstr)
+ def exec_rb_extra_args(self, time, extra_args, *args):
+ """Run rdiff-backup on given arguments"""
+ arglist = []
+ if time: arglist.append("--current-time %s" % str(time))
+ arglist.append(self.src_prefix + args[0])
+ if len(args) > 1:
+ arglist.append(self.dest_prefix + args[1])
+ assert len(args) == 2
+
+ cmdstr = "%s %s %s" % (self.rb_schema, extra_args, ' '.join(arglist))
+ print "executing " + cmdstr
+ assert not os.system(cmdstr)
+
def exec_rb_restore(self, time, *args):
"""Restore using rdiff-backup's new syntax and given time"""
arglist = []
@@ -174,6 +188,23 @@ class Final(PathSetter):
self.set_connections("test1/", '../', 'test2/tmp/', '../../')
self.runtest()
+ def testMirroringLocal(self):
+ """Run mirroring only everything remote"""
+ self.delete_tmpdirs()
+ self.set_connections(None, None, None, None)
+ self.exec_rb_extra_args(10000, "-m",
+ "testfiles/various_file_types",
+ "testfiles/output")
+ assert CompareRecursive(Local.vftrp, Local.rpout, exclude_rbdir = None)
+
+ def testMirroringRemote(self):
+ """Run mirroring only everything remote"""
+ self.delete_tmpdirs()
+ self.set_connections("test1/", "../", "test2/tmp/", "../../")
+ self.exec_rb_extra_args(10000, "-m",
+ "testfiles/various_file_types",
+ "testfiles/output")
+ assert CompareRecursive(Local.vftrp, Local.rpout, exclude_rbdir = None)
class FinalSelection(PathSetter):
"""Test selection options"""
diff --git a/rdiff-backup/testing/securitytest.py b/rdiff-backup/testing/securitytest.py
new file mode 100644
index 0000000..689544d
--- /dev/null
+++ b/rdiff-backup/testing/securitytest.py
@@ -0,0 +1,60 @@
+import os, unittest
+from commontest import *
+import rdiff_backup.Security, Security
+
+#Log.setverbosity(5)
+
+class SecurityTest(unittest.TestCase):
+ def assert_exc_sec(self, exc):
+ """Fudge - make sure exception is a security violation
+
+ This is necessary because of some kind of pickling/module
+ problem.
+
+ """
+ assert isinstance(exc, rdiff_backup.Security.Violation)
+ #assert str(exc).find("Security") >= 0, "%s\n%s" % (exc, repr(exc))
+
+ def test_vet_request_ro(self):
+ """Test vetting of ConnectionRequests on read-only server"""
+ remote_cmd = "rdiff-backup --server --restrict-read-only foo"
+ conn = SetConnections.init_connection(remote_cmd)
+ assert type(conn.os.getuid()) is type(5)
+ try: conn.os.remove("/tmp/foobar")
+ except Exception, e: self.assert_exc_sec(e)
+ else: assert 0, "No exception raised"
+ SetConnections.CloseConnections()
+
+ def test_vet_request_minimal(self):
+ """Test vetting of ConnectionRequests on minimal server"""
+ remote_cmd = "rdiff-backup --server --restrict-update-only foo"
+ conn = SetConnections.init_connection(remote_cmd)
+ assert type(conn.os.getuid()) is type(5)
+ try: conn.os.remove("/tmp/foobar")
+ except Exception, e: self.assert_exc_sec(e)
+ else: assert 0, "No exception raised"
+ SetConnections.CloseConnections()
+
+ def test_vet_rpath(self):
+ """Test to make sure rpaths not in restricted path will be rejected"""
+ remote_cmd = "rdiff-backup --server --restrict-update-only foo"
+ conn = SetConnections.init_connection(remote_cmd)
+
+ for rp in [RPath(Globals.local_connection, "blahblah"),
+ RPath(conn, "foo/bar")]:
+ conn.Globals.set("TEST_var", rp)
+ assert conn.Globals.get("TEST_var").path == rp.path
+
+ for rp in [RPath(conn, "foobar"),
+ RPath(conn, "/usr/local"),
+ RPath(conn, "foo/../bar")]:
+ try: conn.Globals.set("TEST_var", rp)
+ except Exception, e:
+ self.assert_exc_sec(e)
+ continue
+ assert 0, "No violation raised by rp %s" % (rp,)
+
+ SetConnections.CloseConnections()
+
+if __name__ == "__main__": unittest.main()
+
diff --git a/rdiff-backup/testing/selectiontest.py b/rdiff-backup/testing/selectiontest.py
index 76af96f..f512f12 100644
--- a/rdiff-backup/testing/selectiontest.py
+++ b/rdiff-backup/testing/selectiontest.py
@@ -221,6 +221,18 @@ testfiles/select/1/1
select.filelist_get_sf(StringIO.StringIO("/foo/bar"), 0,
"test")(root) == None
+ def testOtherFilesystems(self):
+ """Test to see if --exclude-other-filesystems works correctly"""
+ root = DSRPath(1, Globals.local_connection, "/")
+ select = Select(root)
+ sf = select.other_filesystems_get_sf(0)
+ assert sf(root) is None
+ assert sf(RPath(Globals.local_connection, "/usr/bin")) is None, \
+ "Assumption: /usr/bin is on the same filesystem as /"
+ assert sf(RPath(Globals.local_connection, "/proc")) == 0, \
+ "Assumption: /proc is on a different filesystem"
+ assert sf(RPath(Globals.local_connection, "/boot")) == 0, \
+ "Assumption: /boot is on a different filesystem"
class ParseArgsTest(unittest.TestCase):
"""Test argument parsing"""
diff --git a/rdiff-backup/testing/statisticstest.py b/rdiff-backup/testing/statisticstest.py
index 819bb85..cc1f675 100644
--- a/rdiff-backup/testing/statisticstest.py
+++ b/rdiff-backup/testing/statisticstest.py
@@ -57,6 +57,7 @@ ChangedSourceSize 8 (8 bytes)
ChangedMirrorSize 9 (9 bytes)
IncrementFiles 15
IncrementFileSize 10 (10 bytes)
+TotalDestinationSizeChange 7 (7 bytes)
""", "'%s'" % stats_string
def test_line_string(self):
diff --git a/rdiff-backup/testing/timetest.py b/rdiff-backup/testing/timetest.py
index 089ae0c..b6d545f 100644
--- a/rdiff-backup/testing/timetest.py
+++ b/rdiff-backup/testing/timetest.py
@@ -1,4 +1,4 @@
-import unittest
+import unittest, time
from commontest import *
import Globals, Time
@@ -108,5 +108,29 @@ class TimeTest(unittest.TestCase):
self.assertRaises(Time.TimeException, g2t, "")
self.assertRaises(Time.TimeException, g2t, "3q")
+ def testSleeping(self):
+ """Test sleep and sleep ratio"""
+ sleep_ratio = 0.5
+ time1 = time.time()
+ Time.sleep(0) # set initial time
+ time.sleep(1)
+ time2 = time.time()
+ Time.sleep(sleep_ratio)
+ time3 = time.time()
+ time.sleep(0.5)
+ time4 = time.time()
+ Time.sleep(sleep_ratio)
+ time5 = time.time()
+
+ sleep_ratio = 0.25
+ time.sleep(0.75)
+ time6 = time.time()
+ Time.sleep(sleep_ratio)
+ time7 = time.time()
+
+ assert 0.9 < time3 - time2 < 1.1, time3 - time2
+ assert 0.4 < time5 - time4 < 0.6, time5 - time4
+ assert 0.2 < time7 - time6 < 0.3, time7 - time6
+
if __name__ == '__main__': unittest.main()