diff options
Diffstat (limited to 'rdiff-backup')
-rw-r--r-- | rdiff-backup/CHANGELOG | 11 | ||||
-rw-r--r-- | rdiff-backup/rdiff_backup/Globals.py | 7 | ||||
-rw-r--r-- | rdiff-backup/rdiff_backup/Main.py | 34 | ||||
-rw-r--r-- | rdiff-backup/rdiff_backup/backup.py | 20 | ||||
-rw-r--r-- | rdiff-backup/rdiff_backup/connection.py | 3 | ||||
-rw-r--r-- | rdiff-backup/rdiff_backup/eas_acls.py | 236 | ||||
-rw-r--r-- | rdiff-backup/rdiff_backup/fs_abilities.py | 58 | ||||
-rw-r--r-- | rdiff-backup/rdiff_backup/increment.py | 4 | ||||
-rw-r--r-- | rdiff-backup/rdiff_backup/restore.py | 17 | ||||
-rw-r--r-- | rdiff-backup/rdiff_backup/rpath.py | 37 | ||||
-rw-r--r-- | rdiff-backup/testing/commontest.py | 27 | ||||
-rw-r--r-- | rdiff-backup/testing/eas_aclstest.py | 211 | ||||
-rw-r--r-- | rdiff-backup/testing/fs_abilitiestest.py | 6 |
13 files changed, 575 insertions, 96 deletions
diff --git a/rdiff-backup/CHANGELOG b/rdiff-backup/CHANGELOG index 0a39f78..aeaa67e 100644 --- a/rdiff-backup/CHANGELOG +++ b/rdiff-backup/CHANGELOG @@ -8,9 +8,14 @@ attributes, access control lists, hard links, ownership, and directory fsyncing. Options such as --windows-mode, --chars-to-quote, --quoting-char, and --windows-restore-mode have been removed. -Now rdiff-backup supports extended attributes. To take advantage of -this you will need the python module pyxattr and a file system that -supports EAs. Thanks to Greg Freemyer for discussion. +Now rdiff-backup supports user extended attributes (EAs). To take +advantage of this you will need the python module pyxattr and a file +system that supports EAs. Thanks to Greg Freemyer for valuable +discussion on EA and ACL support. + +Support for access control lists (ACLs) was also added. An ACL +capable file system and the python package pylibacl (which exports the +posix1e module) are required. Added --list-increment-sizes switch, which tells you how much space the various backup files take up. (Suggested by Andrew Bressen) diff --git a/rdiff-backup/rdiff_backup/Globals.py b/rdiff-backup/rdiff_backup/Globals.py index 2f60ab7..8b63e8b 100644 --- a/rdiff-backup/rdiff_backup/Globals.py +++ b/rdiff-backup/rdiff_backup/Globals.py @@ -74,6 +74,13 @@ read_eas = None # implies read_eas. write_eas = None +# If true, save access control lists when backup up. +read_acls = None + +# If true, write access control list information to the destination +# when backing up or restoring. Implies read_acls. +write_acls = None + # This will be set as soon as the LocalConnection class loads local_connection = None diff --git a/rdiff-backup/rdiff_backup/Main.py b/rdiff-backup/rdiff_backup/Main.py index eaa1559..2656712 100644 --- a/rdiff-backup/rdiff_backup/Main.py +++ b/rdiff-backup/rdiff_backup/Main.py @@ -320,17 +320,25 @@ def backup_get_mirrortime(): def backup_set_fs_globals(rpin, rpout): """Use fs_abilities to set the globals that depend on filesystem""" - src_fsa = fs_abilities.FSAbilities().init_readonly(rpin) - SetConnections.UpdateGlobal('read_acls', src_fsa.acls) + src_fsa = fs_abilities.FSAbilities('source').init_readonly(rpin) + Log(str(src_fsa), 3) + if Globals.read_acls is None: + SetConnections.UpdateGlobal('read_acls', src_fsa.acls) if src_fsa.eas: rpin.get_ea() - SetConnections.UpdateGlobal('read_eas', src_fsa.eas) + if Globals.read_eas is None: + SetConnections.UpdateGlobal('read_eas', src_fsa.eas) - dest_fsa = fs_abilities.FSAbilities().init_readwrite( + dest_fsa = fs_abilities.FSAbilities('destination').init_readwrite( Globals.rbdir, override_chars_to_quote = Globals.chars_to_quote) + Log(str(dest_fsa), 3) SetConnections.UpdateGlobal('preserve_hardlinks', dest_fsa.hardlinks) SetConnections.UpdateGlobal('fsync_directories', dest_fsa.fsync_dirs) - SetConnections.UpdateGlobal('write_acls', dest_fsa.acls) - SetConnections.UpdateGlobal('write_eas', Globals.read_eas and dest_fsa.eas) + if Globals.write_acls is None: + SetConnections.UpdateGlobal('write_acls', + Globals.read_acls and dest_fsa.acls) + if Globals.write_eas is None: + SetConnections.UpdateGlobal('write_eas', + Globals.read_eas and dest_fsa.eas) SetConnections.UpdateGlobal('change_ownership', dest_fsa.ownership) SetConnections.UpdateGlobal('chars_to_quote', dest_fsa.chars_to_quote) if Globals.chars_to_quote: @@ -404,15 +412,19 @@ def restore_init_quoting(src_rp): def restore_set_fs_globals(target): """Use fs_abilities to set the globals that depend on filesystem""" target_fsa = fs_abilities.FSAbilities().init_readwrite(target, 0) - SetConnections.UpdateGlobal('read_acls', target_fsa.acls) - SetConnections.UpdateGlobal('write_acls', target_fsa.acls) - SetConnections.UpdateGlobal('read_eas', target_fsa.eas) - SetConnections.UpdateGlobal('write_eas', target_fsa.eas) + if Globals.read_acls is None: + SetConnections.UpdateGlobal('read_acls', target_fsa.acls) + if Globals.write_acls is None: + SetConnections.UpdateGlobal('write_acls', target_fsa.acls) + if Globals.read_eas is None: + SetConnections.UpdateGlobal('read_eas', target_fsa.eas) + if Globals.write_eas is None: + SetConnections.UpdateGlobal('write_eas', target_fsa.eas) if Globals.read_eas: target.get_ea() SetConnections.UpdateGlobal('preserve_hardlinks', target_fsa.hardlinks) SetConnections.UpdateGlobal('change_ownership', target_fsa.ownership) - mirror_fsa = fs_abilities.FSAbilities().init_readonly(Globals.rbdir) + mirror_fsa = fs_abilities.FSAbilities().init_readwrite(Globals.rbdir) if Globals.chars_to_quote is None: # otherwise already overridden if mirror_fsa.chars_to_quote: SetConnections.UpdateGlobal('chars_to_quote', diff --git a/rdiff-backup/rdiff_backup/backup.py b/rdiff-backup/rdiff_backup/backup.py index 0be0702..98c6e67 100644 --- a/rdiff-backup/rdiff_backup/backup.py +++ b/rdiff-backup/rdiff_backup/backup.py @@ -123,14 +123,6 @@ class DestinationStruct: destination except rdiff-backup-data directory. """ - def get_basic_iter(): - """Returns iterator of basic metadata""" - metadata_iter = metadata.MetadataFile.get_objects_at_time( - Globals.rbdir, Time.prevtime) - if metadata_iter: return metadata_iter - log.Log("Warning: Metadata file not found.\n" - "Metadata will be read from filesystem.", 2) - def get_iter_from_fs(): """Get the combined iterator from the filesystem""" sel = selection.Select(rpath) @@ -138,10 +130,9 @@ class DestinationStruct: return sel.set_iter() if use_metadata: - if Globals.read_eas: - rorp_iter = eas_acls.ExtendedAttributesFile.\ - get_combined_iter_at_time(Globals.rbdir, Time.prevtime) - else: rorp_iter = get_basic_iter() + rorp_iter = eas_acls.GetCombinedMetadataIter( + Globals.rbdir, Time.prevtime, + acls = Globals.read_acls, eas = Globals.read_eas) if rorp_iter: return rorp_iter return get_iter_from_fs() @@ -257,6 +248,7 @@ class CacheCollatedPostProcess: if Globals.file_statistics: statistics.FileStats.init() metadata.MetadataFile.open_file() if Globals.read_eas: eas_acls.ExtendedAttributesFile.open_file() + if Globals.read_acls: eas_acls.AccessControlListFile.open_file() # the following should map indicies to lists # [source_rorp, dest_rorp, changed_flag, success_flag, increment] @@ -334,6 +326,9 @@ class CacheCollatedPostProcess: if Globals.read_eas and not metadata_rorp.get_ea().empty(): eas_acls.ExtendedAttributesFile.write_object( metadata_rorp.get_ea()) + if Globals.read_acls and not metadata_rorp.get_acl().is_basic(): + eas_acls.AccessControlListFile.write_object( + metadata_rorp.get_acl()) if Globals.file_statistics: statistics.FileStats.update(source_rorp, dest_rorp, changed, inc) @@ -377,6 +372,7 @@ class CacheCollatedPostProcess: while self.cache_indicies: self.shorten_cache() metadata.MetadataFile.close_file() if Globals.read_eas: eas_acls.ExtendedAttributesFile.close_file() + if Globals.read_acls: eas_acls.AccessControlListFile.close_file() if Globals.print_statistics: statistics.print_active_stats() if Globals.file_statistics: statistics.FileStats.close() statistics.write_active_statfileobj() diff --git a/rdiff-backup/rdiff_backup/connection.py b/rdiff-backup/rdiff_backup/connection.py index 06d7e7a..818e5d8 100644 --- a/rdiff-backup/rdiff_backup/connection.py +++ b/rdiff-backup/rdiff_backup/connection.py @@ -518,7 +518,8 @@ class VirtualFile: 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 + TempFile, SetConnections, librsync, log, regress, fs_abilities, \ + eas_acls Globals.local_connection = LocalConnection() Globals.connections.append(Globals.local_connection) diff --git a/rdiff-backup/rdiff_backup/eas_acls.py b/rdiff-backup/rdiff_backup/eas_acls.py index 577a09c..4b4d169 100644 --- a/rdiff-backup/rdiff_backup/eas_acls.py +++ b/rdiff-backup/rdiff_backup/eas_acls.py @@ -28,6 +28,8 @@ access_control_lists.<time>.snapshot. from __future__ import generators import base64, errno, re +try: import posix1e +except ImportError: pass import static, Globals, metadata, connection, rorpiter, log @@ -54,6 +56,9 @@ class ExtendedAttributes: if exc[0] == errno.EOPNOTSUPP: return # if not sup, consider empty raise for attr in attr_list: + if not attr.startswith('user.'): + # Only preserve user extended attributes + continue try: self.attr_dict[attr] = rp.conn.xattr.getxattr(rp.path, attr) except IOError, exc: # File probably modified while reading, just continue @@ -160,68 +165,231 @@ class ExtendedAttributesFile(metadata.FlatFile): _extractor = EAExtractor _object_to_record = staticmethod(EA2Record) - def get_combined_iter_at_time(cls, rbdir, rest_time, - restrict_index = None): - """Return an iter of rorps with extended attributes added""" - def join_eas(basic_iter, ea_iter): - """Join basic_iter with ea iter""" - collated = rorpiter.CollateIterators(basic_iter, ea_iter) + def join(cls, rorp_iter, rbdir, time, restrict_index): + """Add extended attribute information to existing rorp_iter""" + def helper(rorp_iter, ea_iter): + """Add EA information in ea_iter to rorp_iter""" + collated = rorpiter.CollateIterators(rorp_iter, ea_iter) for rorp, ea in collated: assert rorp, (rorp, (ea.index, ea.attr_dict), rest_time) if not ea: ea = ExtendedAttributes(rorp.index) rorp.set_ea(ea) yield rorp - - basic_iter = metadata.MetadataFile.get_objects_at_time( - Globals.rbdir, rest_time, restrict_index) - if not basic_iter: return None - ea_iter = cls.get_objects_at_time(rbdir, rest_time, restrict_index) - if not ea_iter: - log.Log("Warning: Extended attributes file not found", 2) - ea_iter = iter([]) - return join_eas(basic_iter, ea_iter) + + ea_iter = cls.get_objects_at_time(rbdir, time, restrict_index) + if ea_iter: return helper(rorp_iter, ea_iter) + else: + log.Log("Warning: Extended attributes file not found",2) + return rorp_iter static.MakeClass(ExtendedAttributesFile) class AccessControlList: - """Hold a file's access control list information""" - def __init__(self, index, text_acl = None): - """Initialize object with index and possibly text_acl""" + """Hold a file's access control list information + + Since ACL objects cannot be picked, store everything as text, in + self.acl_text and self.def_acl_text. + + """ + def __init__(self, index, acl_text = None, def_acl_text = None): + """Initialize object with index and possibly acl_text""" self.index = index - # self.ACL is a posix1e ACL object - if text_acl is None: self.ACL = None - else: self.ACL = posix1e.ACL(text_acl) + if acl_text: # Check validity of ACL, reorder if necessary + ACL = posix1e.ACL(text=acl_text) + assert ACL.valid(), "Bad ACL: "+acl_text + self.acl_text = str(ACL) + else: self.acl_text = None + + if def_acl_text: + def_ACL = posix1e.ACL(text=def_acl_text) + assert def_ACL.valid(), "Bad default ACL: "+def_acl_text + self.def_acl_text = str(def_ACL) + else: self.def_acl_text = None + + def __str__(self): + """Return human-readable string""" + return ("acl_text: %s\ndef_acl_text: %s" % + (self.acl_text, self.def_acl_text)) def __eq__(self, acl): - """Compare self and other access control list""" - return self.index == acl.index and str(self.ACL) == str(acl.ACL) + """Compare self and other access control list + + Basic acl permissions are considered equal to an empty acl + object. + + """ + assert isinstance(acl, self.__class__) + if self.index != acl.index: return 0 + if self.is_basic(): return acl.is_basic() + if acl.is_basic(): return self.is_basic() + if self.acl_text != acl.acl_text: return 0 + if not self.def_acl_text and not acl.def_acl_text: return 1 + return self.def_acl_text == acl.def_acl_text def __ne__(self, acl): return not self.__eq__(acl) + def eq_verbose(self, acl): + """Returns same as __eq__ but print explanation if not equal""" + if self.index != acl.index: + print "index %s not equal to index %s" % (self.index, acl.index) + return 0 + if self.acl_text != acl.acl_text: + print "ACL texts not equal:" + print self.acl_text + print acl.acl_text + return 0 + if (self.def_acl_text != acl.def_acl_text and + (self.def_acl_text or acl.def_acl_text)): + print "Unequal default acl texts:" + print self.def_acl_text + print acl.def_acl_text + return 0 + return 1 + def get_indexpath(self): return self.index and '/'.join(self.index) or '.' + def is_basic(self): + """True if acl can be reduced to standard unix permissions + + Assume that if they are only three entries, they correspond to + user, group, and other, and thus don't use any special ACL + features. -def get_acl_from_rp(rp): - """Return text acl from an rpath, or None if not supported""" - try: acl = rp.conn.posix1e.ACL(file=rp.path) + """ + if not self.acl_text and not self.def_acl_text: return 1 + lines = self.acl_text.strip().split('\n') + assert len(lines) >= 3, lines + return len(lines) == 3 and not self.def_acl_text + + def read_from_rp(self, rp): + """Set self.ACL from an rpath, or None if not supported""" + self.acl_text, self.def_acl_text = \ + rp.conn.eas_acls.get_acl_text_from_rp(rp) + + def write_to_rp(self, rp): + """Write current access control list to RPath rp""" + rp.conn.eas_acls.set_rp_acl(rp, self.acl_text, self.def_acl_text) + +def set_rp_acl(rp, acl_text = None, def_acl_text = None): + """Set given rp with ACL that acl_text defines. rp should be local""" + assert rp.conn is Globals.local_connection + if acl_text: + acl = posix1e.ACL(text=acl_text) + assert acl.valid() + else: acl = posix1e.ACL() + acl.applyto(rp.path) + if rp.isdir(): + if def_acl_text: + def_acl = posix1e.ACL(text=def_acl_text) + assert def_acl.valid() + else: def_acl = posix1e.ACL() + def_acl.applyto(rp.path, posix1e.ACL_TYPE_DEFAULT) + +def get_acl_text_from_rp(rp): + """Returns (acl_text, def_acl_text) from an rpath. Call locally""" + assert rp.conn is Globals.local_connection + try: acl_text = str(posix1e.ACL(file=rp.path)) except IOError, exc: - if exc[0] == errno.EOPNOTSUPP: return None - raise - return str(acl) + if exc[0] == errno.EOPNOTSUPP: acl_text = None + else: raise + if rp.isdir(): + try: def_acl_text = str(posix1e.ACL(filedef=rp.path)) + except IOError, exc: + if exc[0] == errno.EOPNOTSUPP: def_acl_text = None + else: raise + else: def_acl_text = None + return (acl_text, def_acl_text) def acl_compare_rps(rp1, rp2): - """Return true if rp1 and rp2 have same acls""" - return get_acl_from_rp(rp1) == get_acl_from_rp(rp2) + """Return true if rp1 and rp2 have same acl information""" + acl1 = AccessControlList(rp1.index) + acl1.read_from_rp(rp1) + acl2 = AccessControlList(rp2.index) + acl2.read_from_rp(rp2) + return acl1 == acl2 def ACL2Record(acl): """Convert an AccessControlList object into a text record""" - return "# file: %s\n%s" % (acl.get_indexpath(), str(acl.ACL)) + start = "# file: %s\n%s" % (acl.get_indexpath(), acl.acl_text) + if not acl.def_acl_text: return start + default_lines = acl.def_acl_text.strip().split('\n') + default_text = '\ndefault:'.join(default_lines) + return "%sdefault:%s\n" % (start, default_text) -def Record2EA(acl): +def Record2ACL(record): """Convert text record to an AccessControlList object""" - XXXX + lines = record.split('\n') + first_line = lines.pop(0) + if not first_line.startswith('# file: '): + raise metadata.ParsingError("Bad record beginning: "+ first_line) + filename = first_line[8:] + if filename == '.': index = () + else: index = tuple(filename.split('/')) + + normal_entries = []; default_entries = [] + for line in lines: + if line.startswith('default:'): default_entries.append(line[8:]) + else: normal_entries.append(line) + return AccessControlList(index, acl_text='\n'.join(normal_entries), + def_acl_text='\n'.join(default_entries)) + + +class ACLExtractor(EAExtractor): + """Iterate AccessControlList objects from the ACL information file + + Except for the record_to_object method, we can reuse everything in + the EAExtractor class because the file formats are so similar. + + """ + record_to_object = staticmethod(Record2ACL) + +class AccessControlListFile(metadata.FlatFile): + """Store/retrieve ACLs from extended attributes file""" + _prefix = 'access_control_lists' + _extractor = ACLExtractor + _object_to_record = staticmethod(ACL2Record) + + def join(cls, rorp_iter, rbdir, time, restrict_index): + """Add access control list information to existing rorp_iter""" + def helper(rorp_iter, acl_iter): + """Add ACL information in acl_iter to rorp_iter""" + collated = rorpiter.CollateIterators(rorp_iter, acl_iter) + for rorp, acl in collated: + assert rorp, "Missing rorp for index %s" % (acl.index,) + if not acl: acl = AccessControlList(rorp.index) + rorp.set_acl(acl) + yield rorp + + acl_iter = cls.get_objects_at_time(rbdir, time, restrict_index) + if acl_iter: return helper(rorp_iter, acl_iter) + else: + log.Log("Warning: Access Control List file not found", 2) + return rorp_iter +static.MakeClass(AccessControlListFile) +def GetCombinedMetadataIter(rbdir, time, restrict_index = None, + acls = None, eas = None): + """Return iterator of rorps from metadata and related files + + None will be returned if the metadata file is absent. If acls or + eas is true, access control list or extended attribute information + will be added. + + """ + metadata_iter = metadata.MetadataFile.get_objects_at_time( + rbdir, time, restrict_index) + if not metadata_iter: + log.Log("Warning, metadata file not found.\n" + "Metadata will be read from filesystem.", 2) + return None + if eas: + metadata_iter = ExtendedAttributesFile.join( + metadata_iter, rbdir, time, restrict_index) + if acls: + metadata_iter = AccessControlListFile.join( + metadata_iter, rbdir, time, restrict_index) + return metadata_iter diff --git a/rdiff-backup/rdiff_backup/fs_abilities.py b/rdiff-backup/rdiff_backup/fs_abilities.py index 98c43f8..6ef26ea 100644 --- a/rdiff-backup/rdiff_backup/fs_abilities.py +++ b/rdiff-backup/rdiff_backup/fs_abilities.py @@ -39,6 +39,40 @@ class FSAbilities: hardlinks = None # True if hard linking supported fsync_dirs = None # True if directories can be fsync'd read_only = None # True if capabilities were determined non-destructively + name = None # + + def __init__(self, name = None): + """FSAbilities initializer. name is only used in logging""" + self.name = name + + def __str__(self): + """Return pretty printable version of self""" + s = ['-' * 60] + def addline(desc, val_text): + """Add description line to s""" + s.append(' %s%s%s' % (desc, ' ' * (45-len(desc)), val_text)) + + if self.name: + s.append('Detected abilities for %s file system:' % (self.name,)) + else: s.append('Detected abilities for file system') + + ctq_str = (self.chars_to_quote is None and 'N/A' + or repr(self.chars_to_quote)) + addline('Characters needing quoting', ctq_str) + + for desc, val in [('Ownership changing', self.ownership), + ('Access Control Lists', self.acls), + ('Extended Attributes', self.eas), + ('Hard linking', self.hardlinks), + ('fsync() directories', self.fsync_dirs)]: + if val: val_text = 'On' + elif val is None: val_text = 'N/A' + else: + assert val == 0 + val_text = 'Off' + addline(desc, val_text) + s.append(s[0]) + return '\n'.join(s) def init_readonly(self, rp): """Set variables using fs tested at RPath rp @@ -128,7 +162,7 @@ rdiff-backup-data/chars_to_quote. except (IOError, OSError), exc: if exc[0] == errno.EPERM: log.Log("Warning: ownership cannot be changed on filesystem " - "at %s" % (self.root_rp.path,), 2) + "at %s" % (self.root_rp.path,), 3) self.ownership = 0 else: raise else: self.ownership = 1 @@ -145,7 +179,7 @@ rdiff-backup-data/chars_to_quote. except (IOError, OSError), exc: if exc[0] in (errno.EOPNOTSUPP, errno.EPERM): log.Log("Warning: hard linking not supported by filesystem " - "at %s" % (self.root_rp.path,), 2) + "at %s" % (self.root_rp.path,), 3) self.hardlinks = 0 else: raise else: self.hardlinks = 1 @@ -213,8 +247,8 @@ def test_eas_local(rp, write): assert rp.lstat() try: import xattr except ImportError: - log.Log("Warning: Unable to import module xattr. ACLs not " - "supported on filesystem at %s" % (rp.path,), 2) + log.Log("Unable to import module xattr. EAs not " + "supported on filesystem at %s" % (rp.path,), 4) return 0 try: @@ -224,8 +258,8 @@ def test_eas_local(rp, write): assert xattr.getxattr(rp.path, "user.test") == "test val" except IOError, exc: if exc[0] == errno.EOPNOTSUPP: - log.Log("Warning: Extended attributes not supported by " - "filesystem at %s" % (rp.path,), 2) + log.Log("Extended attributes not supported by " + "filesystem at %s" % (rp.path,), 4) return 0 else: raise else: return 1 @@ -236,16 +270,16 @@ def test_acls_local(rp): assert rp.lstat() try: import posix1e except ImportError: - log.Log("Warning: Unable to import module posix1e from pylibacl " + log.Log("Unable to import module posix1e from pylibacl " "package.\nACLs not supported on filesystem at %s" % - (rp.path,), 2) + (rp.path,), 4) return 0 try: posix1e.ACL(file=rp.path) except IOError, exc: if exc[0] == errno.EOPNOTSUPP: - log.Log("Warning: ACLs appear not to be supported by " - "filesystem at %s" % (rp.path,), 2) + log.Log("ACLs appear not to be supported by " + "filesystem at %s" % (rp.path,), 4) return 0 else: raise else: return 1 @@ -255,8 +289,8 @@ def test_fsync_local(rp): assert rp.conn is Globals.local_connection try: rp.fsync() except (IOError, OSError), exc: - log.Log("Warning: Directories on file system at %s are not " - "fsyncable.\nAssuming it's unnecessary." % (rp.path,), 2) + log.Log("Directories on file system at %s are not " + "fsyncable.\nAssuming it's unnecessary." % (rp.path,), 4) return 0 else: return 1 diff --git a/rdiff-backup/rdiff_backup/increment.py b/rdiff-backup/rdiff_backup/increment.py index 100ee36..f7fe5e7 100644 --- a/rdiff-backup/rdiff_backup/increment.py +++ b/rdiff-backup/rdiff_backup/increment.py @@ -87,7 +87,9 @@ def makedir(mirrordir, incpref): """Make file indicating directory mirrordir has changed""" dirsign = get_inc(incpref, "dir") dirsign.touch() - rpath.copy_attribs(mirrordir, dirsign) + # 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) return dirsign def get_inc(rp, typestr, time = None): diff --git a/rdiff-backup/rdiff_backup/restore.py b/rdiff-backup/rdiff_backup/restore.py index 9ae5df8..5796141 100644 --- a/rdiff-backup/rdiff_backup/restore.py +++ b/rdiff-backup/rdiff_backup/restore.py @@ -154,16 +154,13 @@ class MirrorStruct: """ if rest_time is None: rest_time = _rest_time - if Globals.write_eas: - metadata_iter = eas_acls.ExtendedAttributesFile.\ - get_combined_iter_at_time( - Globals.rbdir, rest_time, restrict_index = cls.mirror_base.index) - else: - metadata_iter = metadata.MetadataFile.get_objects_at_time( - Globals.rbdir, rest_time, restrict_index = cls.mirror_base.index) - if metadata_iter: rorp_iter = metadata_iter - elif require_metadata: log.Log.FatalError("Mirror metadata not found") - else: + + rorp_iter = eas_acls.GetCombinedMetadataIter( + Globals.rbdir, rest_time, restrict_index = cls.mirror_base.index, + acls = Globals.write_acls, eas = Globals.write_eas) + if not rorp_iter: + if require_metadata: + log.Log.FatalError("Mirror metadata not found") log.Log("Warning: Mirror metadata not found, " "reading from directory", 2) rorp_iter = cls.get_rorp_iter_from_rf(cls.root_rf) diff --git a/rdiff-backup/rdiff_backup/rpath.py b/rdiff-backup/rdiff_backup/rpath.py index e54d46b..efad6ed 100644 --- a/rdiff-backup/rdiff_backup/rpath.py +++ b/rdiff-backup/rdiff_backup/rpath.py @@ -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): +def copy_attribs(rpin, rpout, acls = 1): """Change file attributes of rpout to match rpin Only changes the chmoddable bits, uid/gid ownership, and @@ -153,10 +153,11 @@ def copy_attribs(rpin, rpout): log.Log("Copying attributes 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_eas: rpout.write_ea(rpin.get_ea()) if Globals.change_ownership: apply(rpout.chown, rpin.getuidgid()) if Globals.change_permissions: rpout.chmod(rpin.getperms()) + if Globals.write_acls and acls: rpout.write_acl(rpin.get_acl()) if not rpin.isdev(): rpout.setmtime(rpin.getmtime()) - if Globals.write_eas: rpout.write_ea(rpin.get_ea()) def cmp_attribs(rp1, rp2): """True if rp1 has the same file attributes as rp2 @@ -268,6 +269,8 @@ class RORPath: elif key == 'ctime': pass elif key == 'devloc' or key == 'nlink': pass elif key == 'size' and not self.isreg(): pass + elif key == 'ea' and not Globals.read_eas: pass + elif key == 'acl' and not Globals.read_acls: pass elif (key == 'inode' and (not self.isreg() or self.getnumlinks() == 1 or not Globals.compare_inode or @@ -300,13 +303,17 @@ class RORPath: elif key == 'size' and not self.isreg(): pass elif key == 'perms' and not Globals.change_permissions: pass elif key == 'inode': pass + elif (key == 'ea' and + not (Globals.read_eas and Globals.write_eas)): pass + elif (key == 'acl' and + not (Globals.read_acls and Globals.write_acls)): pass elif (not other.data.has_key(key) or self.data[key] != other.data[key]): return 0 return 1 def equal_verbose(self, other, check_index = 1, compare_inodes = 0, compare_ownership = 0, - compare_eas = 0): + compare_acls = 0, compare_eas = 0): """Like __eq__, but log more information. Useful when testing""" if check_index and self.index != other.index: log.Log("Index %s != index %s" % (self.index, other.index), 2) @@ -325,6 +332,7 @@ class RORPath: elif key == 'inode' and (not self.isreg() or not compare_inodes): pass elif key == 'ea' and not compare_eas: pass + elif key == 'acl' and not compare_acls: pass elif (not other.data.has_key(key) or self.data[key] != other.data[key]): if not other.data.has_key(key): @@ -523,6 +531,14 @@ class RORPath: self.index) self.file_already_open = None + def set_acl(self, acl): + """Record access control list in dictionary. Does not write""" + self.data['acl'] = acl + + def get_acl(self): + """Return access control list object from dictionary""" + return self.data['acl'] + def set_ea(self, ea): """Record extended attributes in dictionary. Does not write""" self.data['ea'] = ea @@ -591,6 +607,7 @@ class RPath(RORPath): """Set data dictionary using C extension""" self.data = self.conn.C.make_file_dict(self.path) if Globals.read_eas and self.lstat(): self.get_ea() + if Globals.read_acls and self.lstat(): self.get_acl() def make_file_dict_old(self): """Create the data dictionary""" @@ -949,6 +966,20 @@ class RPath(RORPath): assert not fp.close() return s + def get_acl(self): + """Return access control list object, setting if necessary""" + try: acl = self.data['acl'] + except KeyError: + acl = eas_acls.AccessControlList(self.index) + if not self.issym(): acl.read_from_rp(self) + self.data['acl'] = acl + return acl + + def write_acl(self, acl): + """Change access control list of rp""" + acl.write_to_rp(self) + self.data['acl'] = acl + def get_ea(self): """Return extended attributes object, setting if necessary""" try: ea = self.data['ea'] diff --git a/rdiff-backup/testing/commontest.py b/rdiff-backup/testing/commontest.py index f80da59..b60018d 100644 --- a/rdiff-backup/testing/commontest.py +++ b/rdiff-backup/testing/commontest.py @@ -168,7 +168,7 @@ def _reset_connections(src_rp, dest_rp): def CompareRecursive(src_rp, dest_rp, compare_hardlinks = 1, equality_func = None, exclude_rbdir = 1, ignore_tmp_files = None, compare_ownership = 0, - compare_eas = 0): + compare_eas = 0, compare_acls = 0): """Compare src_rp and dest_rp, which can be directories This only compares file attributes, not the actual data. This @@ -180,8 +180,9 @@ def CompareRecursive(src_rp, dest_rp, compare_hardlinks = 1, src_rp.setdata() dest_rp.setdata() - Log("Comparing %s and %s, hardlinks %s, eas %s" % - (src_rp.path, dest_rp.path, compare_hardlinks, compare_eas), 3) + Log("Comparing %s and %s, hardlinks %s, eas %s, acls %s" % + (src_rp.path, dest_rp.path, compare_hardlinks, + compare_eas, compare_acls), 3) src_select = selection.Select(src_rp) dest_select = selection.Select(dest_rp) @@ -226,6 +227,10 @@ def CompareRecursive(src_rp, dest_rp, compare_hardlinks = 1, Log("Different EAs in files %s and %s" % (src_rorp.get_indexpath(), dest_rorp.get_indexpath()), 3) return None + if compare_acls and not eas_acls.acl_compare_rps(src_rorp, dest_rorp): + Log("Different ACLs in files %s and %s" % + (src_rorp.get_indexpath(), dest_rorp.get_indexpath()), 3) + return None return 1 def rbdir_equal(src_rorp, dest_rorp): @@ -245,6 +250,10 @@ def CompareRecursive(src_rp, dest_rp, compare_hardlinks = 1, Log("Different EAs in files %s and %s" % (src_rorp.get_indexpath(), dest_rorp.get_indexpath())) return None + if compare_acls and not eas_acls.acl_compare_rps(src_rorp, dest_rorp): + Log("Different ACLs in files %s and %s" % + (src_rorp.get_indexpath(), dest_rorp.get_indexpath()), 3) + return None if compare_hardlinks: if Hardlink.rorp_eq(src_rorp, dest_rorp): return 1 elif src_rorp.equal_verbose(dest_rorp, @@ -285,7 +294,8 @@ def BackupRestoreSeries(source_local, dest_local, list_of_dirnames, dest_dirname = "testfiles/output", restore_dirname = "testfiles/rest_out", compare_backups = 1, - compare_eas = 0): + compare_eas = 0, + compare_acls = 0): """Test backing up/restoring of a series of directories The dirnames correspond to a single directory at different times. @@ -297,6 +307,8 @@ def BackupRestoreSeries(source_local, dest_local, list_of_dirnames, Globals.set('preserve_hardlinks', compare_hardlinks) Globals.set('write_eas', compare_eas) Globals.set('read_eas', compare_eas) + Globals.set('write_acls', compare_acls) + Globals.set('read_acls', compare_acls) time = 10000 dest_rp = rpath.RPath(Globals.local_connection, dest_dirname) restore_rp = rpath.RPath(Globals.local_connection, restore_dirname) @@ -312,7 +324,8 @@ def BackupRestoreSeries(source_local, dest_local, list_of_dirnames, _reset_connections(src_rp, dest_rp) if compare_backups: assert CompareRecursive(src_rp, dest_rp, compare_hardlinks, - compare_eas = compare_eas) + compare_eas = compare_eas, + compare_acls = compare_acls) time = 10000 for dirname in list_of_dirnames[:-1]: @@ -321,7 +334,9 @@ def BackupRestoreSeries(source_local, dest_local, list_of_dirnames, InternalRestore(dest_local, source_local, dest_dirname, restore_dirname, time) src_rp = rpath.RPath(Globals.local_connection, dirname) - assert CompareRecursive(src_rp, restore_rp, compare_eas = compare_eas) + assert CompareRecursive(src_rp, restore_rp, + compare_eas = compare_eas, + compare_acls = compare_acls) # Restore should default back to newest time older than it # with a backup then. diff --git a/rdiff-backup/testing/eas_aclstest.py b/rdiff-backup/testing/eas_aclstest.py index a9a880b..ed1a5b4 100644 --- a/rdiff-backup/testing/eas_aclstest.py +++ b/rdiff-backup/testing/eas_aclstest.py @@ -62,7 +62,12 @@ class EATest(unittest.TestCase): assert 0, "We shouldn't have gotten this far" def make_backup_dirs(self): - """Create testfiles/ea_test[12] directories""" + """Create testfiles/ea_test[12] directories + + Goal is to set range of extended attributes, to give good test + to extended attribute code. + + """ if self.ea_testdir1.lstat(): self.ea_testdir1.delete() if self.ea_testdir2.lstat(): self.ea_testdir2.delete() self.ea_testdir1.mkdir() @@ -132,4 +137,208 @@ class EATest(unittest.TestCase): 'testfiles/empty', 'testfiles/ea_test1'] BackupRestoreSeries(None, None, dirlist, compare_eas = 1) + + +class ACLTest(unittest.TestCase): + """Test access control lists""" + sample_acl = AccessControlList((),"""user::rwx +user:root:rwx +group::r-x +group:root:r-x +mask::r-x +other::---""") + dir_acl = AccessControlList((), """user::rwx +user:root:rwx +group::r-x +group:root:r-x +mask::r-x +other::---""", + """user::rwx +user:root:--- +group::r-x +mask::r-x +other::---""") + acl1 = AccessControlList(('1',), """user::r-- +user:ben:--- +group::--- +group:root:--- +mask::--- +other::---""") + acl2 = AccessControlList(('2',), """user::rwx +group::r-x +group:ben:rwx +mask::--- +other::---""") + acl3 = AccessControlList(('3',), """user::rwx +user:root:--- +group::r-x +mask::--- +other::---""") + empty_acl = AccessControlList((), "user::rwx\ngroup::---\nother::---") + acl_testdir1 = rpath.RPath(Globals.local_connection, 'testfiles/acl_test1') + acl_testdir2 = rpath.RPath(Globals.local_connection, 'testfiles/acl_test2') + def make_temp(self): + """Make temp directory testfile/output""" + if tempdir.lstat(): tempdir.delete() + tempdir.mkdir() + + def testBasic(self): + """Test basic writing and reading of ACLs""" + self.make_temp() + new_acl = AccessControlList(()) + tempdir.chmod(0700) + new_acl.read_from_rp(tempdir) + assert new_acl.is_basic(), new_acl.acl_text + assert not new_acl == self.sample_acl + assert new_acl != self.sample_acl + assert new_acl == self.empty_acl, \ + (new_acl.acl_text, self.empty_acl.acl_text) + + self.sample_acl.write_to_rp(tempdir) + new_acl.read_from_rp(tempdir) + assert new_acl.acl_text == self.sample_acl.acl_text, \ + (new_acl.acl_text, self.sample_acl.acl_text) + assert new_acl == self.sample_acl + + def testBasicDir(self): + """Test reading and writing of ACL w/ defaults to directory""" + self.make_temp() + new_acl = AccessControlList(()) + new_acl.read_from_rp(tempdir) + assert new_acl.is_basic() + assert new_acl != self.dir_acl + + self.dir_acl.write_to_rp(tempdir) + new_acl.read_from_rp(tempdir) + assert not new_acl.is_basic() + if not new_acl == self.dir_acl: + assert new_acl.eq_verbose(self.dir_acl) + assert 0, "Shouldn't be here---eq != eq_verbose?" + + def testRecord(self): + """Test writing a record and reading it back""" + record = ACL2Record(self.sample_acl) + new_acl = Record2ACL(record) + assert new_acl == self.sample_acl + + record2 = ACL2Record(self.dir_acl) + new_acl2 = Record2ACL(record2) + if not new_acl2 == self.dir_acl: + assert new_acl2.eq_verbose(self.dir_acl) + assert 0 + + def make_backup_dirs(self): + """Create testfiles/acl_test[12] directories""" + if self.acl_testdir1.lstat(): self.acl_testdir1.delete() + if self.acl_testdir2.lstat(): self.acl_testdir2.delete() + self.acl_testdir1.mkdir() + rp1_1 = self.acl_testdir1.append('1') + rp1_2 = self.acl_testdir1.append('2') + rp1_3 = self.acl_testdir1.append('3') + map(rpath.RPath.touch, [rp1_1, rp1_2, rp1_3]) + self.dir_acl.write_to_rp(self.acl_testdir1) + self.acl1.write_to_rp(rp1_1) + self.acl2.write_to_rp(rp1_2) + self.acl3.write_to_rp(rp1_3) + + self.acl_testdir2.mkdir() + rp2_1, rp2_2, rp2_3 = map(self.acl_testdir2.append, ('1', '2', '3')) + map(rpath.RPath.touch, (rp2_1, rp2_2, rp2_3)) + self.sample_acl.write_to_rp(self.acl_testdir2) + self.acl3.write_to_rp(rp2_1) + self.acl1.write_to_rp(rp2_2) + self.acl2.write_to_rp(rp2_3) + + def testIterate(self): + """Test writing several records and then reading them back""" + self.make_backup_dirs() + rp1 = self.acl_testdir1.append('1') + rp2 = self.acl_testdir1.append('2') + rp3 = self.acl_testdir1.append('3') + + # Now write records corresponding to above rps into file + Globals.rbdir = tempdir + Time.setcurtime(10000) + AccessControlListFile.open_file() + for rp in [self.acl_testdir1, rp1, rp2, rp3]: + acl = AccessControlList(rp.index) + acl.read_from_rp(rp) + AccessControlListFile.write_object(acl) + AccessControlListFile.close_file() + + # Read back records and compare + acl_iter = AccessControlListFile.get_objects_at_time(tempdir, 10000) + assert acl_iter, "No acl file found" + dir_acl_reread = acl_iter.next() + assert dir_acl_reread == self.dir_acl + acl1_reread = acl_iter.next() + assert acl1_reread == self.acl1 + acl2_reread = acl_iter.next() + assert acl2_reread == self.acl2 + acl3_reread = acl_iter.next() + assert acl3_reread == self.acl3 + try: extra = acl_iter.next() + except StopIteration: pass + else: assert 0, "Got unexpected object: " + repr(extra) + + def testSeriesLocal(self): + """Test backing up and restoring directories with ACLs locally""" + self.make_backup_dirs() + dirlist = ['testfiles/acl_test1', 'testfiles/empty', + 'testfiles/acl_test2', 'testfiles/acl_test1'] + BackupRestoreSeries(1, 1, dirlist, compare_acls = 1) + + def testSeriesRemote(self): + """Test backing up, restoring directories with EA remotely""" + self.make_backup_dirs() + dirlist = ['testfiles/acl_test1', 'testfiles/acl_test2', + 'testfiles/empty', 'testfiles/acl_test1'] + BackupRestoreSeries(None, None, dirlist, compare_acls = 1) + + +class CombinedTest(unittest.TestCase): + """Test backing up and restoring directories with both EAs and ACLs""" + combo_testdir1 = rpath.RPath(Globals.local_connection, + 'testfiles/ea_acl_test1') + combo_testdir2 = rpath.RPath(Globals.local_connection, + 'testfiles/ea_acl_test2') + def make_backup_dirs(self): + """Create testfiles/ea_acl_test[12] directories""" + if self.combo_testdir1.lstat(): self.combo_testdir1.delete() + if self.combo_testdir2.lstat(): self.combo_testdir2.delete() + self.combo_testdir1.mkdir() + rp1_1, rp1_2, rp1_3 = map(self.combo_testdir1.append, ('1', '2', '3')) + map(rpath.RPath.touch, [rp1_1, rp1_2, rp1_3]) + ACLTest.dir_acl.write_to_rp(self.combo_testdir1) + EATest.sample_ea.write_to_rp(self.combo_testdir1) + ACLTest.acl1.write_to_rp(rp1_1) + EATest.ea2.write_to_rp(rp1_2) + ACLTest.acl3.write_to_rp(rp1_3) + EATest.ea3.write_to_rp(rp1_3) + + self.combo_testdir2.mkdir() + rp2_1, rp2_2, rp2_3 = map(self.combo_testdir2.append, ('1', '2', '3')) + map(rpath.RPath.touch, [rp2_1, rp2_2, rp2_3]) + ACLTest.sample_acl.write_to_rp(self.combo_testdir2) + EATest.ea1.write_to_rp(rp2_1) + EATest.ea3.write_to_rp(rp2_2) + ACLTest.acl2.write_to_rp(rp2_2) + + def testSeriesLocal(self): + """Test backing up and restoring EAs/ACLs locally""" + self.make_backup_dirs() + dirlist = ['testfiles/ea_acl_test1', 'testfiles/ea_acl_test2', + 'testfiles/empty', 'testfiles/ea_acl_test1'] + BackupRestoreSeries(1, 1, dirlist, + compare_eas = 1, compare_acls = 1) + + def testSeriesRemote(self): + """Test backing up and restoring EAs/ACLs locally""" + self.make_backup_dirs() + dirlist = ['testfiles/ea_acl_test1', 'testfiles/empty', + 'testfiles/ea_acl_test2', 'testfiles/ea_acl_test1'] + BackupRestoreSeries(None, None, dirlist, + compare_eas = 1, compare_acls = 1) + + if __name__ == "__main__": unittest.main() diff --git a/rdiff-backup/testing/fs_abilitiestest.py b/rdiff-backup/testing/fs_abilitiestest.py index 95fd898..c40b3c0 100644 --- a/rdiff-backup/testing/fs_abilitiestest.py +++ b/rdiff-backup/testing/fs_abilitiestest.py @@ -28,7 +28,8 @@ class FSAbilitiesTest(unittest.TestCase): def testReadOnly(self): """Test basic querying read only""" base_dir = rpath.RPath(Globals.local_connection, self.dir_to_test) - fsa = fs_abilities.FSAbilities().init_readonly(base_dir) + fsa = fs_abilities.FSAbilities('read-only').init_readonly(base_dir) + print fsa assert fsa.read_only == 1, fsa.read_only assert fsa.eas == self.eas, fsa.eas assert fsa.acls == self.acls, fsa.acls @@ -41,8 +42,9 @@ class FSAbilitiesTest(unittest.TestCase): new_dir.setdata() new_dir.mkdir() t = time.time() - fsa = fs_abilities.FSAbilities().init_readwrite(new_dir) + fsa = fs_abilities.FSAbilities('read/write').init_readwrite(new_dir) print "Time elapsed = ", time.time() - t + print fsa assert fsa.read_only == 0, fsa.read_only assert fsa.eas == self.eas, fsa.eas assert fsa.acls == self.acls, fsa.acls |