Commit 1213096f authored by ben's avatar ben

Various changes to 0.9.3, see CHANGELOG


git-svn-id: http://svn.savannah.nongnu.org/svn/rdiff-backup@157 2b77aa54-bcbc-44c9-a7ec-4f6cf2b41109
parent 05dea566
......@@ -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]
......
......@@ -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)
# 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))
......@@ -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)"""
......
......@@ -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()
......@@ -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 */
......
......@@ -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 *
......
......@@ -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)
......
......@@ -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)
......
......@@ -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
......
......@@ -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"""
......
......@@ -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]
......
......@@ -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)
# 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))
......@@ -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)"""
......
......@@ -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()
......@@ -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 */
......
......@@ -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 *
......
......@@ -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)
......
......@@ -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)
......
......@@ -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
......
......@@ -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"""
......
......@@ -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)
......
......@@ -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",
......
......@@ -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"""
......
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()
......@@ -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"""
......
......@@ -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):
......
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()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment