]> git.ipfire.org Git - thirdparty/bacula.git/commitdiff
Fix #9749: backup windows mount points and cleanup and improve win32 code
authorAlain Spineux <alain@baculasystems.com>
Fri, 17 Feb 2023 14:42:54 +0000 (15:42 +0100)
committerEric Bollengier <eric@baculasystems.com>
Thu, 14 Sep 2023 11:57:01 +0000 (13:57 +0200)
bacula/src/filed/job.c
bacula/src/win32/compat/mtab.cpp
bacula/src/win32/compat/mtab.h
bacula/src/win32/filed/vss_generic.cpp

index 20ee2293c599a50eabd63154da7eb6204562dc3a..a719feae2dffd92274683bd392de81cfd36c068b 100644 (file)
@@ -2132,7 +2132,21 @@ static int set_options(findFOPTS *fo, const char *opts)
 #ifdef HAVE_WIN32
 
 /* TODO: merge find.c ? */
-static bool is_excluded(findFILESET *fileset, char *path)
+/* Check if path is excluded by the exclude list of the fileset
+ * path is checked using Bacula's fnmatch() (in win32)
+ * and then compared char by char with some flexibility with the ending '/'
+ * or '\\' on the FileSet side
+ * File=C:/mnt/ in the FileSet will match C:/mnt and C:/mnt/
+ * If you know that the path is a directory, th'n you can set is_path_a_dir
+ * and then
+ * File=C:/mnt in the FileSet will match C:/mnt and C:/mnt/
+ * Notice that
+ * File=C:/mnt/+ (with a * and not a +) don't match C:/mnt nor C:/mnt/
+ *
+ * This function is only used when "building" the FileSet, it is not "yet" used
+ * when walking the tree during the backup
+ */
+static bool is_excluded(findFILESET *fileset, char *path, bool is_path_a_dir)
 {
    int fnm_flags=FNM_CASEFOLD;  /* FIXME: Not exactly accurate, the IGNORECASE option is not available... */
    int fnmode=0;
@@ -2169,12 +2183,16 @@ static bool is_excluded(findFILESET *fileset, char *path)
          }
 
          /* Looks to be the same string, but with a trailing slash */
-         if (fname[0] && IsPathSeparator(fname[0]) && fname[1] == '\0'
-             && p[0] == '\0')
-         {
+         if (IsPathSeparator(fname[0]) && fname[1] == '\0' && p[0] == '\0') {
             Dmsg1(DT_VOLUME|50, "Reject: %s\n", path);
             return true;
          }
+         /* if path is a directory, try stripping the final '/' in path */
+         if (is_path_a_dir && IsPathSeparator(p[0]) && p[1] == '\0'
+               && fname[0] == '\0') {
+            Dmsg1(DT_VOLUME|50, "Reject dir: %s\n", path);
+            return true;
+         }
       }
    }
    return false;
@@ -2229,7 +2247,7 @@ static findINCEXE *is_included(findFILESET *fileset, char *path)
 }
 
 /* Here we include all drives that are listed in your mtab structure
- * "/" backs up everyting with a drive letter.
+ * "/" backs up everything with a drive letter.
  * "OneFS = No" would then go down mounted file systems.
  */
 static void list_drives(findFILESET *fileset, dlist *name_list, MTab *mtab)
@@ -2247,27 +2265,23 @@ static void list_drives(findFILESET *fileset, dlist *name_list, MTab *mtab)
       }
 
       WCHAR *dir;
