e7a346
From a9db68fc1f05639cb79defef6ed7da58572113ea Mon Sep 17 00:00:00 2001
e7a346
From: Kotresh HR <khiremat@redhat.com>
e7a346
Date: Thu, 5 Jul 2018 07:07:38 -0400
e7a346
Subject: [PATCH 328/333] geo-rep: Fix issues with gfid conflict handling
e7a346
e7a346
1. MKDIR/RMDIR is recorded on all bricks. So if
e7a346
   one brick succeeds creating it, other bricks
e7a346
   should ignore it. But this was not happening.
e7a346
   The fix rename of directories in hybrid crawl,
e7a346
   was trying to rename the directory to itself
e7a346
   and in the process crashing with ENOENT if the
e7a346
   directory is removed.
e7a346
e7a346
2. If file is created, deleted and a directory is
e7a346
   created with same name, it was failing to sync.
e7a346
   Again the issue is around the fix for rename
e7a346
   of directories in hybrid crawl. Fixed the same.
e7a346
e7a346
   If the same case was done with hardlink present
e7a346
   for the file, it was failing. This patch fixes
e7a346
   that too.
e7a346
e7a346
Backport of
e7a346
  > Patch: https://review.gluster.org/#/c/20473/
e7a346
  > fixes: bz#1598884
e7a346
  > Change-Id: I6f3bca44e194e415a3d4de3b9d03cc8976439284
e7a346
  > Signed-off-by: Kotresh HR <khiremat@redhat.com>
e7a346
e7a346
BUG: 1598384
e7a346
Change-Id: I6f3bca44e194e415a3d4de3b9d03cc8976439284
e7a346
Signed-off-by: Kotresh HR <khiremat@redhat.com>
e7a346
Reviewed-on: https://code.engineering.redhat.com/gerrit/143400
e7a346
Tested-by: RHGS Build Bot <nigelb@redhat.com>
e7a346
Reviewed-by: Aravinda Vishwanathapura Krishna Murthy <avishwan@redhat.com>
e7a346
Reviewed-by: Sunil Kumar Heggodu Gopala Acharya <sheggodu@redhat.com>
e7a346
---
e7a346
 geo-replication/syncdaemon/master.py     | 157 ++++++++++++++++++++++---------
e7a346
 geo-replication/syncdaemon/resource.py   |  57 ++++++-----
e7a346
 geo-replication/syncdaemon/syncdutils.py |  35 +++++++
e7a346
 3 files changed, 180 insertions(+), 69 deletions(-)
e7a346
e7a346
diff --git a/geo-replication/syncdaemon/master.py b/geo-replication/syncdaemon/master.py
e7a346
index 64e9836..1399378 100644
e7a346
--- a/geo-replication/syncdaemon/master.py
e7a346
+++ b/geo-replication/syncdaemon/master.py
e7a346
@@ -692,7 +692,8 @@ class GMasterChangelogMixin(GMasterCommon):
e7a346
     TYPE_GFID = "D "
e7a346
     TYPE_ENTRY = "E "
e7a346
 
e7a346
-    MAX_EF_RETRIES = 15
e7a346
+    MAX_EF_RETRIES = 10
e7a346
+    MAX_OE_RETRIES = 5
e7a346
 
e7a346
     # flat directory hierarchy for gfid based access
e7a346
     FLAT_DIR_HIERARCHY = '.'
e7a346
@@ -788,38 +789,53 @@ class GMasterChangelogMixin(GMasterCommon):
e7a346
 
e7a346
         self.status.inc_value("failures", num_failures)
e7a346
 
e7a346
-    def fix_possible_entry_failures(self, failures, retry_count):
e7a346
+    def fix_possible_entry_failures(self, failures, retry_count, entries):
e7a346
         pfx = gauxpfx()
e7a346
         fix_entry_ops = []
e7a346
         failures1 = []
e7a346
         for failure in failures:
e7a346
-            if failure[2]['dst']:
e7a346
+            if failure[2]['name_mismatch']:
e7a346
+                pbname = failure[2]['slave_entry']
e7a346
+            elif failure[2]['dst']:
e7a346
                 pbname = failure[0]['entry1']
e7a346
             else:
e7a346
                 pbname = failure[0]['entry']
