summaryrefslogtreecommitdiff
path: root/rdiff-backup
diff options
context:
space:
mode:
Diffstat (limited to 'rdiff-backup')
-rw-r--r--rdiff-backup/CHANGELOG6
-rw-r--r--rdiff-backup/TODO2
-rw-r--r--rdiff-backup/rdiff-backup.16
-rw-r--r--rdiff-backup/rdiff_backup/Globals.py3
-rw-r--r--rdiff-backup/rdiff_backup/Main.py14
-rw-r--r--rdiff-backup/rdiff_backup/eas_acls.py71
-rw-r--r--rdiff-backup/rdiff_backup/rpath.py16
-rw-r--r--rdiff-backup/rdiff_backup/user_group.py28
-rw-r--r--rdiff-backup/testing/eas_aclstest.py71
9 files changed, 191 insertions, 26 deletions
diff --git a/rdiff-backup/CHANGELOG b/rdiff-backup/CHANGELOG
index 3b7546e..8daadba 100644
--- a/rdiff-backup/CHANGELOG
+++ b/rdiff-backup/CHANGELOG
@@ -1,9 +1,13 @@
-New in v0.13.2 (??????????)
+New in v0.13.2 (2003/09/16)
---------------------------
Change ownership policy and added --user-mapping-file and
--group-mapping-file switches. See man page for more information.
+Added option --never-drop-acls to cause fatal error instead of
+dropping any acls or acl entries. Thanks to Greg Freemyer for
+suggestion.
+
Specified socket type as SOCK_STREAM. (Error reported by Erik
Forsberg.)
diff --git a/rdiff-backup/TODO b/rdiff-backup/TODO
index e958f5e..0ddc302 100644
--- a/rdiff-backup/TODO
+++ b/rdiff-backup/TODO
@@ -10,8 +10,6 @@ by Andrew Bressen.
Profile 0.13.x
-Look at EAs when unames change.
-
Examine regress handling of acls/eas/resource forks.
Look into that strange regress error July 22nd on
diff --git a/rdiff-backup/rdiff-backup.1 b/rdiff-backup/rdiff-backup.1
index 796d93a..44f0871 100644
--- a/rdiff-backup/rdiff-backup.1
+++ b/rdiff-backup/rdiff-backup.1
@@ -205,6 +205,12 @@ 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 --never-drop-acls
+Exit with error instead of dropping acls or acl entries. Normally
+this may happen (with a warning) because the destination does not
+support them or because the relevant user/group names do not exist on
+the destination side.
+.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
diff --git a/rdiff-backup/rdiff_backup/Globals.py b/rdiff-backup/rdiff_backup/Globals.py
index e40fd0a..de334f7 100644
--- a/rdiff-backup/rdiff_backup/Globals.py
+++ b/rdiff-backup/rdiff_backup/Globals.py
@@ -207,6 +207,9 @@ compare_inode = 1
# guarantee that any changes have been committed to disk.
fsync_directories = 1
+# If set, exit with error instead of dropping ACLs or ACL entries.
+never_drop_acls = None
+
def get(name):
"""Return the value of something in this module"""
diff --git a/rdiff-backup/rdiff_backup/Main.py b/rdiff-backup/rdiff_backup/Main.py
index 68832d3..d1c7b97 100644
--- a/rdiff-backup/rdiff_backup/Main.py
+++ b/rdiff-backup/rdiff_backup/Main.py
@@ -57,9 +57,10 @@ def parse_cmdlineoptions(arglist):
"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-compression", "no-compression-regexp=",
- "no-file-statistics", "no-hard-links", "null-separator",
+ "list-increment-sizes", "never-drop-acls",
+ "no-compare-inode", "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=",
@@ -109,6 +110,7 @@ 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 == "--never-drop-acls": Globals.set("never_drop_acls", 1)
elif opt == "--no-compare-inode": Globals.set("compare_inode", 0)
elif opt == "--no-compression": Globals.set("compression", None)
elif opt == "--no-compression-regexp":
@@ -368,6 +370,9 @@ def backup_set_fs_globals(rpin, rpout):
dest_fsa = rpout.conn.fs_abilities.get_fsabilities_readwrite(
'destination', Globals.rbdir, 1, Globals.chars_to_quote)
Log(str(dest_fsa), 3)
+ if Globals.never_drop_acls and not dest_fsa.acls:
+ Log.FatalError("--never-drop-acls specified, but ACL support\n"
+ "disabled on destination filesystem")
update_bool_global('read_acls', src_fsa.acls)
update_bool_global('read_eas', src_fsa.eas)
@@ -462,6 +467,9 @@ def restore_set_fs_globals(target):
mirror_fsa = Globals.rbdir.conn.fs_abilities.get_fsabilities_restoresource(
Globals.rbdir)
Log(str(mirror_fsa), 3)
+ if Globals.never_drop_acls and not target_fsa.acls:
+ Log.FatalError("--never-drop-acls specified, but ACL support\n"
+ "disabled on destination filesystem")
update_bool_global('read_acls', target_fsa.acls)
update_bool_global('write_acls', target_fsa.acls)
diff --git a/rdiff-backup/rdiff_backup/eas_acls.py b/rdiff-backup/rdiff_backup/eas_acls.py
index 442bdbd..e8258ad 100644
--- a/rdiff-backup/rdiff_backup/eas_acls.py
+++ b/rdiff-backup/rdiff_backup/eas_acls.py
@@ -33,6 +33,11 @@ except ImportError: pass
import static, Globals, metadata, connection, rorpiter, log, C, \
rpath, user_group
+# When an ACL gets dropped, put name in dropped_acl_names. This is
+# only used so that only the first dropped ACL for any given name
+# triggers a warning.
+dropped_acl_names = {}
+
class ExtendedAttributes:
"""Hold a file's extended attribute information"""
def __init__(self, index, attr_dict = None):
@@ -288,6 +293,7 @@ class AccessControlLists:
"""
assert isinstance(acl, self.__class__)
if self.index != acl.index: return 0
+ if self.is_basic(): return acl.is_basic()
return (self.cmp_entry_list(self.entry_list, acl.entry_list) and
self.cmp_entry_list(self.default_entry_list,
acl.default_entry_list))
@@ -327,21 +333,23 @@ class AccessControlLists:
self.entry_list, self.default_entry_list = \
rp.conn.eas_acls.get_acl_lists_from_rp(rp)
- def write_to_rp(self, rp):
+ def write_to_rp(self, rp, map_names = 1):
"""Write current access control list to RPath rp"""
rp.conn.eas_acls.set_rp_acl(rp, self.entry_list,
- self.default_entry_list)
+ self.default_entry_list, map_names)
-def set_rp_acl(rp, entry_list = None, default_entry_list = None):
+def set_rp_acl(rp, entry_list = None, default_entry_list = None,
+ map_names = 1):
"""Set given rp with ACL that acl_text defines. rp should be local"""
assert rp.conn is Globals.local_connection
- if entry_list: acl = list_to_acl(entry_list)
+ if entry_list: acl = list_to_acl(entry_list, map_names)
else: acl = posix1e.ACL()
acl.applyto(rp.path)
if rp.isdir():
- if default_entry_list: def_acl = list_to_acl(default_entry_list)
+ if default_entry_list:
+ def_acl = list_to_acl(default_entry_list, map_names)
else: def_acl = posix1e.ACL()
def_acl.applyto(rp.path, posix1e.ACL_TYPE_DEFAULT)
@@ -388,18 +396,55 @@ def acl_to_list(acl):
return (entry.tag_type, owner_pair, perms)
return map(entry_to_tuple, acl)
-def list_to_acl(entry_list):
- """Return posix1e.ACL object from list representation"""
+def list_to_acl(entry_list, map_names = 1):
+ """Return posix1e.ACL object from list representation
+
+ If map_names is true, use user_group to update the names for the
+ current system, and drop if not available. Otherwise just use the
+ same id.
+
+ """
+ def warn_drop(name):
+ """Warn about acl with name getting dropped"""
+ global dropped_acl_names
+ if Globals.never_drop_acls:
+ log.Log.FatalError(
+"--never-drop-acls specified but cannot map name\n"
+"%s occurring inside an ACL." % (name,))
+ if dropped_acl_names.has_key(name): return
+ log.Log("Warning: name %s not found on system, dropping ACL entry.\n"
+ "Further ACL entries dropped with this name will not "
+ "trigger further warnings" % (name,), 2)
+ dropped_acl_names[name] = name
+
+ def map_id_name(owner_pair, group = None):
+ """Return id of mapped id and user given original owner_pair"""
+ id, name = owner_pair
+ Map = group and user_group.GroupMap or user_group.UserMap
+ if name: return Map.get_id_from_name(name)
+ else:
+ assert id is not None
+ return Map.get_id_from_id(id)
+
acl = posix1e.ACL()
for tag, owner_pair, perms in entry_list:
- entry = posix1e.Entry(acl)
- entry.tag_type = tag
+ id = None
if owner_pair:
- if tag == posix1e.ACL_USER:
- entry.qualifier = user_group.UserMap.get_id(*owner_pair)
+ if map_names:
+ if tag == posix1e.ACL_USER: id = map_id_name(owner_pair, 0)
+ else:
+ assert tag == posix1e.ACL_GROUP, (tag, owner_pair, perms)
+ id = map_id_name(owner_pair, 1)
+ if id is None:
+ warn_drop(owner_pair[1])
+ continue
else:
- assert tag == posix1e.ACL_GROUP, (tag, owner_pair, perms)
- entry.qualifier = user_group.GroupMap.get_id(*owner_pair)
+ assert owner_pair[0] is not None, (tag, owner_pair, perms)
+ id = owner_pair[0]
+
+ entry = posix1e.Entry(acl)
+ entry.tag_type = tag
+ if id is not None: entry.qualifier = id
entry.permset.read = perms >> 2
entry.permset.write = perms >> 1 & 1
entry.permset.execute = perms & 1
diff --git a/rdiff-backup/rdiff_backup/rpath.py b/rdiff-backup/rdiff_backup/rpath.py
index de2d2f7..a834c5c 100644
--- a/rdiff-backup/rdiff_backup/rpath.py
+++ b/rdiff-backup/rdiff_backup/rpath.py
@@ -179,8 +179,7 @@ def copy_attribs_inc(rpin, rpout):
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 Globals.write_acls: rpout.write_acl(rpin.get_acl(), map_names = 0)
if not rpin.isdev(): rpout.setmtime(rpin.getmtime())
def cmp_attribs(rp1, rp2):
@@ -302,7 +301,8 @@ class RORPath:
not Globals.compare_inode or
not Globals.preserve_hardlinks)): pass
elif (not other.data.has_key(key) or
- self.data[key] != other.data[key]): return None
+ self.data[key] != other.data[key]):
+ return None
return 1
def equal_loose(self, other):
@@ -1035,9 +1035,13 @@ class RPath(RORPath):
except KeyError: acl = self.data['acl'] = acl_get(self)
return acl
- def write_acl(self, acl):
- """Change access control list of rp"""
- acl.write_to_rp(self)
+ def write_acl(self, acl, map_names = 1):
+ """Change access control list of rp
+
+ If map_names is true, map the ids in acl by user/group names.
+
+ """
+ acl.write_to_rp(self, map_names)
self.data['acl'] = acl
def get_ea(self):
diff --git a/rdiff-backup/rdiff_backup/user_group.py b/rdiff-backup/rdiff_backup/user_group.py
index 896e60e..b34f0cf 100644
--- a/rdiff-backup/rdiff_backup/user_group.py
+++ b/rdiff-backup/rdiff_backup/user_group.py
@@ -60,6 +60,16 @@ def gid2gname(gid):
gid2gname_dict[gid] = gname
return gname
+def uname2uid(uname):
+ """Given uname, return uid or None if cannot find"""
+ try: uname = pwd.getpwnam(uname)[2]
+ except KeyError: return None
+
+def gname2gid(gname):
+ """Given gname, return gid or None if cannot find"""
+ try: gname = grp.getgrnam(gname)[2]
+ except KeyError: return None
+
class Map:
"""Used for mapping names and id on source side to dest side"""
@@ -77,12 +87,25 @@ class Map:
self.name2id_dict[name] = out_id
return out_id
+ def get_id_from_name(self, name):
+ """Return mapped id from name only, or None if cannot"""
+ try: return self.name2id_dict[name]
+ except KeyError:
+ out_id = self.find_id_from_name(name)
+ self.name2id_dict[name] = out_id
+ return out_id
+
def get_id_from_id(self, id): return id
def find_id(self, id, name):
"""Find the proper id to use with given id and name"""
try: return self.name2id_func(name)
except KeyError: return id
+
+ def find_id_from_name(self, name):
+ """Look up proper id to use with name, or None"""
+ try: return self.name2id_func(name)
+ except KeyError: return None
class DefinedMap(Map):
"""Map names and ids on source side to appropriate ids on dest side
@@ -134,6 +157,11 @@ class DefinedMap(Map):
try: return self.id_mapping_dict[id]
except KeyError: return Map.find_id(self, id, name)
+ def find_id_from_name(self, name):
+ """Find id to map name to, or None if we can't"""
+ try: return self.name_mapping_dict[name]
+ except KeyError: return Map.find_id_from_name(name)
+
def init_user_mapping(mapping_string = None):
"""Initialize user mapping with given mapping string or None"""
global UserMap
diff --git a/rdiff-backup/testing/eas_aclstest.py b/rdiff-backup/testing/eas_aclstest.py
index 2ae565e..91e9d75 100644
--- a/rdiff-backup/testing/eas_aclstest.py
+++ b/rdiff-backup/testing/eas_aclstest.py
@@ -1,4 +1,4 @@
-import unittest, os, time, cStringIO
+import unittest, os, time, cStringIO, posix1e, pwd, grp
from commontest import *
from rdiff_backup.eas_acls import *
from rdiff_backup import Globals, rpath, Time, user_group
@@ -422,6 +422,75 @@ other::---
assert CompareRecursive(self.acl_testdir2, restore_dir,
compare_acls = 1)
+ def test_acl_mapping(self):
+ """Test mapping ACL names"""
+ def make_dir(rootrp):
+ if rootrp.lstat(): rootrp.delete()
+ rootrp.mkdir()
+ rp = rootrp.append('1')
+ rp.touch()
+ acl = AccessControlLists(('1',), """user::rwx
+user:root:rwx
+user:ben:---
+user:bin:r--
+group::r-x
+group:root:r-x
+group:ben:-w-
+mask::r-x
+other::---""")
+ rp.write_acl(acl)
+ return rp
+
+ def write_mapping_file(rootrp):
+ map_rp = rootrp.append('mapping_file')
+ map_rp.write_string("root:ben\nben:bin\nbin:root")
+ return map_rp
+
+ def get_perms_of_user(acl, user):
+ """Return the permissions of ACL_USER in acl, or None"""
+ for type, owner_pair, perms in acl.entry_list:
+ if type == posix1e.ACL_USER and owner_pair[1] == user:
+ return perms
+ return None
+
+ self.make_temp()
+ rootrp = rpath.RPath(Globals.local_connection,
+ 'testfiles/acl_map_test')
+ rp = make_dir(rootrp)
+ map_rp = write_mapping_file(rootrp)
+
+ rdiff_backup(1, 1, rootrp.path, tempdir.path,
+ extra_options = "--user-mapping-file %s" % (map_rp.path,))
+
+ out_rp = tempdir.append('1')
+ assert out_rp.isreg()
+ out_acl = tempdir.append('1').get_acl()
+ assert get_perms_of_user(out_acl, 'root') == 4
+ assert get_perms_of_user(out_acl, 'ben') == 7
+ assert get_perms_of_user(out_acl, 'bin') == 0
+
+ def test_acl_dropping(self):
+ """Test dropping of ACL names"""
+ self.make_temp()
+ rp = tempdir.append('1')
+ rp.touch()
+ acl = AccessControlLists(('1',), """user::rwx
+user:aoensutheu:r--
+group::r-x
+group:aeuai:r-x
+group:enutohnh:-w-
+other::---""")
+ rp.write_acl(acl)
+ rp2 = tempdir.append('1')
+ acl2 = AccessControlLists(('1',))
+ acl2.read_from_rp(rp2)
+ assert acl2.is_basic()
+ Globals.never_drop_acls = 1
+ try: rp.write_acl(acl)
+ except SystemExit: pass
+ else: assert 0, "Above should have exited with fatal error"
+ Globals.never_drop_acls = None
+
class CombinedTest(unittest.TestCase):
"""Test backing up and restoring directories with both EAs and ACLs"""