cifs: Add support for failover in cifs_mount()
authorPaulo Alcantara <palcantara@suse.de>
Wed, 14 Nov 2018 18:53:52 +0000 (16:53 -0200)
committerSteve French <stfrench@microsoft.com>
Fri, 28 Dec 2018 16:10:29 +0000 (10:10 -0600)
This patch adds support for failover when failing to connect in
cifs_mount().

Signed-off-by: Paulo Alcantara <palcantara@suse.de>
Reviewed-by: Aurelien Aptel <aaptel@suse.com>
Signed-off-by: Steve French <stfrench@microsoft.com>
fs/cifs/cifs_dfs_ref.c
fs/cifs/connect.c
fs/cifs/misc.c

index 8bd6f6b..d9b99ab 100644 (file)
@@ -255,20 +255,30 @@ static struct vfsmount *cifs_dfs_do_refmount(struct dentry *mntpt,
 {
        struct vfsmount *mnt;
        char *mountdata;
-       char *devname = NULL;
+       char *devname;
+
+       /*
+        * Always pass down the DFS full path to smb3_do_mount() so we
+        * can use it later for failover.
+        */
+       devname = kstrndup(fullpath, strlen(fullpath), GFP_KERNEL);
+       if (!devname)
+               return ERR_PTR(-ENOMEM);
+
+       convert_delimiter(devname, '/');
 
        /* strip first '\' from fullpath */
        mountdata = cifs_compose_mount_options(cifs_sb->mountdata,
-                       fullpath + 1, ref, &devname);
-
-       if (IS_ERR(mountdata))
+                                              fullpath + 1, ref, NULL);
+       if (IS_ERR(mountdata)) {
+               kfree(devname);
                return (struct vfsmount *)mountdata;
+       }
 
        mnt = vfs_submount(mntpt, &cifs_fs_type, devname, mountdata);
        kfree(mountdata);
        kfree(devname);
        return mnt;
-
 }
 
 static void dump_referral(const struct dfs_info3_param *ref)
