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