e7a346
-            if failure[2]['gfid_mismatch']:
e7a346
+
e7a346
+            op = failure[0]['op']
e7a346
+            # name exists but gfid is different
e7a346
+            if failure[2]['gfid_mismatch'] or failure[2]['name_mismatch']:
e7a346
                 slave_gfid = failure[2]['slave_gfid']
e7a346
                 st = lstat(os.path.join(pfx, slave_gfid))
e7a346
+                # Takes care of scenarios with no hardlinks
e7a346
                 if isinstance(st, int) and st == ENOENT:
e7a346
-                    logging.info(lf('Fixing gfid mismatch in slave. Deleting'
e7a346
-                                    ' the entry', retry_count=retry_count,
e7a346
+                    logging.info(lf('Entry not present on master. Fixing gfid '
e7a346
+                                    'mismatch in slave. Deleting the entry',
e7a346
+                                    retry_count=retry_count,
e7a346
                                     entry=repr(failure)))
e7a346
-                    #Add deletion to fix_entry_ops list
e7a346
+                    # Add deletion to fix_entry_ops list
e7a346
                     if failure[2]['slave_isdir']:
e7a346
-                        fix_entry_ops.append(edct('RMDIR',
e7a346
-                                                  gfid=failure[2]['slave_gfid'],
e7a346
-                                                  entry=pbname))
e7a346
+                        fix_entry_ops.append(
e7a346
+                            edct('RMDIR',
e7a346
+                                 gfid=failure[2]['slave_gfid'],
e7a346
+                                 entry=pbname))
e7a346
                     else:
e7a346
-                        fix_entry_ops.append(edct('UNLINK',
e7a346
-                                                  gfid=failure[2]['slave_gfid'],
e7a346
-                                                  entry=pbname))
e7a346
+                        fix_entry_ops.append(
e7a346
+                            edct('UNLINK',
e7a346
+                                 gfid=failure[2]['slave_gfid'],
e7a346
+                                 entry=pbname))
e7a346
+                # Takes care of scenarios of hardlinks/renames on master
e7a346
                 elif not isinstance(st, int):
e7a346
-                    #The file exists on master but with different name.
e7a346
-                    #Probabaly renamed and got missed during xsync crawl.
e7a346
-                    if failure[2]['slave_isdir']:
e7a346
-                        logging.info(lf('Fixing gfid mismatch in slave',
e7a346
+                    if matching_disk_gfid(slave_gfid, pbname):
e7a346
+                        # Safe to ignore the failure as master contains same
e7a346
+                        # file with same gfid. Remove entry from entries list
e7a346
+                        logging.info(lf('Fixing gfid mismatch in slave. '
e7a346
+                                        ' Safe to ignore, take out entry',
e7a346
                                         retry_count=retry_count,
e7a346
                                         entry=repr(failure)))
e7a346
+                        entries.remove(failure[0])
e7a346
+                    # The file exists on master but with different name.
e7a346
+                    # Probably renamed and got missed during xsync crawl.
e7a346
+                    elif failure[2]['slave_isdir']:
e7a346
                         realpath = os.readlink(os.path.join(gconf.local_path,
e7a346
                                                             ".glusterfs",
e7a346
                                                             slave_gfid[0:2],
e7a346
@@ -827,64 +843,99 @@ class GMasterChangelogMixin(GMasterCommon):
e7a346
                                                             slave_gfid))
e7a346
                         dst_entry = os.path.join(pfx, realpath.split('/')[-2],
e7a346
                                                  realpath.split('/')[-1])
e7a346
-                        rename_dict = edct('RENAME', gfid=slave_gfid,
e7a346
-                                           entry=failure[0]['entry'],
e7a346
-                                           entry1=dst_entry, stat=st,
e7a346
-                                           link=None)
e7a346
-                        logging.info(lf('Fixing gfid mismatch in slave. '
e7a346
-                                        'Renaming', retry_count=retry_count,
e7a346
-                                        entry=repr(rename_dict)))
e7a346
-                        fix_entry_ops.append(rename_dict)
e7a346
+                        src_entry = pbname
e7a346
+                        logging.info(lf('Fixing dir name/gfid mismatch in '
e7a346
+                                        'slave', retry_count=retry_count,
e7a346
+                                        entry=repr(failure)))
e7a346
+                        if src_entry == dst_entry:
e7a346
+                            # Safe to ignore the failure as master contains
e7a346
+                            # same directory as in slave with same gfid.
e7a346
+                            # Remove the failure entry from entries list
e7a346
+                            logging.info(lf('Fixing dir name/gfid mismatch'
e7a346
+                                            ' in slave. Safe to ignore, '
e7a346
+                                            'take out entry',
e7a346
+                                            retry_count=retry_count,
e7a346
+                                            entry=repr(failure)))
e7a346
+                            entries.remove(failure[0])
e7a346
+                        else:
e7a346
+                            rename_dict = edct('RENAME', gfid=slave_gfid,
e7a346
+                                               entry=src_entry,
e7a346
+                                               entry1=dst_entry, stat=st,
e7a346
+                                               link=None)
e7a346
+                            logging.info(lf('Fixing dir name/gfid mismatch'
e7a346
+                                            ' in slave. Renaming',
e7a346
+                                            retry_count=retry_count,
e7a346
+                                            entry=repr(rename_dict)))
e7a346
+                            fix_entry_ops.append(rename_dict)
e7a346
                     else:
e7a346
-                        logging.info(lf('Fixing gfid mismatch in slave. '
e7a346
-                                        ' Deleting the entry',
e7a346
+                        # A hardlink file exists with different name or
e7a346
+                        # renamed file exists and we are sure from
e7a346
+                        # matching_disk_gfid check that the entry doesn't
e7a346
+                        # exist with same gfid so we can safely delete on slave
e7a346
+                        logging.info(lf('Fixing file gfid mismatch in slave. '
e7a346
+                                        'Hardlink/Rename Case. Deleting entry',
e7a346
+                                        retry_count=retry_count,
e7a346
+                                        entry=repr(failure)))
e7a346
+                        fix_entry_ops.append(
e7a346
+                            edct('UNLINK',
e7a346
+                                 gfid=failure[2]['slave_gfid'],
e7a346
+                                 entry=pbname))
e7a346
+            elif failure[1] == ENOENT:
e7a346
+                # Ignore ENOENT error for fix_entry_ops aka retry_count > 1
e7a346
+                if retry_count > 1:
e7a346
+                    logging.info(lf('ENOENT error while fixing entry ops. '
e7a346
+                                    'Safe to ignore, take out entry',
e7a346
+                                    retry_count=retry_count,
e7a346
+                                    entry=repr(failure)))
e7a346
+                    entries.remove(failure[0])
e7a346
+                elif op in ('MKNOD', 'CREATE', 'MKDIR'):
e7a346
+                    pargfid = pbname.split('/')[1]
e7a346
+                    st = lstat(os.path.join(pfx, pargfid))
e7a346
+                    # Safe to ignore the failure as master doesn't contain
e7a346
+                    # parent directory.
e7a346
+                    if isinstance(st, int):
e7a346
+                        logging.info(lf('Fixing ENOENT error in slave. Parent '
e7a346
+                                        'does not exist on master. Safe to '
e7a346
+                                        'ignore, take out entry',
e7a346
                                         retry_count=retry_count,
e7a346
                                         entry=repr(failure)))
e7a346
-                        fix_entry_ops.append(edct('UNLINK',
e7a346
-                                                  gfid=failure[2]['slave_gfid'],
e7a346
-                                                  entry=pbname))
e7a346
-                        logging.error(lf('Entry cannot be fixed in slave due '
e7a346
-                                         'to GFID mismatch, find respective '
e7a346
-                                         'path for the GFID and trigger sync',
e7a346
-                                         gfid=slave_gfid))
e7a346
+                        entries.remove(failure[0])
e7a346
 
e7a346
         if fix_entry_ops:
e7a346
-            #Process deletions of entries whose gfids are mismatched
e7a346
+            # Process deletions of entries whose gfids are mismatched
e7a346
             failures1 = self.slave.server.entry_ops(fix_entry_ops)
e7a346
-            if not failures1:
e7a346
-                logging.info ("Sucessfully fixed entry ops with gfid mismatch")
e7a346
 
e7a346
-        return failures1
e7a346
+        return (failures1, fix_entry_ops)
e7a346
 
e7a346
     def handle_entry_failures(self, failures, entries):
e7a346
         retries = 0
e7a346
         pending_failures = False
e7a346
         failures1 = []
e7a346
         failures2 = []
e7a346
+        entry_ops1 = []
e7a346
+        entry_ops2 = []
e7a346
 
e7a346
         if failures:
e7a346
             pending_failures = True
e7a346
             failures1 = failures
e7a346
+            entry_ops1 = entries
e7a346
 
e7a346
             while pending_failures and retries < self.MAX_EF_RETRIES:
e7a346
                 retries += 1
e7a346
-                failures2 = self.fix_possible_entry_failures(failures1,
e7a346
-                                                             retries)
e7a346
+                (failures2, entry_ops2) = self.fix_possible_entry_failures(
e7a346
+                    failures1, retries, entry_ops1)
e7a346
                 if not failures2:
e7a346
                     pending_failures = False
e7a346
+                    logging.info(lf('Sucessfully fixed entry ops with gfid '
e7a346
+                                 'mismatch', retry_count=retries))
e7a346
                 else:
e7a346
                     pending_failures = True
e7a346
                     failures1 = failures2
e7a346
+                    entry_ops1 = entry_ops2
e7a346
 
e7a346
             if pending_failures:
e7a346
                 for failure in failures1:
e7a346
                     logging.error("Failed to fix entry ops %s", repr(failure))
e7a346
-            else:
e7a346
-                #Retry original entry list 5 times
e7a346
-                failures = self.slave.server.entry_ops(entries)
e7a346
-
e7a346
-            self.log_failures(failures, 'gfid', gauxpfx(), 'ENTRY')
e7a346
-
e7a346
 
e7a346
     def process_change(self, change, done, retry):
e7a346
         pfx = gauxpfx()
e7a346
@@ -1112,7 +1163,19 @@ class GMasterChangelogMixin(GMasterCommon):
e7a346
             self.status.inc_value("entry", len(entries))
e7a346
 
e7a346
             failures = self.slave.server.entry_ops(entries)
e7a346
-            self.handle_entry_failures(failures, entries)
e7a346
+            count = 0
e7a346
+            while failures and count < self.MAX_OE_RETRIES:
e7a346
+                count += 1
e7a346
+                self.handle_entry_failures(failures, entries)
e7a346
+                logging.info("Retry original entries. count = %s" % count)
e7a346
+                failures = self.slave.server.entry_ops(entries)
e7a346
+                if not failures:
e7a346
+                    logging.info("Sucessfully fixed all entry ops with gfid "
e7a346
+                                 "mismatch")
e7a346
+                    break
e7a346
+
e7a346
+            self.log_failures(failures, 'gfid', gauxpfx(), 'ENTRY')
e7a346
+
e7a346
             self.status.dec_value("entry", len(entries))
e7a346
 
e7a346
             # Update Entry stime in Brick Root only in case of Changelog mode
e7a346
diff --git a/geo-replication/syncdaemon/resource.py b/geo-replication/syncdaemon/resource.py
e7a346
index 0d5462a..eb696f3 100644
e7a346
--- a/geo-replication/syncdaemon/resource.py
e7a346
+++ b/geo-replication/syncdaemon/resource.py
e7a346
@@ -38,9 +38,9 @@ from syncdutils import CHANGELOG_AGENT_CLIENT_VERSION
e7a346
 from syncdutils import GX_GFID_CANONICAL_LEN
e7a346
 from gsyncdstatus import GeorepStatus
e7a346
 from syncdutils import get_master_and_slave_data_from_args
e7a346
-from syncdutils import lf, Popen, sup, Volinfo
e7a346
+from syncdutils import lf, Popen, sup
e7a346
 from syncdutils import Xattr, matching_disk_gfid, get_gfid_from_mnt
e7a346
-from syncdutils import unshare_propagation_supported
e7a346
+from syncdutils import unshare_propagation_supported, get_slv_dir_path
e7a346
 
e7a346
 UrlRX = re.compile('\A(\w+)://([^ *?[]*)\Z')
e7a346
 HostRX = re.compile('[a-zA-Z\d](?:[a-zA-Z\d.-]*[a-zA-Z\d])?', re.I)
e7a346
@@ -50,7 +50,6 @@ ENOTSUP = getattr(errno, 'ENOTSUP', 'EOPNOTSUPP')
e7a346
 
e7a346
 slv_volume = None
e7a346
 slv_host = None
e7a346
-slv_bricks = None
e7a346
 
e7a346
 def desugar(ustr):
e7a346
     """transform sugared url strings to standard <scheme>://<urlbody> form
e7a346
@@ -463,13 +462,23 @@ class Server(object):
e7a346
             # to be purged is the GFID gotten from the changelog.
e7a346
             # (a stat(changelog_gfid) would also be valid here)
e7a346
             # The race here is between the GFID check and the purge.
e7a346
+
e7a346
+            # If the entry or the gfid of the file to be deleted is not present
e7a346
+            # on slave, we can ignore the unlink/rmdir
e7a346
+            if isinstance(lstat(entry), int) or \
e7a346
+               isinstance(lstat(os.path.join(pfx, gfid)), int):
e7a346
+                return
e7a346
+
e7a346
             if not matching_disk_gfid(gfid, entry):
e7a346
                 collect_failure(e, EEXIST)
e7a346
                 return
e7a346
 
e7a346
             if op == 'UNLINK':
e7a346
                 er = errno_wrap(os.unlink, [entry], [ENOENT, ESTALE], [EBUSY])
e7a346
-                return er
e7a346
+                # EISDIR is safe error, ignore. This can only happen when
e7a346
+                # unlink is sent from master while fixing gfid conflicts.
e7a346
+                if er != EISDIR:
e7a346
+                    return er
e7a346
 
e7a346
             elif op == 'RMDIR':
e7a346
                 er = errno_wrap(os.rmdir, [entry], [ENOENT, ESTALE,
e7a346
@@ -480,7 +489,11 @@ class Server(object):
e7a346
         def collect_failure(e, cmd_ret, dst=False):
e7a346
             slv_entry_info = {}
e7a346
             slv_entry_info['gfid_mismatch'] = False
e7a346
+            slv_entry_info['name_mismatch'] = False
e7a346
             slv_entry_info['dst'] = dst
e7a346
+            slv_entry_info['slave_isdir'] = False
e7a346
+            slv_entry_info['slave_name'] = None
e7a346
+            slv_entry_info['slave_gfid'] = None
e7a346
             # We do this for failing fops on Slave
e7a346
             # Master should be logging this
e7a346
             if cmd_ret is None:
e7a346
@@ -498,6 +511,9 @@ class Server(object):
e7a346
                     if not isinstance(st, int):
e7a346
                         if st and stat.S_ISDIR(st.st_mode):
e7a346
                             slv_entry_info['slave_isdir'] = True
e7a346
+                            dir_name = get_slv_dir_path(slv_host, slv_volume,
e7a346
+                                                        disk_gfid)
e7a346
+                            slv_entry_info['slave_name'] = dir_name
e7a346
                         else:
e7a346
                             slv_entry_info['slave_isdir'] = False
e7a346
                     slv_entry_info['slave_gfid'] = disk_gfid
e7a346
@@ -618,37 +634,34 @@ class Server(object):
e7a346
                                          [ENOENT, EEXIST], [ESTALE])
e7a346
                     collect_failure(e, cmd_ret)
e7a346
             elif op == 'MKDIR':
e7a346
+                en = e['entry']
e7a346
                 slink = os.path.join(pfx, gfid)
e7a346
                 st = lstat(slink)
e7a346
                 # don't create multiple entries with same gfid
e7a346
                 if isinstance(st, int):
e7a346
                     blob = entry_pack_mkdir(
e7a346
                         gfid, bname, e['mode'], e['uid'], e['gid'])
e7a346
-                else:
e7a346
+                elif (isinstance(lstat(en), int) or
e7a346
+                      not matching_disk_gfid(gfid, en)):
e7a346
                     # If gfid of a directory exists on slave but path based
e7a346
                     # create is getting EEXIST. This means the directory is
e7a346
                     # renamed in master but recorded as MKDIR during hybrid
e7a346
                     # crawl. Get the directory path by reading the backend
e7a346
                     # symlink and trying to rename to new name as said by
e7a346
                     # master.
e7a346
-                    global slv_bricks
e7a346
-                    global slv_volume
e7a346
-                    global slv_host
e7a346
-                    if not slv_bricks:
e7a346
-                        slv_info = Volinfo (slv_volume, slv_host)
e7a346
-                        slv_bricks = slv_info.bricks
e7a346
-                    # Result of readlink would be of format as below.
e7a346
-                    # readlink = "../../pgfid[0:2]/pgfid[2:4]/pgfid/basename"
e7a346
-                    realpath = os.readlink(os.path.join(slv_bricks[0]['dir'],
e7a346
-                                                        ".glusterfs", gfid[0:2],
e7a346
-                                                        gfid[2:4], gfid))
e7a346
-                    realpath_parts = realpath.split('/')
e7a346
-                    src_pargfid = realpath_parts[-2]
e7a346
-                    src_basename = realpath_parts[-1]
e7a346
-                    src_entry = os.path.join(pfx, src_pargfid, src_basename)
e7a346
                     logging.info(lf("Special case: rename on mkdir",
e7a346
-                                   gfid=gfid, entry=repr(entry)))
e7a346
-                    rename_with_disk_gfid_confirmation(gfid, src_entry, entry)
e7a346
+                                    gfid=gfid, entry=repr(entry)))
e7a346
+                    src_entry = get_slv_dir_path(slv_host, slv_volume, gfid)
e7a346
+                    if src_entry is not None and src_entry != entry:
e7a346
+                        slv_entry_info = {}
e7a346
+                        slv_entry_info['gfid_mismatch'] = False
e7a346
+                        slv_entry_info['name_mismatch'] = True
e7a346
+                        slv_entry_info['dst'] = False
e7a346
+                        slv_entry_info['slave_isdir'] = True
e7a346
+                        slv_entry_info['slave_gfid'] = gfid
e7a346
+                        slv_entry_info['slave_entry'] = src_entry
e7a346
+
e7a346
+                        failures.append((e, EEXIST, slv_entry_info))
e7a346
             elif op == 'LINK':
e7a346
                 slink = os.path.join(pfx, gfid)
e7a346
                 st = lstat(slink)
e7a346
diff --git a/geo-replication/syncdaemon/syncdutils.py b/geo-replication/syncdaemon/syncdutils.py
e7a346
index 6dafb0a..d798356 100644
e7a346
--- a/geo-replication/syncdaemon/syncdutils.py
e7a346
+++ b/geo-replication/syncdaemon/syncdutils.py
e7a346
@@ -77,6 +77,7 @@ CHANGELOG_AGENT_CLIENT_VERSION = 1.0
e7a346
 NodeID = None
e7a346
 rsync_version = None
e7a346
 unshare_mnt_propagation = None
e7a346
+slv_bricks = None
e7a346
 SPACE_ESCAPE_CHAR = "%20"
e7a346
 NEWLINE_ESCAPE_CHAR = "%0A"
e7a346
 PERCENTAGE_ESCAPE_CHAR = "%25"
e7a346
@@ -671,6 +672,40 @@ def get_rsync_version(rsync_cmd):
e7a346
     return rsync_version
e7a346
 
e7a346
 
e7a346
+def get_slv_dir_path(slv_host, slv_volume, gfid):
e7a346
+    global slv_bricks
e7a346
+
e7a346
+    dir_path = ENOENT
e7a346
+
e7a346
+    if not slv_bricks:
e7a346
+        slv_info = Volinfo(slv_volume, slv_host)
e7a346
+        slv_bricks = slv_info.bricks
e7a346
+    # Result of readlink would be of format as below.
e7a346
+    # readlink = "../../pgfid[0:2]/pgfid[2:4]/pgfid/basename"
e7a346
+    for brick in slv_bricks:
e7a346
+        dir_path = errno_wrap(os.path.join,
e7a346
+                              [brick['dir'],
e7a346
+                               ".glusterfs", gfid[0:2],
e7a346
+                               gfid[2:4],
e7a346
+                               gfid], [ENOENT], [ESTALE])
e7a346
+        if dir_path != ENOENT:
e7a346
+            break
e7a346
+
e7a346
+    if not isinstance(dir_path, int):
e7a346
+        realpath = errno_wrap(os.readlink, [dir_path],
e7a346
+                              [ENOENT], [ESTALE])
e7a346
+
e7a346
+        if not isinstance(realpath, int):
e7a346
+            realpath_parts = realpath.split('/')
e7a346
+            pargfid = realpath_parts[-2]
e7a346
+            basename = realpath_parts[-1]
e7a346
+            pfx = gauxpfx()
e7a346
+            dir_entry = os.path.join(pfx, pargfid, basename)
e7a346
+            return dir_entry
e7a346
+
e7a346
+    return None
e7a346
+
e7a346
+
e7a346
 def lf(event, **kwargs):
e7a346
     """
e7a346
     Log Format helper function, log messages can be
e7a346
-- 
e7a346
1.8.3.1
e7a346