index 182b16e..658a0d1 100644 (file)
@@ -3891,10 +3891,11 @@ static int mount_setup_tlink(struct cifs_sb_info *cifs_sb, struct cifs_ses *ses,
  */
 static char *
 build_unc_path_to_root(const struct smb_vol *vol,
-               const struct cifs_sb_info *cifs_sb)
+                      const struct cifs_sb_info *cifs_sb, bool useppath)
 {
        char *full_path, *pos;
-       unsigned int pplen = vol->prepath ? strlen(vol->prepath) + 1 : 0;
+       unsigned int pplen = useppath && vol->prepath ?
+               strlen(vol->prepath) + 1 : 0;
        unsigned int unc_len = strnlen(vol->UNC, MAX_TREE_SIZE + 1);
 
        full_path = kmalloc(unc_len + pplen + 1, GFP_KERNEL);
@@ -3939,7 +3940,7 @@ expand_dfs_referral(const unsigned int xid, struct cifs_ses *ses,
        if (cifs_sb->mnt_cifs_flags & CIFS_MOUNT_NO_DFS)
                return -EREMOTE;
 
-       full_path = build_unc_path_to_root(volume_info, cifs_sb);
+       full_path = build_unc_path_to_root(volume_info, cifs_sb, true);
        if (IS_ERR(full_path))
                return PTR_ERR(full_path);
 
@@ -3971,6 +3972,143 @@ expand_dfs_referral(const unsigned int xid, struct cifs_ses *ses,
        kfree(full_path);
        return rc;
 }
+
+static inline int get_next_dfs_tgt(const char *path,
+                                  struct dfs_cache_tgt_list *tgt_list,
+                                  struct dfs_cache_tgt_iterator **tgt_it)
+{
+       if (!*tgt_it)
+               *tgt_it = dfs_cache_get_tgt_iterator(tgt_list);
+       else
+               *tgt_it = dfs_cache_get_next_tgt(tgt_list, *tgt_it);
+       return !*tgt_it ? -EHOSTDOWN : 0;
+}
+
+static int update_vol_info(const struct dfs_cache_tgt_iterator *tgt_it,
+                          struct smb_vol *fake_vol, struct smb_vol *vol)
+{
+       const char *tgt = dfs_cache_get_tgt_name(tgt_it);
+       int len = strlen(tgt) + 2;
+       char *new_unc;
+
+       new_unc = kmalloc(len, GFP_KERNEL);
+       if (!new_unc)
+               return -ENOMEM;
+       snprintf(new_unc, len, "\\%s", tgt);
+
+       kfree(vol->UNC);
+       vol->UNC = new_unc;
+
+       if (fake_vol->prepath) {
+               kfree(vol->prepath);
+               vol->prepath = fake_vol->prepath;
+               fake_vol->prepath = NULL;
+       }
+       memcpy(&vol->dstaddr, &fake_vol->dstaddr, sizeof(vol->dstaddr));
+
+       return 0;
+}
+
+static int setup_dfs_tgt_conn(const char *path,
+                             const struct dfs_cache_tgt_iterator *tgt_it,
+                             struct cifs_sb_info *cifs_sb,
+                             struct smb_vol *vol,
+                             unsigned int *xid,
+                             struct TCP_Server_Info **server,
+                             struct cifs_ses **ses,
+                             struct cifs_tcon **tcon)
+{
+       int rc;
+       struct dfs_info3_param ref = {0};
+       char *mdata = NULL, *fake_devname = NULL;
+       struct smb_vol fake_vol = {0};
+
+       cifs_dbg(FYI, "%s: dfs path: %s\n", __func__, path);
+
+       rc = dfs_cache_get_tgt_referral(path, tgt_it, &ref);
+       if (rc)
+               return rc;
+
+       mdata = cifs_compose_mount_options(cifs_sb->mountdata, path, &ref,
+                                          &fake_devname);
+       free_dfs_info_param(&ref);
+
+       if (IS_ERR(mdata)) {
+               rc = PTR_ERR(mdata);
+               mdata = NULL;
+       } else {
+               cifs_dbg(FYI, "%s: fake_devname: %s\n", __func__, fake_devname);
+               rc = cifs_setup_volume_info(&fake_vol, mdata, fake_devname,
+                                           false);
+       }
+       kfree(mdata);
+       kfree(fake_devname);
+
+       if (!rc) {
+               /*
+                * We use a 'fake_vol' here because we need pass it down to the
+                * mount_{get,put} functions to test connection against new DFS
+                * targets.
+                */
+               mount_put_conns(cifs_sb, *xid, *server, *ses, *tcon);
+               rc = mount_get_conns(&fake_vol, cifs_sb, xid, server, ses,
+                                    tcon);
+               if (!rc) {
+                       /*
+                        * We were able to connect to new target server.
+                        * Update current volume info with new target server.
+                        */
+                       rc = update_vol_info(tgt_it, &fake_vol, vol);
+               }
+       }
+       cifs_cleanup_volume_info_contents(&fake_vol);
+       return rc;
+}
+
+static int mount_do_dfs_failover(const char *path,
+                                struct cifs_sb_info *cifs_sb,
+                                struct smb_vol *vol,
+                                struct cifs_ses *root_ses,
+                                unsigned int *xid,
+                                struct TCP_Server_Info **server,
+                                struct cifs_ses **ses,
+                                struct cifs_tcon **tcon)
+{
+       int rc;
+       struct dfs_cache_tgt_list tgt_list;
+       struct dfs_cache_tgt_iterator *tgt_it = NULL;
+
+       if (cifs_sb->mnt_cifs_flags & CIFS_MOUNT_NO_DFS)
+               return -EOPNOTSUPP;
+
+       rc = dfs_cache_noreq_find(path, NULL, &tgt_list);
+       if (rc)
+               return rc;
+
+       for (;;) {
+               /* Get next DFS target server - if any */
+               rc = get_next_dfs_tgt(path, &tgt_list, &tgt_it);
+               if (rc)
+                       break;
+               /* Connect to next DFS target */
+               rc = setup_dfs_tgt_conn(path, tgt_it, cifs_sb, vol, xid, server,
+                                       ses, tcon);
+               if (!rc || rc == -EACCES || rc == -EOPNOTSUPP)
+                       break;
+       }
+       if (!rc) {
+               /*
+                * Update DFS target hint in DFS referral cache with the target
+                * server we successfully reconnected to.
+                */
+               rc = dfs_cache_update_tgthint(*xid, root_ses ? root_ses : *ses,
+                                             cifs_sb->local_nls,
+                                             cifs_remap(cifs_sb), path,
+                                             tgt_it);
+       }
+       dfs_cache_free_tgts(&tgt_list);
+       return rc;
+}
 #endif
 
 static int
@@ -4123,22 +4261,47 @@ int cifs_mount(struct cifs_sb_info *cifs_sb, struct smb_vol *vol)
        int rc = 0;
        unsigned int xid;
        struct cifs_ses *ses;
+       struct cifs_tcon *root_tcon = NULL;
        struct cifs_tcon *tcon = NULL;
        struct TCP_Server_Info *server;
+       char *root_path = NULL, *full_path = NULL;
        char *old_mountdata;
        int count;
 
        rc = mount_get_conns(vol, cifs_sb, &xid, &server, &ses, &tcon);
        if (!rc && tcon) {
-               rc = is_path_remote(cifs_sb, vol, xid, server, tcon);
-               if (!rc)
-                       goto out;
-               if (rc != -EREMOTE)
-                       goto error;
+               /* If not a standalone DFS root, then check if path is remote */
+               rc = dfs_cache_find(xid, ses, cifs_sb->local_nls,
+                                   cifs_remap(cifs_sb), vol->UNC + 1, NULL,
+                                   NULL);
+               if (rc) {
+                       rc = is_path_remote(cifs_sb, vol, xid, server, tcon);
+                       if (!rc)
+                               goto out;
+                       if (rc != -EREMOTE)
+                               goto error;
+               }
        }
-       if ((rc == -EACCES) || (rc == -EOPNOTSUPP) || (ses == NULL) || (server == NULL))
+       /*
+        * If first DFS target server went offline and we failed to connect it,
+        * server and ses pointers are NULL at this point, though we still have
+        * chance to get a cached DFS referral in expand_dfs_referral() and
+        * retry next target available in it.
+        *
+        * If a NULL ses ptr is passed to dfs_cache_find(), a lookup will be
+        * performed against DFS path and *no* requests will be sent to server
+        * for any new DFS referrals. Hence it's safe to skip checking whether
+        * server or ses ptr is NULL.
+        */
+       if (rc == -EACCES || rc == -EOPNOTSUPP)
                goto error;
 
+       root_path = build_unc_path_to_root(vol, cifs_sb, false);
+       if (IS_ERR(root_path)) {
+               rc = PTR_ERR(root_path);
+               root_path = NULL;
+               goto error;
+       }
 
        /*
         * Perform an unconditional check for whether there are DFS
@@ -4163,8 +4326,36 @@ int cifs_mount(struct cifs_sb_info *cifs_sb, struct smb_vol *vol)
        if (rc) {
                if (rc == -EACCES || rc == -EOPNOTSUPP)
                        goto error;
+               /* Perform DFS failover to any other DFS targets */
+               rc = mount_do_dfs_failover(root_path + 1, cifs_sb, vol, NULL,
+                                          &xid, &server, &ses, &tcon);
+               if (rc)
+                       goto error;
        }
 
+       kfree(root_path);
+       root_path = build_unc_path_to_root(vol, cifs_sb, false);
+       if (IS_ERR(root_path)) {
+               rc = PTR_ERR(root_path);
+               root_path = NULL;
+               goto error;
+       }
+       /* Cache out resolved root server */
+       (void)dfs_cache_find(xid, ses, cifs_sb->local_nls, cifs_remap(cifs_sb),
+                            root_path + 1, NULL, NULL);
+       /*
+        * Save root tcon for additional DFS requests to update or create a new
+        * DFS cache entry, or even perform DFS failover.
+        */
+       spin_lock(&cifs_tcp_ses_lock);
+       tcon->tc_count++;
+       tcon->dfs_path = root_path;
+       root_path = NULL;
+       tcon->remap = cifs_remap(cifs_sb);
+       spin_unlock(&cifs_tcp_ses_lock);
+
+       root_tcon = tcon;
+
        for (count = 1; ;) {
                if (!rc && tcon) {
                        rc = is_path_remote(cifs_sb, vol, xid, server, tcon);
@@ -4182,8 +4373,16 @@ int cifs_mount(struct cifs_sb_info *cifs_sb, struct smb_vol *vol)
                        break;
                }
 
+               kfree(full_path);
+               full_path = build_unc_path_to_root(vol, cifs_sb, true);
+               if (IS_ERR(full_path)) {
+                       rc = PTR_ERR(full_path);
+                       full_path = NULL;
+                       break;
+               }
+
                old_mountdata = cifs_sb->mountdata;
-               rc = expand_dfs_referral(xid, tcon->ses, vol, cifs_sb,
+               rc = expand_dfs_referral(xid, root_tcon->ses, vol, cifs_sb,
                                         true);
                if (rc)
                        break;
@@ -4194,11 +4393,18 @@ int cifs_mount(struct cifs_sb_info *cifs_sb, struct smb_vol *vol)
                                             &tcon);
                }
                if (rc) {
+                       if (rc == -EACCES || rc == -EOPNOTSUPP)
+                               break;
+                       /* Perform DFS failover to any other DFS targets */
+                       rc = mount_do_dfs_failover(full_path + 1, cifs_sb, vol,
+                                                  root_tcon->ses, &xid,
+                                                  &server, &ses, &tcon);
                        if (rc == -EACCES || rc == -EOPNOTSUPP || !server ||
                            !ses)
                                goto error;
                }
        }
+       cifs_put_tcon(root_tcon);
 
        if (rc)
                goto error;
@@ -4214,6 +4420,8 @@ out:
        return mount_setup_tlink(cifs_sb, ses, tcon);
 
 error:
+       kfree(full_path);
+       kfree(root_path);
        mount_put_conns(cifs_sb, xid, server, ses, tcon);
        return rc;
 }
index 10ae1a3..7c858d4 100644 (file)
@@ -146,6 +146,9 @@ tconInfoFree(struct cifs_tcon *buf_to_free)
        kfree(buf_to_free->nativeFileSystem);
        kzfree(buf_to_free->password);
        kfree(buf_to_free->crfid.fid);
+#ifdef CONFIG_CIFS_DFS_UPCALL
+       kfree(buf_to_free->dfs_path);
+#endif
        kfree(buf_to_free);
 }