Commit 3b7e5592 authored by bescoto's avatar bescoto

Changed handling of ownership, added --user/group-mapping-file options


git-svn-id: http://svn.savannah.nongnu.org/svn/rdiff-backup@436 2b77aa54-bcbc-44c9-a7ec-4f6cf2b41109
parent a89c36c2
New in v0.13.2 (??????????)
---------------------------
Change ownership policy and added --user-mapping-file and
--group-mapping-file switches. See man page for more information.
Specified socket type as SOCK_STREAM. (Error reported by Erik
Forsberg.)
......@@ -13,9 +16,11 @@ If there is data missing from the destination dir (for instance if a
user mistakenly deletes it), only warn when restoring, instead of
exiting with error.
Fixed bug in EA/ACL restoring, noticed by Greg Freemyer. Also updated
quoting of filenames and extended attributes names to match
forthcoming attr/facl utilities.
Fixed bug in EA/ACL restoring, noticed by Greg Freemyer.
Updated quoting of filenames and extended attributes names to match
forthcoming attr/facl utilities. Strange characters should now be
properly escaped.
Fixed problems with --restrict options that would cause proper
sessions to fail. Thanks to Randall Nortman for error report.
......@@ -27,6 +32,9 @@ by Alan Bailward.
File examples.html added to distribution; examples section removed
from man page.
Removed option --no-change-dir-inc-perms. Instead when copying
permissions to directory increments, mask with 0777.
New in v0.13.1 (2003/08/08)
---------------------------
......
Look at manual page examples.
Consider adding --datadir option (Jean-Sébastien GOETSCHY)
See if regressing takes too much memory (large directories).
......
......@@ -116,7 +116,8 @@ def MakeTar():
"robust.py", "rorpiter.py", "rpath.py",
"Security.py", "selection.py",
"SetConnections.py", "static.py",
"statistics.py", "TempFile.py", "Time.py"]:
"statistics.py", "TempFile.py", "Time.py",
"user_group.py"]:
assert not os.system("cp %s/%s %s/rdiff_backup" %
(SourceDir, filename, tardir)), filename
......
......@@ -134,6 +134,13 @@ Exclude all device files, fifos, sockets, and symlinks.
Authorize the updating or overwriting of a destination path.
rdiff-backup will generally tell you if it needs this.
.TP
.BI "--group-mapping-file " filename
Map group names and ids according the the group mapping file
.IR filename .
See the
.B USERS AND GROUPS
section for more information.
.TP
.BI "--include " shell_pattern
Similar to
.B --exclude
......@@ -198,10 +205,6 @@ to --remove-older-than. Specifying a subdirectory is allowable; then
only the sizes of the mirror and increments pertaining to that
subdirectory will be listed.
.TP
.B --no-change-dir-inc-perms
Do not change the permissions of the directory increments to match the
directories they represent.
.TP
.B --no-compare-inode
This relatively esoteric option prevents rdiff-backup from flagging a
file as changed when its inode changes. This option may be useful if
......@@ -338,6 +341,13 @@ Test for the presence of a compatible rdiff-backup server as specified
in the following host::filename argument(s). The filename section
will be ignored.
.TP
.BI "--user-mapping-file " filename
Map user names and ids according to the user mapping file
.IR filename .
See the
.B USERS and GROUPS
section for more information.
.TP
.BI -v [0-9] ", --verbosity " [0-9]
Specify verbosity level (0 is totally silent, 3 is the default, and 9
is noisiest). This determines how much is written to the log file.
......@@ -735,8 +745,50 @@ matches any files whose full pathnames contain 7 consecutive digits
which aren't followed by 'foo'. However, it wouldn't match /home even
if /home/ben/1234567 existed.
.SH STATISTICS
.SH USERS AND GROUPS
There can be complications preserving ownership across systems. For
instance the username that owns a file on the source system may not
exist on the destination. Here is how rdiff-backup maps ownership on
the source to the destination:
.TP
.B 1.
Attempt to preserve the user and group names for ownership and in
ACLs. This may result in files having different uids and gids across
systems.
.TP
.B 2.
If this fails (e.g. because the username does not exist), preserve the
original id, but only in cases of user and group ownership. For ACLs,
omit any entry that has a bad user or group name.
.TP
.B 3.
However, the
.B --user-mapping-file
and
.B --group-mapping-file
options can override this behavior. If either of these options is
given, the policy descriped in 1 and 2 above will be followed, but
with the mapped user and group instead of the original.
.RE
The user and group mapping files both have the same form:
.RS
old_name_or_id1:new_name_or_id1
.RE
.RS
old_name_or_id2:new_name_or_id2
.RE
.RS
<etc>
.RE
Each line should contain a name or id, followed by a colon ":",
followed by another name or id. If a name or id is not listed, they
are treated in the default way described above.
.SH STATISTICS
Every session rdiff-backup saves various statistics into two files,
the session statistics file at
rdiff-backup-data/session_statistics.<time>.data and the directory
......@@ -766,7 +818,6 @@ The log file is not compressed and can become quite large if
rdiff-backup is run with high verbosity.
.SH EXIT STATUS
If rdiff-backup finishes successfully, the exit status will be 0. If
there is an error, it will be non-zero (usually 1, but don't depend on
this specific value). When setting up rdiff-backup to run
......
......@@ -207,11 +207,6 @@ compare_inode = 1
# guarantee that any changes have been committed to disk.
fsync_directories = 1
# If set, directory increments are given the same permissions as the
# directories they represent. Otherwise they have the default
# permissions.
change_dir_inc_perms = 1
def get(name):
"""Return the value of something in this module"""
......
......@@ -32,6 +32,7 @@ remote_cmd, remote_schema = None, None
force = None
select_opts = []
select_files = []
user_mapping_filename, group_mapping_filename = None, None
# These are global because they are set while we are trying to figure
# whether to restore or to backup
restore_root, restore_index, restore_root_set = None, None, 0
......@@ -40,6 +41,7 @@ def parse_cmdlineoptions(arglist):
"""Parse argument list and set global preferences"""
global args, action, force, restore_timestr, remote_cmd, remote_schema
global remove_older_than_string
global user_mapping_filename, group_mapping_filename
def sel_fl(filename):
"""Helper function for including/excluding filelists below"""
try: return open(filename, "r")
......@@ -51,20 +53,19 @@ def parse_cmdlineoptions(arglist):
"exclude-filelist=", "exclude-filelist-stdin",
"exclude-globbing-filelist=", "exclude-mirror=",
"exclude-other-filesystems", "exclude-regexp=",
"exclude-special-files", "force", "include=",
"include-filelist=", "include-filelist-stdin",
"exclude-special-files", "force", "group-mapping-file=",
"include=", "include-filelist=", "include-filelist-stdin",
"include-globbing-filelist=", "include-regexp=",
"list-at-time=", "list-changed-since=", "list-increments",
"list-increment-sizes", "no-compare-inode",
"no-change-dir-inc-perms", "no-compression",
"no-compression-regexp=", "no-file-statistics",
"no-hard-links", "null-separator",
"no-compression", "no-compression-regexp=",
"no-file-statistics", "no-hard-links", "null-separator",
"override-chars-to-quote=", "parsable-output",
"print-statistics", "remote-cmd=", "remote-schema=",
"remove-older-than=", "restore-as-of=", "restrict=",
"restrict-read-only=", "restrict-update-only=", "server",
"ssh-no-compression", "terminal-verbosity=", "test-server",
"verbosity=", "version"])
"user-mapping-file=", "verbosity=", "version"])
except getopt.error, e:
commandline_error("Bad commandline options: %s" % str(e))
......@@ -89,6 +90,7 @@ def parse_cmdlineoptions(arglist):
opt == "--exclude-regexp" or
opt == "--exclude-special-files"): select_opts.append((opt, arg))
elif opt == "--force": force = 1
elif opt == "--group-mapping-file": group_mapping_filename = arg
elif opt == "--include": select_opts.append((opt, arg))
elif opt == "--include-filelist":
select_opts.append((opt, arg))
......@@ -107,8 +109,6 @@ def parse_cmdlineoptions(arglist):
elif opt == "-l" or opt == "--list-increments":
action = "list-increments"
elif opt == '--list-increment-sizes': action = 'list-increment-sizes'
elif opt == '--no-change-dir-inc-perms':
Globals.set('change_dir_inc_perms', 0)
elif opt == "--no-compare-inode": Globals.set("compare_inode", 0)
elif opt == "--no-compression": Globals.set("compression", None)
elif opt == "--no-compression-regexp":
......@@ -141,6 +141,7 @@ def parse_cmdlineoptions(arglist):
Globals.set('ssh_compression', None)
elif opt == "--terminal-verbosity": Log.setterm_verbosity(arg)
elif opt == "--test-server": action = "test-server"
elif opt == "--user-mapping-file": user_mapping_filename = arg
elif opt == "-V" or opt == "--version":
print "rdiff-backup " + Globals.version
sys.exit(0)
......@@ -190,6 +191,21 @@ def misc_setup(rps):
conn.robust.install_signal_handlers()
conn.Hardlink.initialize_dictionaries()
def init_user_group_mapping(destination_conn):
"""Initialize user and group mapping on destination connection"""
global user_mapping_filename, group_mapping_filename
def get_string_from_file(filename):
if not filename: return None
rp = rpath.RPath(Globals.local_connection, filename)
try: return rp.get_data()
except OSError, e:
log.FatalError("Error '%s' reading mapping file '%s'" %
(str(e), filename))
user_mapping_string = get_string_from_file(user_mapping_filename)
destination_conn.user_group.init_user_mapping(user_mapping_string)
group_mapping_string = get_string_from_file(group_mapping_filename)
destination_conn.user_group.init_group_mapping(group_mapping_string)
def take_action(rps):
"""Do whatever action says"""
if action == "server":
......@@ -237,6 +253,7 @@ def Backup(rpin, rpout):
backup_set_fs_globals(rpin, rpout)
if Globals.chars_to_quote: rpout = backup_quoted_rpaths(rpout)
backup_final_init(rpout)
init_user_group_mapping(rpout.conn)
backup_set_select(rpin)
if prevtime:
rpout.conn.Main.backup_touch_curmirror_local(rpin, rpout)
......@@ -363,7 +380,6 @@ def backup_set_fs_globals(rpin, rpout):
update_bool_global('write_eas', Globals.read_eas and dest_fsa.eas)
update_bool_global('write_resource_forks',
Globals.read_resource_forks and dest_fsa.resource_forks)
update_bool_global('change_dir_inc_perms', dest_fsa.dir_inc_perms)
SetConnections.UpdateGlobal('chars_to_quote', dest_fsa.chars_to_quote)
if Globals.chars_to_quote:
for conn in Globals.connections:
......@@ -417,6 +433,7 @@ def Restore(src_rp, dest_rp, restore_as_of = None):
try: time = Time.genstrtotime(restore_timestr, rp = inc_rpath)
except Time.TimeException, exc: Log.FatalError(str(exc))
else: time = src_rp.getinctime()
init_user_group_mapping(dest_rp.conn)
restore_set_select(restore_root, dest_rp)
restore_start_log(src_rp, dest_rp, time)
restore.Restore(restore_root.new_index(restore_index),
......@@ -664,6 +681,7 @@ def CheckDest(dest_rp):
elif need_check == 0:
Log.FatalError("Destination dir %s does not need checking" %
(dest_rp.path,))
init_user_group_mapping(dest_rp.conn)
dest_rp.conn.regress.Regress(dest_rp)
def checkdest_need_check(dest_rp):
......
......@@ -519,7 +519,7 @@ import Globals, Time, Rdiff, Hardlink, FilenameMapping, C, Security, \
Main, rorpiter, selection, increment, statistics, manage, lazy, \
iterfile, rpath, robust, restore, manage, backup, connection, \
TempFile, SetConnections, librsync, log, regress, fs_abilities, \
eas_acls
eas_acls, user_group
Globals.local_connection = LocalConnection()
Globals.connections.append(Globals.local_connection)
......
......@@ -86,17 +86,14 @@ def makediff(new, mirror, incpref):
Rdiff.write_delta(new, mirror, diff, compress)
new.chmod(old_new_perms)
else: Rdiff.write_delta(new, mirror, diff, compress)
rpath.copy_attribs(mirror, diff)
rpath.copy_attribs_inc(mirror, diff)
return diff
def makedir(mirrordir, incpref):
"""Make file indicating directory mirrordir has changed"""
dirsign = get_inc(incpref, "dir")
dirsign.touch()
if Globals.change_dir_inc_perms:
# Below, don't copy acls because directories can have more of
# them than ordinary files (they have default acls also).
rpath.copy_attribs(mirrordir, dirsign, acls = 0)
rpath.copy_attribs_inc(mirrordir, dirsign)
return dirsign
def get_inc(rp, typestr, time = None):
......
......@@ -107,7 +107,9 @@ def RORP2Record(rorpath):
# Add user, group, and permission information
uid, gid = rorpath.getuidgid()
str_list.append(" Uid %s\n" % uid)
str_list.append(" Uname %s\n" % rorpath.getuname() or ":")
str_list.append(" Gid %s\n" % gid)
str_list.append(" Gname %s\n" % rorpath.getgname() or ":")
str_list.append(" Permissions %s\n" % rorpath.getperms())
return "".join(str_list)
......@@ -140,6 +142,12 @@ def Record2RORP(record_string):
elif field == "ModTime": data_dict['mtime'] = long(data)
elif field == "Uid": data_dict['uid'] = int(data)
elif field == "Gid": data_dict['gid'] = int(data)
elif field == "Uname":
if data == ":": data_dict['uname'] = None
else: data_dict['uname'] = data
elif field == "Gname":
if data == ':': data_dict['gname'] = None
else: data_dict['gname'] = data
elif field == "Permissions": data_dict['perms'] = int(data)
else: raise ParsingError("Unknown field in line '%s'" % line)
return rpath.RORPath(index, data_dict)
......
......@@ -36,7 +36,7 @@ are dealing with are local or remote.
"""
import os, stat, re, sys, shutil, gzip, socket, time
import Globals, Time, static, log
import Globals, Time, static, log, user_group
class SkipFileException(Exception):
......@@ -143,7 +143,7 @@ def cmp(rpin, rpout):
elif rpin.issock(): return rpout.issock()
else: raise RPathException("File %s has unknown type" % rpin.path)
def copy_attribs(rpin, rpout, acls = 1):
def copy_attribs(rpin, rpout):
"""Change file attributes of rpout to match rpin
Only changes the chmoddable bits, uid/gid ownership, and
......@@ -151,14 +151,36 @@ def copy_attribs(rpin, rpout, acls = 1):
"""
log.Log("Copying attributes from %s to %s" % (rpin.index, rpout.path), 7)
assert rpin.lstat() == rpout.lstat() is not None, "different file types"
if rpin.issym(): return # symlinks have no valid attributes
if Globals.write_resource_forks and rpin.isreg():
rpout.write_resource_fork(rpin.get_resource_fork())
if Globals.write_eas: rpout.write_ea(rpin.get_ea())
if Globals.change_ownership: rpout.chown(*user_group.map_rpath(rpin))
rpout.chmod(rpin.getperms())
if Globals.write_acls: rpout.write_acl(rpin.get_acl())
if not rpin.isdev(): rpout.setmtime(rpin.getmtime())
def copy_attribs_inc(rpin, rpout):
"""Change file attributes of rpout to match rpin
Like above, but used to give increments the same attributes as the
originals. Therefore, don't copy all directory acl and
permissions.
"""
log.Log("Copying inc attrs from %s to %s" % (rpin.index, rpout.path), 7)
check_for_files(rpin, rpout)
if rpin.issym(): return # symlinks have no valid attributes
if Globals.write_resource_forks and rpin.isreg() and rpout.isreg():
rpout.write_resource_fork(rpin.get_resource_fork())
if Globals.write_eas: rpout.write_ea(rpin.get_ea())
if Globals.change_ownership: apply(rpout.chown, rpin.getuidgid())
rpout.chmod(rpin.getperms())
if Globals.write_acls and acls: rpout.write_acl(rpin.get_acl())
if rpin.isdir() and not rpout.isdir():
rpout.chmod(rpin.getperms() & 0777)
else: rpout.chmod(rpin.getperms())
if Globals.write_acls and not (rpin.isdir() and not rpout.isdir()):
rpout.write_acl(rpin.get_acl())
if not rpin.isdev(): rpout.setmtime(rpin.getmtime())
def cmp_attribs(rp1, rp2):
......@@ -265,7 +287,7 @@ class RORPath:
if self.index != other.index: return None
for key in self.data.keys(): # compare dicts key by key
if (key == 'uid' or key == 'gid') and self.issym():
if self.issym() and key in ('uid', 'gid', 'uname', 'gname'):
pass # Don't compare gid/uid for symlinks
elif key == 'atime' and not Globals.preserve_atime: pass
elif key == 'ctime': pass
......@@ -293,11 +315,7 @@ class RORPath:
"""
for key in self.data.keys(): # compare dicts key by key
if ((key == 'uid' or key == 'gid') and
(self.issym() or not Globals.change_ownership)):
# Don't compare gid/uid for symlinks, and only root
# can change ownership
pass
if key in ('uid', 'gid', 'uname', 'gname'): pass
elif (key == 'type' and self.isspecial() and
other.isreg() and other.getsize() == 0):
pass # Special files may be replaced with empty regular files
......@@ -312,6 +330,11 @@ class RORPath:
pass
elif (not other.data.has_key(key) or
self.data[key] != other.data[key]): return 0
if self.lstat() and not self.issym() and Globals.change_ownership:
# Now compare ownership. Symlinks don't have ownership
if user_group.map_rpath(self) != other.getuidgid(): return 0
return 1
def equal_verbose(self, other, check_index = 1,
......@@ -323,7 +346,7 @@ class RORPath:
return None
for key in self.data.keys(): # compare dicts key by key
if ((key == 'uid' or key == 'gid') and
if (key in ('uid', 'gid', 'uname', 'gname') and
(self.issym() or not compare_ownership)):
# Don't compare gid/uid for symlinks, or if told not to
pass
......@@ -425,6 +448,14 @@ class RORPath:
"""Return permission block of file"""
return self.data['perms']
def getuname(self):
"""Return username that owns the file"""
return self.data['uname']
def getgname(self):
"""Return groupname that owns the file"""
return self.data['gname']
def hassize(self):
"""True if rpath has a size parameter"""
return self.data.has_key('size')
......@@ -620,10 +651,11 @@ class RPath(RORPath):
def setdata(self):
"""Set data dictionary using C extension"""
self.data = self.conn.C.make_file_dict(self.path)
if Globals.read_eas and self.lstat():
self.data['ea'] = self.conn.rpath.ea_get(self)
if Globals.read_acls and self.lstat():
self.data['acl'] = self.conn.rpath.acl_get(self)
if not self.lstat(): return
self.data['uname'] = self.conn.user_group.uid2uname(self.data['uid'])
self.data['gname'] = self.conn.user_group.gid2gname(self.data['gid'])
if Globals.read_eas: self.data['ea'] = self.conn.rpath.ea_get(self)
if Globals.read_acls: self.data['acl'] = self.conn.rpath.acl_get(self)
if Globals.read_resource_forks and self.isreg():
self.get_resource_fork()
......@@ -1063,3 +1095,4 @@ class RPathFileHook:
# problems.
def acl_get(rp): assert 0
def ea_get(rp): assert 0
......@@ -65,7 +65,6 @@ class Map:
"""Used for mapping names and id on source side to dest side"""
def __init__(self, name2id_func):
"""Map initializer, set dictionaries"""
assert Globals.isdest, "Should run on destination connection"
self.name2id_dict = {}
self.name2id_func = name2id_func
......@@ -150,5 +149,9 @@ def init_group_mapping(mapping_string):
else: GroupMap = Map(name2id_func)
def map_rpath(rp):
"""Return (uid, gid) of mapped ownership of given rpath"""
old_uid, old_gid = rp.getuidgid()
new_uid = UserMap.get_id(old_uid, rp.getuname())
new_gid = GroupMap.get_id(old_gid, rp.getgname())
return (new_uid, new_gid)
......@@ -51,11 +51,10 @@ def rdiff_backup(source_local, dest_local, src_dir, dest_dir,
"""
if not source_local:
src_dir = ("cd test1; ../%s/rdiff-backup --server::../%s" %
(SourceDir, src_dir))
src_dir = ("'cd test1; ../%s --server'::../%s" % (RBBin, src_dir))
if not dest_local:
dest_dir = ("test2/tmp; ../../%s/rdiff-backup --server::../../%s" %
(SourceDir, dest_dir))
dest_dir = ("'cd test2/tmp; ../../%s --server'::../../%s" %
(RBBin, dest_dir))
cmdargs = [RBBin, extra_options]
if not (source_local and dest_local): cmdargs.append("--remote-schema %s")
......
......@@ -14,6 +14,7 @@ Globals.counter = 0
verbosity = 6
log.Log.setverbosity(verbosity)
user = 'ben' # Non-root user to su to
userid = 500 # id of user above
assert os.getuid() == 0, "Run this test as root!"
def Run(cmd):
......@@ -28,6 +29,50 @@ class RootTest(unittest.TestCase):
def testLocal2(self): BackupRestoreSeries(1, 1, self.dirlist2)
def testRemote(self): BackupRestoreSeries(None, None, self.dirlist1)
def test_ownership_mapping(self):
"""Test --user-mapping-file and --group-mapping-file options"""
def write_ownership_dir():
"""Write the directory testfiles/root_mapping"""
rp = rpath.RPath(Globals.local_connection,
"testfiles/root_mapping")
if rp.lstat(): Myrm(rp.path)
rp.mkdir()
rp1 = rp.append('1')
rp1.touch()
rp2 = rp.append('2')
rp2.touch()
rp2.chown(userid, 1) # use groupid 1, usually bin
return rp
def write_mapping_files(dir_rp):
"""Write user and group mapping files, return paths"""
user_map_rp = dir_rp.append('user_map')
group_map_rp = dir_rp.append('group_map')
user_map_rp.write_string('root:%s\n%s:root' % (user, user))
group_map_rp.write_string('0:1')
return user_map_rp.path, group_map_rp.path
def get_ownership(dir_rp):
"""Return pair (ids of dir_rp/1, ids of dir_rp2) of ids"""
rp1, rp2 = map(dir_rp.append, ('1', '2'))
assert rp1.isreg() and rp2.isreg(), (rp1.isreg(), rp2.isreg())
return (rp1.getuidgid(), rp2.getuidgid())
in_rp = write_ownership_dir()
user_map, group_map = write_mapping_files(in_rp)
out_rp = rpath.RPath(Globals.local_connection, 'testfiles/output')
if out_rp.lstat(): Myrm(out_rp.path)
assert get_ownership(in_rp) == ((0,0), (userid, 1)), \
get_ownership(in_rp)
rdiff_backup(1, 0, in_rp.path, out_rp.path,
extra_options = ("--user-mapping-file %s "
"--group-mapping-file %s" %
(user_map, group_map)))
assert get_ownership(out_rp) == ((userid, 1), (0, 1)), \
get_ownership(in_rp)
class HalfRoot(unittest.TestCase):
"""Backing up files where origin is root and destination is non-root"""
def make_dirs(self):
......
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