-      /* We determine the drive letter of the volume if any */
+      /* We determine a path that is not excluded FileSet */
       for (dir = elt->first(); dir ; dir = elt->next(dir)) {
-         if (wcslen(dir) == 3 && dir[1] == L':') {
-            break;
+         if (wcslen(dir) != 3 || dir[1] != L':') {
+            continue; /* Exclude path that are not Drive Letter */
          }
-      }
-      if (dir && wchar_path_2_wutf8(&buf, dir) > 0) {
-
-         /* Path from MTAb are using \ and the catalog
-          * contains c:\subdir\path/ instead of c:/subdir/path/
-          * so we do a quick fix...
-          */
-         for(char *p = buf; *p ; p++) {
-            if (*p == '\\') {
-               *p = '/';
-            }
+         if (wchar_path_2_wutf8(&buf, dir) <= 0) {
+            continue; /* something wrong with the name, skip */
          }
-         if (!is_excluded(fileset, buf)) {
-            Dmsg1(DT_VOLUME|50,"Adding drive in include list %s\n",buf);
-            name_list->append(new_dlistString(buf));
+         win32_to_unix_slash(buf);
+         if (is_excluded(fileset, buf, true)) {
+            continue; /* the name is in the exclude list */
          }
+         remove_win32_trailing_slash(buf);
+         /* add the name without the ending '/' */
+         Dmsg1(DT_VOLUME|50,"Adding drive in include list %s\n",buf);
+         name_list->append(new_dlistString(buf));
+         break; /* adding a Volume once is enough */
       }
    }
    free_pool_memory(buf);
@@ -2279,6 +2293,8 @@ static void list_drives(findFILESET *fileset, dlist *name_list, MTab *mtab)
  * are used, because we create a snapshot of all used
  * drives before operation
  *
+ * szDrives : is a list of the drives where we must active the snapshot
+ *   this is used by some plugins like cdp & delta
  */
 static int
 get_win32_driveletters(JCR *jcr, FF_PKT *ff, char* szDrives, void* mtab_def)
@@ -2286,7 +2302,8 @@ get_win32_driveletters(JCR *jcr, FF_PKT *ff, char* szDrives, void* mtab_def)
    int nCount = 0;
 #ifdef HAVE_WIN32
    findFILESET *fileset = ff->fileset;
-   findINCEXE *alldrives = NULL, *inc = NULL;
+   findINCEXE *alldrives = NULL; /* the fileset that holds "File = /" if any */
+   findINCEXE *inc = NULL;
    uint64_t    flags = 0;
    char drive[4];
 
@@ -2294,9 +2311,11 @@ get_win32_driveletters(JCR *jcr, FF_PKT *ff, char* szDrives, void* mtab_def)
    MTab *mtab = (MTab*) mtab_def;
    if (!mtab_def) {
       mtab = New(MTab());
-      mtab->get();
+      mtab->load_volumes();
    }
 
+   dump_name_list(__FILE__, __LINE__, DT_VOLUME|50, "NameList in", fileset);
+
    /* We check if we need to complete the fileset with File=/ */
    if (fileset) {
       for (int i=0; i<fileset->include_list.size(); i++) {
@@ -2322,7 +2341,7 @@ get_win32_driveletters(JCR *jcr, FF_PKT *ff, char* szDrives, void* mtab_def)
          drive[0] = szDrives[i];
          if (mtab->addInSnapshotSet(drive)) { /* When all volumes are selected, we can stop */
             Dmsg0(DT_VOLUME|50, "All Volumes are marked, stopping the loop here\n");
-            goto all_included;
+            goto all_included;  // TODO ASX I don't like this goto, I would prefer a break
          }
       }
    }
@@ -2330,7 +2349,7 @@ get_win32_driveletters(JCR *jcr, FF_PKT *ff, char* szDrives, void* mtab_def)
    if (fileset) {
 
       /* Writer dictates the snapshot included components
-       * but we still give the chanche to include extra files to
+       * but we still give the chance to include extra files to
        * the snapshot set via fileset include_list
        */
       dlistString *node;
@@ -2344,94 +2363,117 @@ get_win32_driveletters(JCR *jcr, FF_PKT *ff, char* szDrives, void* mtab_def)
             if (mtab->addInSnapshotSet(fname)) {
                /* When all volumes are selected, we can stop */
                Dmsg0(DT_VOLUME|50, "All Volumes are marked, stopping the loop here\n");
-               goto all_included;
+               goto all_included;  // TODO ASX I don't like this goto, I would prefer a break
             }
          }
       }
 
+      dump_name_list(__FILE__, __LINE__, DT_VOLUME|50, "NameList intermediate", fileset);
+
       /* TODO: it needs to be done Include by Include, but in the worst case,
        * we take too much snapshots...
        */
       if (flags & FO_MULTIFS) {
-         /* Need to add subdirectories */
+         /* Need to add subdirectories that are mount points
+          * The idea is that the FD will not cross directories that are mount
+          * points, but instead, we add the directory to the Include list.
+          */
          POOLMEM   *fn = get_pool_memory(PM_FNAME);
          MTabEntry *elt, *elt2;
          int len;
 
          Dmsg0(DT_VOLUME|50, "OneFS is set, looking for remaining volumes\n");
+         mtab->dump(__FILE__, __LINE__, DT_VOLUME|50, "ASX mtab: ");
 
          foreach_rblist(elt, mtab->entries) {
             if (elt->in_SnapshotSet) {
-               continue;         /* Already in */
+               Dmsg1(DT_VOLUME|50, "Skip volume that is already in the snapshot list: %ls\n", elt->first());
+               continue;         /* Was already selected in the Include list */
+               /* ASX We should not do that
+                * imaginons qu'un volume soit monté sur C:/mnt et F:/ en meme temps
+                * et qu'on a File=C:/, dans ce cas C:/ et c/mnt sont backupé
+                * plus tard l'utilisateur ajoute File=F:/toto
+                * ce dernier va forcer le snapshot sur le volume en question
+                * et du coups C:/mnt ne sera plus considéré et donc non backupé
+                * si on inclu aussi C:/mnt, on aura 2x toto
+                * on devrait pas faire un continue ici
+                */
             }
             /* A volume can have multiple mount points */
             for (wchar_t *p = elt->first() ; p && *p ; p = elt->next(p)) {
                wchar_path_2_wutf8(&fn, p);
+               win32_to_unix_slash(fn);
+               /* Notice that fn is ended with a '/' */
 
                Dmsg1(DT_VOLUME|50, "Looking for path %s\n", fn);
 
-               /* First case, root drive (c:/, e:/, d:/), not a submount point */
+               /* Bacula don't consider drive letter (X:/), as submount point
+                * Notice that when "File=/" is used, the drive letters have
+                * been already added above
+                */
                len = strlen(fn);
                if (len <= 3) {
-                  Dmsg1(DT_VOLUME|50, "Skiping %s\n", fn);
-                  continue;
+                  Dmsg1(DT_VOLUME|50, "Skipping drive letter: %s\n", fn);
+                  continue; /* we are looking for mount point */
                }
 
                /* First thing is to look in the exclude list to see if this directory
                 * is explicitly excluded
                 */
-               if (!is_excluded(fileset, fn)) {
-                  elt->setInSnapshotSet();
-               } else {
+               if (is_excluded(fileset, fn, true)) {
                   Dmsg1(DT_VOLUME|50, "Looks to be excluded %s\n", fn);
-                  continue;
+                  continue; /* try another path for the volume if any */
                }
-
-               /* c:/vol/vol2/vol3
-               * will look c:/, then c:/vol/, then c:/vol2/ and if one of them
-               * is selected, the sub volume will be directly marked.
+               /* In the case of a FO_MULTIFS
+                * If X:/ is Included in the fileset, then every volumes mounted
+                * below X:/, let say X:/mnt must also be Included and the
+                * snapshot activated.
+                *
+                * Say differently, looking from the list of mount points,
+                * if X:/mnt is a mount point and X:/ is included, then X:/mnt
+                * must be included.
+                * In the case of "c:/vol/vol2/vol3/"  (notice the ending '/')
+                * we will look for c:, c:/vol, c:/vol/vol2 and c:/vol/vol2/vol3
+                * if one is included then we add c:/vol/vol2/vol3
                */
-               for (char *p1 = fn ; *p1 && !elt->in_SnapshotSet ; p1++) {
-                  if (IsPathSeparator(*p1)) {
-                     bool to_add=false; /* Add this volume to the FileSet ? */
-                     char c = *(p1 + 1);
-                     *(p1 + 1) = 0;
-
-                     /* We look for the previous directory, and if marked, we mark
-                     * the current one as well
-                     */
-                     Dmsg1(DT_VOLUME|50, "Looking for %s\n", fn);
-                     elt2 = mtab->search(fn);
-                     if (elt2 && elt2->in_SnapshotSet) {
-                        Dmsg0(DT_VOLUME|50, "Put volume in SnapshotSet\n");
-                        elt->setInSnapshotSet();
-                        to_add = true;
-                     }
-
-                     /* Find where to add the new volume, normally near the
-                     * root volume, or if we are using /
-                     */
-                     if (alldrives) {
-                        inc = alldrives;
-                     } else {
-                        inc = is_included(fileset, fn);
-                        Dmsg2(DT_VOLUME|50, "Adding volume in fileset 0x%p %s\n", inc, fn);
-                     }
-
-                     *(p1 + 1) = c; /* restore path separator */
-
-                     /* We can add the current volume to the FileSet */
-                     if (to_add && inc != NULL) {
-                        /* Convert the path to /xxx/xxx */
-                        dlistString *tmp = new_dlistString(fn);
-                        for(char *p = tmp->c_str(); *p ; p++) {
-                           if (*p == '\\') {
-                              *p = '/';
-                           }
-                        }
-                        inc->name_list.append(tmp);
-                        Dmsg1(DT_VOLUME|50,"Adding volume in file list %s\n",tmp->c_str());
-                     }
+               for (char *p1 = fn; *p1 != '\0'; p1++) {
+                  if (!IsPathSeparator(*p1)) {
+                     continue;
+                  }
+                  *p1 = '\0'; // replace the '/' with a '\0'
+
+                  /* We look for the previous directory, and if marked, we mark
+                  * the current one as well
+                  */
+                  Dmsg1(DT_VOLUME|50, "Looking for %s\n", fn);
+                  elt2 = mtab->search(fn);
+                  if (elt2 == NULL || !elt2->in_SnapshotSet) {
+                     /* fn is not in a volume that is snapshoted */
+                     *p1 = '/'; /* restore path separator */
+                     continue;
+                  }
+
+                  /* Find where to add the new volume, normally near the
+                  * root volume, or if we are using /
+                  */
+                  if (alldrives) {
+                     inc = alldrives;
+                  } else {
+                     inc = is_included(fileset, fn);
+                     Dmsg2(DT_VOLUME|50, "Adding volume in fileset 0x%p %s\n", inc, fn);
+                  }
+
+                  *p1 = '/'; /* restore path separator */
+
+                  /* We can add the current volume to the FileSet */
+                  if (inc != NULL) {
+                     dlistString *tmp = new_dlistString(fn);
+                     remove_win32_trailing_slash(tmp->c_str()); /* remove the trailing slash */
+                     inc->name_list.append(tmp);
+                     Dmsg1(DT_VOLUME|50,"Adding volume in file list %s\n", fn);
+                     elt->setInSnapshotSet();
+                     Dmsg1(DT_VOLUME|50, "Put volume in SnapshotSet: %s\n", fn);
+                     break;
                   }
                }
             }
@@ -2439,6 +2481,8 @@ get_win32_driveletters(JCR *jcr, FF_PKT *ff, char* szDrives, void* mtab_def)
          free_pool_memory(fn);
       }
 all_included:
+      dump_name_list(__FILE__, __LINE__, DT_VOLUME|50, "NameList out", fileset);
+
       /* Now, we look the volume list to know which one to include */
       MTabEntry *elt;
       foreach_rblist(elt, mtab->entries) {
@@ -3015,6 +3059,7 @@ static int backup_cmd(JCR *jcr)
       jcr->pVSSClient = VSSInit();
       if (jcr->pVSSClient->InitializeForBackup(jcr)) {
          MTab *tab = jcr->pVSSClient->GetVolumeList();
+         tab->dump(__FILE__, __LINE__, DT_VOLUME|50, "mtab: ");
          generate_plugin_event(jcr, bEventVssBackupAddComponents, tab);
          /* tell vss which drives to snapshot */
          char szWinDriveLetters[27];
@@ -3040,6 +3085,7 @@ static int backup_cmd(JCR *jcr)
          } else {
             Jmsg(jcr, M_WARNING, 0, _("No drive letters found for generating VSS snapshots.\n"));
          }
+         tab->dump(__FILE__, __LINE__, DT_VOLUME|50, "mtab: ");
       } else {
          berrno be;
          Jmsg(jcr, M_FATAL, 0, _("VSS was not initialized properly. ERR=%s\n"),
index fc8627d3daebfd3280edce104dfb5e84a77d8b8d..30fb3c22f60802192eeddc088a2443b32d082fe7 100644 (file)
@@ -107,6 +107,7 @@ MTab::MTab() {
     lasterror = ERROR_SUCCESS;
     lasterror_str = "";
     nb_in_SnapshotSet = 0;
+    nb_in_SuitableForSnapshot = 0;
     entries = New(rblist(elt, &elt->link));
 }
 
@@ -120,21 +121,30 @@ MTab::~MTab() {
    }
 }
 
+/* we are looking for DRIVE_FIXED drive */
 UINT MTabEntry::getDriveType()
 {
    WCHAR *root = first();
 
    // Make sure to discard CD-ROM and network drives
    if (!root) {
-      return 0;
+      return DRIVE_UNKNOWN; /* zero */
    }
 
    driveType = GetDriveTypeW(root);
    return driveType;
 }
 
-/* Return true if the current volume can be snapshoted (ie not CDROM or fat32) */
+/* Return true if the current volume can be snapshoted:
+ * if it is a FIXED disk with a Filesystem that support snapshot
+ * (ie not CDROM or fat32)
+ */
 bool MTabEntry::isSuitableForSnapshot()
+{
+   return can_Snapshot;
+}
+
+bool MTabEntry::initIsSuitableForSnapshot()
 {
    DWORD componentlength, fsflags;
    WCHAR fstype[50];
@@ -178,7 +188,9 @@ bail_out:
    return can_Snapshot;
 }
 
-/* Find a volume for a specific path */
+/* Find a volume for a specific path
+ *  "C:\\" or "c:\\mnt\\" -> \\?\Volume{086fc09e-7781-4076-9b2e-a4ff5c6b52c7}\
+ */
 MTabEntry *MTab::search(char *p)
 {
    wstring volume;
@@ -209,11 +221,38 @@ bool MTab::addInSnapshotSet(char *p)
          elt->setInSnapshotSet();
       }
    }
-   return nb_in_SnapshotSet == entries->size();
+   return nb_in_SnapshotSet == nb_in_SuitableForSnapshot;
+}
+void MTab::dump(const char* file, int lineno, int lvl, const char *prefix)
+{
+   MTabEntry *elt;
+   WCHAR mountPaths[MAX_PATH];
+   if (chk_dbglvl(lvl)) d_msg(file, lineno, lvl, "%s suitable for snapshot %d/%d/%d\n", prefix, nb_in_SnapshotSet, nb_in_SuitableForSnapshot, entries->size());
+   foreach_rblist(elt, entries) {
+      // replace intermediat L'\0' in mountPaths by '+' for display
+      mountPaths[0]=L'\0';
+      if (elt->mountPaths != NULL) {
+         WCHAR *s = elt->mountPaths;
+         WCHAR *d = mountPaths;
+         while (*s != L'\0' && d-mountPaths<(int)sizeof(mountPaths)) {
+            *d++ = *s++;
+            if (*s == L'\0' && s[1] != L'\0') {
+               *d++ = L'+';
+               s++;
+            }
+         }
+         *d = L'\0';
+      }
+      if (chk_dbglvl(lvl)) d_msg(file, lineno, lvl, "%s Vol=%ls dev=%ls mounts=%ls snap=%ls active=%c\n", prefix, elt->volumeName, elt->deviceName, mountPaths, elt->shadowCopyName, elt->in_SnapshotSet?'Y':'N');
+   }
 }
 
-/* Initialize the "entries" list will all existing volumes */
-bool MTab::get()
+/* Load the list volumes that exists on the Windows host
+ * with tuples like :
+ *    VolumeName='\\\\?\\Volume{086fc09e-7781-4076-9b2e-a4ff5c6b52c7}\\'
+ *    DeviceName='\\Device\\HarddiskVolume2'
+ */
+bool MTab::load_volumes()
 {
    DWORD  count                = 0;
    WCHAR  DeviceName[MAX_PATH] = L"";
@@ -268,8 +307,10 @@ bool MTab::get()
       // VolumeName='\\\\?\\Volume{086fc09e-7781-4076-9b2e-a4ff5c6b52c7}\\'
       // DeviceName='\\Device\\HarddiskVolume2'
       MTabEntry *entry = New(MTabEntry(DeviceName, VolumeName));
+      if (entry->initIsSuitableForSnapshot()) {
+         nb_in_SuitableForSnapshot++;
+      }
       entries->insert(entry, volume_cmp);
-
       //
       //  Move on to the next volume.
       Success = FindNextVolumeW(FindHandle, VolumeName, ARRAYSIZE(VolumeName));
index 960776be2fd0d00041c78c5643a0b30b33921be9..75b302d555720ef0948f14fc3cac7743e436d2a4 100644 (file)
  */
 class MTabEntry: public SMARTALLOC {
 public:
-   WCHAR *volumeName;         // Name of the current volume
-   WCHAR *mountPaths;         // List of mount paths
-   WCHAR *deviceName;
-   WCHAR *shadowCopyName;
+   WCHAR *volumeName;       // The uuid of the volume
+                            // "\\?\Volume{086fc09e-7781-4076-9b2e-a4ff5c6b52c7}\"
+   WCHAR *mountPaths;       // list of mount point (if any) separated by zero
+                            // "C:\\\0C:\\mnt\\\0\0"
+   WCHAR *deviceName;       // \\Device\\HarddiskVolume2
+   WCHAR *shadowCopyName;   // \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy216
    bool   in_SnapshotSet;
    bool   can_Snapshot;
    UINT   driveType;
@@ -82,6 +84,9 @@ public:
    /* Return  the drive type (cdrom, fixed, network, ...) */
    UINT getDriveType();
 
+   /* Check if the current volume can be snapshoted and initialize "can_Snapshot" */
+   bool initIsSuitableForSnapshot();
+
    /* Return true if the current volume can be snapshoted (ie not CDROM or fat32) */
    bool isSuitableForSnapshot();
 
@@ -101,7 +106,7 @@ public:
       }
    };
 
-   /* Compute the path list assiciated with the current volume */
+   /* Compute the path list associated with the current volume */
    bool get_paths() {
       DWORD  count = MAX_PATH + 1;
       bool   ret = false;
@@ -157,6 +162,7 @@ public:
    const char *lasterror_str;
    rblist     *entries;         /* MTabEntry */
    int         nb_in_SnapshotSet;
+   int         nb_in_SuitableForSnapshot;
 
    MTab();
    ~MTab(); 
@@ -167,8 +173,12 @@ public:
    /* Try to add a volume to the current snapshotset */
    bool addInSnapshotSet(char *file);
 
-   /* Fill the "entries" list will all detected volumes of the system*/
-   bool get();
+   /* Fill the "entries" list will all detected volumes of the system */
+   bool load_volumes();
+
+   /* Dump for debugging at the given debug level */
+   void dump(const char* file, int lineno, int level, const char* prefix);
+
 };
 
 #endif /* BEE_MTAB_ENTRY_H */
index 5346555cef00787d21fc052ca3385685d64d1b1d..2063a502afac903557859b1e629f28ab1c5acc04 100644 (file)
@@ -287,7 +287,7 @@ bool VSSClientGeneric::Initialize(DWORD dwContext, bool bDuringRestore)
    }
 
    m_VolumeList = New(MTab());  // TODO: See if we do this part only in backup
-   if (!m_VolumeList->get()) {
+   if (!m_VolumeList->load_volumes()) {
       Jmsg(m_jcr, M_ERROR, 0, "Unable to list devices and volumes.\n");
       return false;
    }
@@ -490,7 +490,8 @@ bool VSSClientGeneric::CreateSnapshots(alist *mount_points)
          MTabEntry *elt = (MTabEntry*)m_VolumeList->entries->search(p, volume_cmp);
          ASSERT2(elt, "Should find the volume in the list");
          Jmsg(m_jcr, M_INFO, 0, "    Snapshot mount point: %ls\n", elt->first());
-         Dmsg1(50, "AddToSnapshot OK for Vol: %ls\n", p);
+         Dmsg2(50, "AddToSnapshot OK for %ls vol=%ls\n", elt->first(), p);
+         /* Here the snapshot don't exist yet, elt->shadowCopyName == NULL */
       } else {
          //Dmsg1(50, "AddToSnapshot() failed for Vol: %ls\n", (LPWSTR)volume.c_str());
          Dmsg2(50, "AddToSnapshot() failed for path: %ls hresult=0x%x\n", p, hresult);