Bläddra i källkod

HPCC-26959 Dfs Esp service - phase 1

Implementation of the DFS esp service, interfaces and access
methods.
Re-implementation of existing DFS client interfaces that
use new DFS meta info service.
Allow remote target planes to be mapped.

Signed-off-by: Jake Smith <jake.smith@lexisnexisrisk.com>
Jake Smith 3 år sedan
förälder
incheckning
2bd08bcd6a
44 ändrade filer med 1856 tillägg och 84 borttagningar
  1. 8 8
      common/workunit/workunit.cpp
  2. 31 25
      dali/base/dadfs.cpp
  3. 4 2
      dali/base/dadfs.hpp
  4. 25 7
      dali/base/dafdesc.cpp
  5. 2 0
      dali/base/dafdesc.hpp
  6. 49 0
      dali/base/dautils.cpp
  7. 2 0
      dali/base/dautils.hpp
  8. 230 1
      dali/daliadmin/daliadmin.cpp
  9. 4 0
      dali/daliadmin/daliadminlib.cmake
  10. 1 0
      esp/applications/eclservices/application.yaml
  11. 1 0
      esp/applications/eclservices/eclservices.yaml
  12. 5 0
      esp/applications/eclservices/ldap_authorization_map.yaml
  13. 2 0
      esp/applications/eclservices/plugins.yaml
  14. 1 0
      esp/applications/eclwatch/application.yaml
  15. 1 0
      esp/applications/eclwatch/eclwatch.yaml
  16. 5 0
      esp/applications/eclwatch/ldap_authorization_map.yaml
  17. 3 1
      esp/applications/eclwatch/plugins.yaml
  18. 1 0
      esp/clients/CMakeLists.txt
  19. 67 0
      esp/clients/ws_dfsclient/CMakeLists.txt
  20. 739 0
      esp/clients/ws_dfsclient/ws_dfsclient.cpp
  21. 56 0
      esp/clients/ws_dfsclient/ws_dfsclient.hpp
  22. 1 0
      esp/scm/smcscm.cmake
  23. 64 0
      esp/scm/ws_dfs.ecm
  24. 1 0
      esp/services/CMakeLists.txt
  25. 83 0
      esp/services/ws_dfsservice/CMakeLists.txt
  26. 65 0
      esp/services/ws_dfsservice/ws_dfsplugin.cpp
  27. 188 0
      esp/services/ws_dfsservice/ws_dfsservice.cpp
  28. 40 0
      esp/services/ws_dfsservice/ws_dfsservice.hpp
  29. 4 0
      helm/hpcc/templates/_helpers.tpl
  30. 1 1
      helm/hpcc/templates/eclagent.yaml
  31. 6 1
      helm/hpcc/templates/esp.yaml
  32. 1 1
      helm/hpcc/templates/localroxie.yaml
  33. 1 1
      helm/hpcc/templates/roxie.yaml
  34. 1 1
      helm/hpcc/templates/thor.yaml
  35. 42 1
      helm/hpcc/values.schema.json
  36. 1 0
      initfiles/componentfiles/configschema/xsd/buildset.xml
  37. 30 0
      initfiles/componentfiles/configxml/@temp/esp_service_WsSMC.xsl
  38. 4 0
      initfiles/componentfiles/configxml/buildsetCC.xml.in
  39. 19 4
      initfiles/etc/DIR_NAME/environment.xml.in
  40. 7 0
      system/jlib/jfile.cpp
  41. 1 0
      system/jlib/jfile.hpp
  42. 13 0
      testing/regress/environment.xml.in
  43. 32 30
      thorlcr/mfilemanager/CMakeLists.txt
  44. 14 0
      thorlcr/mfilemanager/thmfilemanager.cpp

+ 8 - 8
common/workunit/workunit.cpp

@@ -14494,7 +14494,7 @@ std::pair<std::string, unsigned> getExternalService(const char *serviceName)
     StringBuffer output;
     try
     {
-        VStringBuffer getServiceCmd("kubectl get svc --selector=server=%s --output=jsonpath={.items[0].status.loadBalancer.ingress[0].hostname},{.items[0].spec.ports[0].port}", serviceName);
+        VStringBuffer getServiceCmd("kubectl get svc --selector=server=%s --output=jsonpath={.items[0].status.loadBalancer.ingress[0].hostname},{.items[0].status.loadBalancer.ingress[0].ip},{.items[0].spec.ports[0].port}", serviceName);
         runKubectlCommand("get-external-service", getServiceCmd, nullptr, &output);
     }
     catch (IException *e)
@@ -14509,15 +14509,15 @@ std::pair<std::string, unsigned> getExternalService(const char *serviceName)
     fields.appendList(output, ",");
 
     // NB: add even if no result, want non-result to be cached too
-    std::string host;
-    unsigned port = 0;
-    if (fields.ordinality())
+    std::string host, port;
+    if (fields.ordinality() == 3) // hostname,ip,port. NB: hostname may be missing, but still present as a blank field
     {
-        host = fields.item(0);
-        if (fields.ordinality()>1)
-            port = atoi(fields.item(1));
+        host = fields.item(0); // hostname
+        if (0 == host.length())
+            host = fields.item(1); // ip
+        port = fields.item(2);
     }
-    auto servicePair = std::make_pair(host, port);
+    auto servicePair = std::make_pair(host, atoi(port.c_str()));
     externalServiceCache.add(serviceName, servicePair);
     return servicePair;
 }

+ 31 - 25
dali/base/dadfs.cpp

@@ -1079,10 +1079,10 @@ public:
     /* createNew always creates an unnamed unattached distributed file
      * The caller must associated it with a name and credentials when it is attached (attach())
      */
-    IDistributedFile *createNew(IFileDescriptor * fdesc);
+    IDistributedFile *createNew(IFileDescriptor * fdesc, const char *optionalName=nullptr);
     IDistributedFile *createExternal(IFileDescriptor *desc, const char *name);
     IDistributedSuperFile *createSuperFile(const char *logicalname,IUserDescriptor *user,bool interleaved,bool ifdoesnotexist,IDistributedFileTransaction *transaction=NULL);
-    IDistributedSuperFile *createNewSuperFile(IPropertyTree *tree, const char *optionalName=nullptr);
+    IDistributedSuperFile *createNewSuperFile(IPropertyTree *tree, const char *optionalName=nullptr, IArrayOf<IDistributedFile> *subFiles=nullptr);
     void removeSuperFile(const char *_logicalname, bool delSubs, IUserDescriptor *user, IDistributedFileTransaction *transaction);
 
     IDistributedFileIterator *getIterator(const char *wildname, bool includesuper,IUserDescriptor *user,bool isPrivilegedUser);
@@ -1171,6 +1171,7 @@ public:
         return ret;
     }
     virtual bool removePhysicalPartFiles(const char *logicalName, IFileDescriptor *fileDesc, IMultiException *mexcept, unsigned numParallelDeletes=0) override;
+    virtual void setFileAccessed(const char *logicalName, const CDateTime &dt, const INode *foreigndali, unsigned foreigndalitimeout);
 };
 
 
@@ -2799,7 +2800,9 @@ public:
     void resetHistory()
     {
         DistributedFilePropertyLock lock(this);
-        queryAttributes().removeTree(queryHistory());
+        IPropertyTree *history = queryHistory();
+        if (history)
+            queryAttributes().removeTree(history);
     }
     void lockFileAttrLock(CFileAttrLock & attrLock)
     {
@@ -3689,21 +3692,7 @@ public:
         unsigned nc = fdesc->numClusters();
         if (nc) {
             for (unsigned i=0;i<nc;i++) {
-                StringBuffer cname;
-                StringBuffer clabel;
-                IClusterInfo &cluster = *createClusterInfo(
-                                    fdesc->getClusterGroupName(i,cname,NULL).str(),
-                                    fdesc->queryClusterGroup(i),
-                                    fdesc->queryPartDiskMapping(i),
-                                    &queryNamedGroupStore()
-                                    );
-#ifdef EXTRA_LOGGING
-                PROGLOG("setClusters(%d,%s)",i,cname.str());
-#endif
-
-                if (!cluster.queryGroup(&queryNamedGroupStore())) {
-                    IERRLOG("IDistributedFileDescriptor cannot set cluster for %s",logicalName.get());
-                }
+                IClusterInfo &cluster = OLINK(*fdesc->queryClusterNum(i));
                 clusters.append(cluster);
             }
         }
@@ -5564,11 +5553,16 @@ public:
         updateFileAttrs();
     }
 
-    CDistributedSuperFile(CDistributedFileDirectory *_parent, IPropertyTree *_root, const char *optionalName)
+    CDistributedSuperFile(CDistributedFileDirectory *_parent, IPropertyTree *_root, const char *optionalName, IArrayOf<IDistributedFile> *subFiles)
     {
         commonInit(_parent, _root);
         if (optionalName)
             logicalName.set(optionalName);
+        if (subFiles)
+        {
+            ForEachItemIn(i,*subFiles)
+                subfiles.append(OLINK(subFiles->item(i)));
+        }
     }
 
     ~CDistributedSuperFile()
@@ -6594,11 +6588,13 @@ public:
             ForEachItemIn(i,subfiles) {
                 StringArray clusters;
                 IDistributedFile &f=subfiles.item(i);
+                Owned<IFileDescriptor> fdesc = f.getFileDescriptor();
                 unsigned nc = f.numClusters();
                 for(unsigned j=0;j<nc;j++) {
                     f.getClusterName(j,name.clear());
-                    if (clusterscache.find(name.str())==NotFound) {
-                        IClusterInfo &cluster = *createClusterInfo(name.str(),f.queryClusterGroup(j),f.queryPartDiskMapping(j),&queryNamedGroupStore());
+                    if (clusterscache.find(name.str())==NotFound)
+                    {
+                        IClusterInfo &cluster = OLINK(*fdesc->queryClusterNum(j));
                         clusterscache.append(cluster);
                     }
                 }
@@ -8031,9 +8027,12 @@ bool CDistributedFileDirectory::existsPhysical(const char *_logicalname, IUserDe
     return file->existsPhysicalPartFiles(0);
 }
 
-IDistributedFile *CDistributedFileDirectory::createNew(IFileDescriptor *fdesc)
+IDistributedFile *CDistributedFileDirectory::createNew(IFileDescriptor *fdesc, const char *optName)
 {
-    return new CDistributedFile(this, fdesc, NULL, false);
+    CDistributedFile *ret = new CDistributedFile(this, fdesc, NULL, false);
+    if (optName)
+        ret->setLogicalName(optName);
+    return ret;
 }
 
 IDistributedFile *CDistributedFileDirectory::createExternal(IFileDescriptor *fdesc, const char *name)
@@ -8524,9 +8523,9 @@ IDistributedSuperFile *CDistributedFileDirectory::createSuperFile(const char *_l
 }
 
 // MORE: This should be implemented in DFSAccess later on
-IDistributedSuperFile *CDistributedFileDirectory::createNewSuperFile(IPropertyTree *tree, const char *optionalName)
+IDistributedSuperFile *CDistributedFileDirectory::createNewSuperFile(IPropertyTree *tree, const char *optionalName, IArrayOf<IDistributedFile> *subFiles)
 {
-    return new CDistributedSuperFile(this, tree, optionalName);
+    return new CDistributedSuperFile(this, tree, optionalName, subFiles);
 }
 
 // MORE: This should be implemented in DFSAccess later on
@@ -11094,6 +11093,13 @@ void CDistributedFileDirectory::setFileAccessed(CDfsLogicalFileName &dlfn,IUserD
     checkDfsReplyException(mb);
 }
 
+void CDistributedFileDirectory::setFileAccessed(const char *logicalName, const CDateTime &dt, const INode *foreigndali, unsigned foreigndalitimeout)
+{
+    CDfsLogicalFileName dlfn;
+    dlfn.set(logicalName);
+    setFileAccessed(dlfn, nullptr, dt, foreigndali, foreigndalitimeout);
+}
+
 void CDistributedFileDirectory::setFileProtect(CDfsLogicalFileName &dlfn,IUserDescriptor *user, const char *owner, bool set, const INode *foreigndali,unsigned foreigndalitimeout)
 {
     // this accepts either a foreign dali node or a foreign lfn

+ 4 - 2
dali/base/dadfs.hpp

@@ -587,7 +587,7 @@ interface IDistributedFileDirectory: extends IInterface
                                         unsigned timeout=INFINITE
                                     ) = 0;  // links, returns NULL if not found
 
-    virtual IDistributedFile *createNew(IFileDescriptor *desc) = 0;
+    virtual IDistributedFile *createNew(IFileDescriptor *desc, const char *optName=nullptr) = 0;
     virtual IDistributedFile *createExternal(IFileDescriptor *desc, const char *name) = 0;
 
     virtual IDistributedFileIterator *getIterator(const char *wildname, bool includesuper, IUserDescriptor *user,bool isPrivilegedUser) = 0;
@@ -612,7 +612,7 @@ interface IDistributedFileDirectory: extends IInterface
     virtual IFileDescriptor *getFileDescriptor(const char *lname,IUserDescriptor *user,const INode *foreigndali=NULL, unsigned foreigndalitimeout=FOREIGN_DALI_TIMEOUT) =0;
 
     virtual IDistributedSuperFile *createSuperFile(const char *logicalname,IUserDescriptor *user,bool interleaved,bool ifdoesnotexist=false,IDistributedFileTransaction *transaction=NULL) = 0;
-    virtual IDistributedSuperFile *createNewSuperFile(IPropertyTree *tree, const char *optionamName=nullptr) = 0;
+    virtual IDistributedSuperFile *createNewSuperFile(IPropertyTree *tree, const char *optionamName=nullptr, IArrayOf<IDistributedFile> *subFiles=nullptr) = 0;
     virtual IDistributedSuperFile *lookupSuperFile(const char *logicalname,IUserDescriptor *user,
                                                     IDistributedFileTransaction *transaction=NULL, // transaction only used for looking up sub files
                                                     unsigned timeout=INFINITE) = 0;  // NB lookup will also return superfiles
@@ -703,6 +703,8 @@ interface IDistributedFileDirectory: extends IInterface
 
     // useful to clearup after temporary unpublished file.
     virtual bool removePhysicalPartFiles(const char *logicalName, IFileDescriptor *fileDesc, IMultiException *mexcept, unsigned numParallelDeletes=0) = 0;
+
+    virtual void setFileAccessed(const char *logicalName, const CDateTime &dt, const INode *foreigndali=nullptr, unsigned foreigndalitimeout=FOREIGN_DALI_TIMEOUT) = 0;
 };
 
 

+ 25 - 7
dali/base/dafdesc.cpp

@@ -1218,6 +1218,11 @@ class CFileDescriptor:  public CFileDescriptorBase, implements ISuperFileDescrip
         return NULL;
     }
 
+    virtual IClusterInfo *queryClusterNum(unsigned idx) override
+    {
+        return &clusters.item(idx);
+    }
+
     void replaceClusterDir(unsigned partno,unsigned copy, StringBuffer &path)
     {
         // assumes default dir matches one of clusters
@@ -3559,12 +3564,9 @@ private:
 
 
 //MORE: This could be cached
-IStoragePlane * getDataStoragePlane(const char * name, bool required)
+static IStoragePlane * getStoragePlane(const char * name, const std::vector<std::string> &categories, bool required)
 {
-    StringBuffer group;
-    group.append(name).toLowerCase();
-
-    VStringBuffer xpath("storage/planes[@name='%s']", group.str());
+    VStringBuffer xpath("storage/planes[@name='%s']", name);
     Owned<IPropertyTree> match = getGlobalConfigSP()->getPropTree(xpath);
     if (!match)
     {
@@ -3573,12 +3575,28 @@ IStoragePlane * getDataStoragePlane(const char * name, bool required)
         return nullptr;
     }
     const char * category = match->queryProp("@category");
-    if (!streq(category, "data") && !streq(category, "lz"))
+    auto r = std::find(categories.begin(), categories.end(), category);
+    if (r == categories.end())
     {
         if (required)
-            throw makeStringExceptionV(-1, "storage plane '%s' does not store data (category %s)", name, category);
+            throw makeStringExceptionV(-1, "storage plane '%s' does not match request categories (plane category=%s)", name, category);
         return nullptr;
     }
 
     return new CStoragePlaneInfo(match);
 }
+
+IStoragePlane * getDataStoragePlane(const char * name, bool required)
+{
+    StringBuffer group;
+    group.append(name).toLowerCase();
+
+    return getStoragePlane(group, { "data", "lz" }, required);
+}
+
+IStoragePlane * getRemoteStoragePlane(const char * name, bool required)
+{
+    StringBuffer group;
+    group.append(name).toLowerCase();
+    return getStoragePlane(group, { "remote" }, required);
+}

+ 2 - 0
dali/base/dafdesc.hpp

@@ -231,6 +231,7 @@ if endCluster is not called it will assume only one cluster and not replicated
 
     virtual unsigned numClusters() = 0;
     virtual IClusterInfo *queryCluster(const char *clusterName) = 0;
+    virtual IClusterInfo *queryClusterNum(unsigned idx) = 0;
     virtual ClusterPartDiskMapSpec &queryPartDiskMapping(unsigned clusternum) = 0;
     virtual IGroup *queryClusterGroup(unsigned clusternum) = 0;                     // returns group for cluster if known
     virtual void setClusterGroup(unsigned clusternum,IGroup *grp) = 0;              // sets group for cluster
@@ -347,6 +348,7 @@ extern da_decl bool setReplicateDir(const char *name,StringBuffer &out, bool isr
 extern da_decl void initializeStorageGroups(bool createPlanesFromGroups);
 extern da_decl bool getDefaultStoragePlane(StringBuffer &ret);
 extern da_decl IStoragePlane * getDataStoragePlane(const char * name, bool required);
+extern da_decl IStoragePlane * getRemoteStoragePlane(const char * name, bool required);
 
 extern da_decl IFileDescriptor *createFileDescriptor();
 extern da_decl IFileDescriptor *createFileDescriptor(IPropertyTree *attr);      // ownership of attr tree is taken

+ 49 - 0
dali/base/dautils.cpp

@@ -41,6 +41,7 @@
 #define EXTERNAL_SCOPE      "file"
 #define PLANE_SCOPE         "plane"
 #define FOREIGN_SCOPE       "foreign"
+#define REMOTE_SCOPE        "remote"
 #define SELF_SCOPE          "."
 #define SDS_DFS_ROOT        "Files" // followed by scope/name
 #define SDS_RELATIONSHIPS_ROOT  "Files/Relationships"
@@ -208,6 +209,11 @@ public:
                         tmp.append(FOREIGN_SCOPE "::");
                         foreignEp.getUrlStr(tmp).append("::");
                     }
+                    else if (sub.isRemote())
+                    {
+                        tmp.append(REMOTE_SCOPE "::");
+                        foreignEp.getUrlStr(tmp).append("::");
+                    }
                     tmp.append(name);
                     lfnExpanded.append(tmp.str());
                 }
@@ -310,6 +316,11 @@ bool CDfsLogicalFileName::isExternalPlane() const
     return external && startsWithIgnoreCase(lfn, PLANE_SCOPE "::");
 }
 
+bool CDfsLogicalFileName::isRemote() const
+{
+    return external && startsWithIgnoreCase(lfn, REMOTE_SCOPE "::");
+}
+
 bool CDfsLogicalFileName::getExternalPlane(StringBuffer & plane) const
 {
     if (!isExternalPlane())
@@ -323,6 +334,20 @@ bool CDfsLogicalFileName::getExternalPlane(StringBuffer & plane) const
 }
 
 
+bool CDfsLogicalFileName::getRemoteSpec(StringBuffer &remoteSvc, StringBuffer &logicalName) const
+{
+    if (!isRemote())
+        return false;
+
+    const char * start = lfn.str() + strlen(REMOTE_SCOPE "::");
+    const char * end = strstr(start,"::");
+    assertex(end);
+    remoteSvc.append(end-start, start);
+    logicalName.append(end+2);
+    return true;
+}
+
+
 bool CDfsLogicalFileName::isExternalFile() const
 {
     return external && startsWithIgnoreCase(lfn, EXTERNAL_SCOPE "::");
@@ -743,6 +768,30 @@ bool CDfsLogicalFileName::normalizeExternal(const char * name, StringAttr &res,
         return true;
     }
 
+    if (startsWithIgnoreCase(name, REMOTE_SCOPE "::"))
+    {
+        //Syntax plane::<remote>::<path>
+        lfn.clear();
+        StringBuffer str;
+        const char *s=strstr(name,"::");
+        normalizeScope(name, name, s-name, str, strict);    // "remote"
+
+        //find the name of the remote (service)
+        const char *s1 = s+2;
+        const char *ns1 = strstr(s1,"::");
+        if (!ns1)
+            return false;
+
+        StringBuffer remoteSvc;
+        normalizeScope(s1, s1, ns1-s1, remoteSvc, strict);
+
+        str.append("::").append(remoteSvc);
+        str.append(ns1);
+        str.toLowerCase();
+        res.set(str);
+        return true;
+    }
+
     return false;
 }
 

+ 2 - 0
dali/base/dautils.hpp

@@ -94,7 +94,9 @@ public:
     void setExternal(const RemoteFilename &rfn);
     bool isExternal() const { return external; }
     bool isExternalPlane() const;
+    bool isRemote() const;
     bool getExternalPlane(StringBuffer & plane) const;
+    bool getRemoteSpec(StringBuffer &remoteSvc, StringBuffer &logicalName) const;
     bool isExternalFile() const;
     bool getExternalHost(StringBuffer & host) const;
     /*

+ 230 - 1
dali/daliadmin/daliadmin.cpp

@@ -20,6 +20,9 @@
 #include "rmtfile.hpp"
 #include "daadmin.hpp"
 
+#include "ws_dfsclient.hpp"
+
+
 using namespace daadmin;
 
 #define DEFAULT_DALICONNECT_TIMEOUT 5 // seconds
@@ -125,6 +128,8 @@ daliadmin:
   name: daliadmin
 )!!";
 
+static void remoteTest(const char *logicalName, bool withDali);
+
 int main(int argc, const char* argv[])
 {
     int ret = 0;
@@ -219,6 +224,8 @@ int main(int argc, const char* argv[])
                         branchType = DXB_File;
                     translateToXpath(params.item(1), branchType);
                 }
+                else if (strieq(cmd, "remotetest"))
+                    remoteTest(params.item(1), false);
                 else
                 {
                     UERRLOG("Unknown command %s",cmd);
@@ -561,6 +568,8 @@ int main(int argc, const char* argv[])
                         }
                         removeOrphanedGlobalVariables(dryrun, reconstruct);
                     }
+                    else if (strieq(cmd, "remotetest"))
+                        remoteTest(params.item(1), true);
                     else
                         UERRLOG("Unknown command %s",cmd);
                 }
@@ -580,4 +589,224 @@ int main(int argc, const char* argv[])
     fflush(stdout);
     fflush(stderr);
     return ret;
-}
+}
+
+static void testDFSFile(IDistributedFile *legacyDfsFile, const char *logicalName)
+{
+    unsigned numParts = legacyDfsFile->numParts();
+    IDistributedFilePart &part0 = legacyDfsFile->queryPart(0);
+    Owned<IDistributedFilePart> part0b = legacyDfsFile->getPart(0);
+    StringBuffer partName;
+    part0b->getPartName(partName);
+    PROGLOG("partName: %s", partName.str());
+    StringBuffer lfn;
+    legacyDfsFile->getLogicalName(lfn);
+    PROGLOG("lfn: %s", lfn.str());
+    verifyex(streq(logicalName, lfn));
+    const char *lfnb = legacyDfsFile->queryLogicalName();
+    verifyex(streq(logicalName, lfnb));
+    Owned<IDistributedFilePartIterator> iter = legacyDfsFile->getIterator();
+    ForEach(*iter)
+    {
+        IDistributedFilePart &part = iter->query();
+        part.getPartName(partName.clear());
+        PROGLOG("partName: %s", partName.str());
+    }
+    Owned<IFileDescriptor> dfsFileDesc = legacyDfsFile->getFileDescriptor();
+    const char *dir = legacyDfsFile->queryDefaultDir();
+    PROGLOG("dir = %s", dir);
+    const char *mask = legacyDfsFile->queryPartMask();
+    PROGLOG("mask = %s", dir);
+    IPropertyTree &attrs = legacyDfsFile->queryAttributes();
+    legacyDfsFile->lockProperties();
+    legacyDfsFile->unlockProperties();
+    CDateTime dt;
+    legacyDfsFile->getModificationTime(dt);
+    StringBuffer dateString;
+    dt.getString(dateString);
+    PROGLOG("Modification time: %s", dateString.str());
+
+    legacyDfsFile->getAccessedTime(dt);
+    dt.getString(dateString.clear());
+    PROGLOG("Accessed time: %s", dateString.str());
+    unsigned numCopies = legacyDfsFile->numCopies(0);
+    PROGLOG("numCopies: %d", numCopies);
+    bool forcePhysical = false;
+    if (!legacyDfsFile->existsPhysicalPartFiles(0))
+        WARNLOG("Could not find physical part files");
+    else
+        forcePhysical = true;
+    __int64 dfsSz = legacyDfsFile->getFileSize(true, forcePhysical);
+    PROGLOG("dfsSz: %" I64F "d", dfsSz);
+    __int64 diskSz = legacyDfsFile->getDiskSize(true, forcePhysical);
+    PROGLOG("diskSz: %" I64F "d", diskSz);
+    unsigned checkSum;
+    legacyDfsFile->getFileCheckSum(checkSum);
+    PROGLOG("checkSum: %d", checkSum);
+    offset_t base;
+    legacyDfsFile->getPositionPart(0, base);
+    PROGLOG("base: %" I64F "d", base);
+    IDistributedSuperFile *super = legacyDfsFile->querySuperFile();
+    PROGLOG("isSuper: %s", boolToStr(super!=NULL));
+    Owned<IDistributedSuperFileIterator> ownerIter = legacyDfsFile->getOwningSuperFiles();
+    ForEach(*ownerIter)
+    {
+        IDistributedSuperFile &superFile = ownerIter->query();
+        PROGLOG("superName: %s", superFile.queryLogicalName());
+    }
+    bool compressed = legacyDfsFile->isCompressed();
+    PROGLOG("compressed: %s", boolToStr(compressed));
+    StringBuffer clusterName;
+    legacyDfsFile->getClusterName(0, clusterName);
+    PROGLOG("clusterName: %s", clusterName.str());
+    StringArray clusters;
+    unsigned numClusterNames = legacyDfsFile->getClusterNames(clusters);
+    ForEachItemIn(i, clusters)
+    {
+        PROGLOG("clusterName: %s", clusters.item(i));
+    }
+    unsigned numClusters = legacyDfsFile->numClusters();
+    PROGLOG("numClusters: %d", numClusters);
+    unsigned clusterNamePos = legacyDfsFile->findCluster(clusterName);
+    PROGLOG("clusterNamePos: %d", clusterNamePos);
+    ClusterPartDiskMapSpec &mapSpec = legacyDfsFile->queryPartDiskMapping(0);
+    IGroup *group = legacyDfsFile->queryClusterGroup(0);
+    StringBuffer clusterGroupName;
+    legacyDfsFile->getClusterGroupName(0, clusterGroupName);
+    PROGLOG("clusterGroupName: %s", clusterGroupName.str());
+    StringBuffer ecl;
+    legacyDfsFile->getECL(ecl);
+    PROGLOG("ecl: %s", ecl.str());
+    StringBuffer reason;
+    bool canModify = legacyDfsFile->canModify(reason);
+    PROGLOG("canModify: %s", boolToStr(canModify));
+    bool canRemove = legacyDfsFile->canRemove(reason.clear());
+    PROGLOG("canRemove: %s", boolToStr(canRemove));
+    StringBuffer err;
+    bool compat = legacyDfsFile->checkClusterCompatible(*dfsFileDesc, err);
+    PROGLOG("compat: %s", boolToStr(compat));
+    unsigned crc;
+    legacyDfsFile->getFormatCrc(crc);
+    PROGLOG("crc: %d", crc);
+    size32_t rsz;
+    legacyDfsFile->getRecordSize(rsz);
+    PROGLOG("rsz: %d", rsz);
+    MemoryBuffer layout;
+    legacyDfsFile->getRecordLayout(layout, "_rtlType");
+    StringBuffer mapping;
+    legacyDfsFile->getColumnMapping(mapping);
+    PROGLOG("mapping: %s", mapping.str());
+    bool restricted = legacyDfsFile->isRestrictedAccess();
+    PROGLOG("restricted: %s", boolToStr(restricted));
+    unsigned oldTimeout = legacyDfsFile->setDefaultTimeout(3500);
+    PROGLOG("oldTimeout: %d", oldTimeout);
+
+    try
+    {
+        legacyDfsFile->validate();
+    }
+    catch (IException *e)
+    {
+        EXCLOG(e);
+        e->Release();
+    }
+
+    IPropertyTree *history = legacyDfsFile->queryHistory();
+    bool isExternal = legacyDfsFile->isExternal();
+    PROGLOG("isExternal: %s", boolToStr(isExternal));
+    unsigned maxSkew, minSkew, maxSkewPart, minSkewPart;
+    if (legacyDfsFile->getSkewInfo(maxSkew, minSkew, maxSkewPart, minSkewPart, true))
+    {
+        PROGLOG("maxSkew: %d", maxSkew);
+        PROGLOG("minSkew: %d", minSkew);
+        PROGLOG("maxSkewPart: %d", maxSkewPart);
+        PROGLOG("minSkewPart: %d", minSkewPart);
+    }
+    int expire = legacyDfsFile->getExpire();
+    PROGLOG("expire: %d", expire);
+    try
+    {
+        double cost = legacyDfsFile->getCost(clusterName.str());
+        PROGLOG("cost: %f", cost);
+    }
+    catch(IException *e)
+    {
+        EXCLOG(e);
+        e->Release();
+    }
+
+    // test some write methods. NB: at the moment, in common with foreign files, these changes do not get propagaged to dali
+    legacyDfsFile->setModified();
+    dt.adjustTime(30);
+    legacyDfsFile->setAccessedTime(dt);
+    legacyDfsFile->setAccessed();
+    legacyDfsFile->addAttrValue("recordCount", 10);
+    legacyDfsFile->setExpire(10);
+    legacyDfsFile->setECL("1;");
+    legacyDfsFile->resetHistory();
+    legacyDfsFile->setProtect("me", true);
+    legacyDfsFile->setColumnMapping("field1");
+    legacyDfsFile->setRestrictedAccess(true);
+
+    // this is simulating what happens in Thor when the manager serializes parts to workers
+    Owned<IFileDescriptor> fileDesc = legacyDfsFile->getFileDescriptor();
+    MemoryBuffer mb;
+    UnsignedArray parts;
+    parts.append(0);
+    fileDesc->serializeParts(mb, parts);
+
+    // worker side
+    IArrayOf<IPartDescriptor> partDescs;
+    deserializePartFileDescriptors(mb, partDescs);
+    IPartDescriptor &partDesc = partDescs.item(0);
+    RemoteFilename rfn;
+    partDesc.getFilename(0, rfn);
+    StringBuffer path;
+    rfn.getPath(path);
+    Owned<IFile> iFile = createIFile(path);
+    PROGLOG("File exists = %s", boolToStr(iFile->exists()));
+}
+
+static void remoteTest(const char *logicalName, bool withDali)
+{
+    CDfsLogicalFileName dlfn;
+    dlfn.set(logicalName);
+    logicalName = dlfn.get();
+    StringBuffer svc, remoteName;
+    CDfsLogicalFileName dlfn2;
+    if (!dlfn.getRemoteSpec(svc, remoteName))
+        remoteName.clear().append(logicalName);
+    dlfn2.set(remoteName.str());
+    remoteName.clear().append(dlfn2.get());
+
+    PROGLOG("Reading file: %s, remoteName: %s", logicalName, remoteName.str());
+
+    unsigned timeoutSecs = 60;
+    unsigned keepAliveExpiryFrequency = 10;
+    Owned<IUserDescriptor> userDesc = createUserDescriptor();
+    userDesc->set("jsmith", "password");
+
+    Owned<IDistributedFile> legacyDfsFile;
+    if (dlfn.isRemote())
+    {
+        Owned<wsdfs::IDFSFile> dfsFile = wsdfs::lookupDFSFile(logicalName, timeoutSecs, keepAliveExpiryFrequency, userDesc);
+
+        if (dfsFile)
+            legacyDfsFile.setown(createLegacyDFSFile(dfsFile));
+    }
+    else
+    {
+        if (!withDali)
+            throw makeStringExceptionV(0, "remotetest for non-remote files needs Dali.");
+
+        legacyDfsFile.setown(queryDistributedFileDirectory().lookup(dlfn, userDesc, false, false, false, nullptr, false));
+    }
+
+    if (!legacyDfsFile)
+    {
+        PROGLOG("Failed to open file: %s", logicalName);
+        return;
+    }
+
+    testDFSFile(legacyDfsFile, remoteName);
+}

+ 4 - 0
dali/daliadmin/daliadminlib.cmake

@@ -40,6 +40,8 @@ include_directories (
          ${HPCC_SOURCE_DIR}/system/include
          ${HPCC_SOURCE_DIR}/system/jlib
          ${HPCC_SOURCE_DIR}/system/security/shared
+         ${HPCC_SOURCE_DIR}/esp/clients/wsdfuaccess
+         ${HPCC_SOURCE_DIR}/esp/clients/ws_dfsclient
     )
 
 ADD_DEFINITIONS ( -D_USRDLL -DDALIADMIN_API_EXPORTS )
@@ -53,4 +55,6 @@ target_link_libraries ( daliadminlib
          dalibase
          workunit
          dllserver
+         wsdfuaccess
+         ws_dfsclient
     )

+ 1 - 0
esp/applications/eclservices/application.yaml

@@ -5,6 +5,7 @@ application:
    - WsWorkunits
    - WsTopology
    - WsDfu
+   - WsDfs
    - WsDfuXRef
    - WsFileIO
    - WsPackageProcess

+ 1 - 0
esp/applications/eclservices/eclservices.yaml

@@ -36,6 +36,7 @@ eclservices:
         xslt:
         - name: def_file
           ^: "./smc_xslt/def_file.xslt"
+   WsDfs:
    WsDfuXRef:
       ViewTimeout: 1000
       LayoutProgram: dot/dot -Tsvg -Gordering=out

+ 5 - 0
esp/applications/eclservices/ldap_authorization_map.yaml

@@ -105,6 +105,11 @@ ldap:
          -  path: DfuAccess
             resource: DfuAccess
             description: Access to DFU
+      WsDfs:
+         Feature:
+         -  path: DfsAccess
+            resource: DfsAccess
+            description: Access to DFS
       WsDfuXRef:
          Feature:
          -  path: DfuXrefAccess

+ 2 - 0
esp/applications/eclservices/plugins.yaml

@@ -7,6 +7,7 @@ service_plugins:
   ws_access: ws_access
   ws_account: ws_account
   WsDfu: ws_dfu
+  WsDfs: ws_dfsservice
   WsDfuXRef: ws_dfu
   ws_elk: ws_elk
   ws_esdlconfig: ws_esdlconfig
@@ -24,6 +25,7 @@ binding_plugins:
   ws_access: ws_access
   ws_account: ws_account
   WsDfu: ws_dfu
+  WsDfs: ws_dfsservice
   WsDfuXRef: ws_dfu
   ws_elk: ws_elk
   ws_esdlconfig: ws_esdlconfig

+ 1 - 0
esp/applications/eclwatch/application.yaml

@@ -5,6 +5,7 @@ application:
    - WsWorkunits
    - WsTopology
    - WsDfu
+   - WsDfs
    - WsDfuXRef
    - WsFileIO
    - WsPackageProcess

+ 1 - 0
esp/applications/eclwatch/eclwatch.yaml

@@ -34,6 +34,7 @@ eclwatch:
         xslt:
         - name: def_file
           ^: "./smc_xslt/def_file.xslt"
+   WsDfs:
    WsDfuXRef:
       ViewTimeout: 1000
       LayoutProgram: dot/dot -Tsvg -Gordering=out

+ 5 - 0
esp/applications/eclwatch/ldap_authorization_map.yaml

@@ -106,6 +106,11 @@ ldap:
          -  path: DfuAccess
             resource: DfuAccess
             description: Access to DFU
+      WsDfs:
+         Feature:
+         -  path: DfsAccess
+            resource: DfsAccess
+            description: Access to DFS
       WsDfuXRef:
          Feature:
          -  path: DfuXrefAccess

+ 3 - 1
esp/applications/eclwatch/plugins.yaml

@@ -7,6 +7,7 @@ service_plugins:
   ws_access: ws_access
   ws_account: ws_account
   WsDfu: ws_dfu
+  WsDfs: ws_dfsservice
   WsDfuXRef: ws_dfu
   ws_ecl: ws_ecl
   ws_elk: ws_elk
@@ -27,6 +28,7 @@ binding_plugins:
   ws_access: ws_access
   ws_account: ws_account
   WsDfu: ws_dfu
+  WsDfs: ws_dfsservice
   WsDfuXRef: ws_dfu
   ws_ecl: ws_ecl
   ws_elk: ws_elk
@@ -41,4 +43,4 @@ binding_plugins:
   ws_codesign: ws_codesign
   ws_resources: ws_resources
   WSDali: ws_dali
-
+  

+ 1 - 0
esp/clients/CMakeLists.txt

@@ -16,3 +16,4 @@
 add_subdirectory (wsecl)
 add_subdirectory (WUManager)
 add_subdirectory (wsdfuaccess)
+add_subdirectory (ws_dfsclient)

+ 67 - 0
esp/clients/ws_dfsclient/CMakeLists.txt

@@ -0,0 +1,67 @@
+################################################################################
+#    HPCC SYSTEMS software Copyright (C) 2022 HPCC Systems®.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+################################################################################
+
+
+# Component: ws_dfsclient
+
+#####################################################
+# Description:
+# ------------
+#    Cmake Input File for ws_dfsclient
+#####################################################
+
+
+project( ws_dfsclient )
+
+include(${HPCC_SOURCE_DIR}/esp/scm/smcscm.cmake)
+
+set (    SRCS
+         ws_dfsclient.cpp
+         ${ESPSCM_GENERATED_DIR}/ws_dfs_esp.cpp
+    )
+
+include_directories (
+         ${HPCC_SOURCE_DIR}/system/include
+         ${HPCC_SOURCE_DIR}/system/jlib
+         ${HPCC_SOURCE_DIR}/system/mp
+         ${HPCC_SOURCE_DIR}/system/xmllib
+         ${HPCC_SOURCE_DIR}/system/security/shared
+         ${HPCC_SOURCE_DIR}/system/security/securesocket
+         ${HPCC_SOURCE_DIR}/system/security/cryptohelper
+         ${HPCC_SOURCE_DIR}/esp/bindings
+         ${HPCC_SOURCE_DIR}/esp/bindings/SOAP/client
+         ${HPCC_SOURCE_DIR}/esp/bindings/SOAP/xpp
+         ${HPCC_SOURCE_DIR}/esp/platform
+         ${HPCC_SOURCE_DIR}/esp/clients
+         ${HPCC_SOURCE_DIR}/esp/bindings/SOAP/Platform
+         ${HPCC_SOURCE_DIR}/esp/smc/SMCLib
+         ${HPCC_SOURCE_DIR}/common/environment
+         ${HPCC_SOURCE_DIR}/dali/base
+         ${HPCC_SOURCE_DIR}/common/thorhelper
+         ${HPCC_SOURCE_DIR}/common/workunit
+    )
+
+ADD_DEFINITIONS( -D_USRDLL -DWS_DFSCLIENT_EXPORTS )
+
+HPCC_ADD_LIBRARY( ws_dfsclient SHARED ${SRCS} )
+install ( TARGETS ws_dfsclient RUNTIME DESTINATION ${EXEC_DIR} LIBRARY DESTINATION ${LIB_DIR} )
+target_link_libraries ( ws_dfsclient
+         jlib
+         xmllib
+         esphttp
+         dalibase
+         workunit
+    )

+ 739 - 0
esp/clients/ws_dfsclient/ws_dfsclient.cpp

@@ -0,0 +1,739 @@
+/*##############################################################################
+
+    HPCC SYSTEMS software Copyright (C) 2022 HPCC Systems®.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+############################################################################## */
+
+#include <vector>
+
+#include "jliball.hpp"
+#include "jflz.hpp"
+#include "jsecrets.hpp"
+#include "seclib.hpp"
+#include "ws_dfs.hpp"
+#include "workunit.hpp"
+
+#include "eclwatch_errorlist.hpp" // only for ECLWATCH_FILE_NOT_EXIST
+#include "soapmessage.hpp"
+
+#include "dafdesc.hpp"
+#include "dadfs.hpp"
+#include "dautils.hpp"
+#ifndef _CONTAINERIZED
+#include "environment.hpp"
+#endif
+
+#include "ws_dfsclient.hpp"
+
+namespace wsdfs
+{
+
+class CKeepAliveThread : public CSimpleInterface, implements IThreaded
+{
+    CThreaded threaded;
+    unsigned periodMs;
+    Semaphore sem;
+public:
+    CKeepAliveThread(unsigned _periodSecs) : threaded("CKeepAliveThread", this), periodMs(_periodSecs * 1000)
+    {
+        threaded.start();
+    }
+    void stop()
+    {
+        sem.signal();
+    }
+    virtual void threadmain() override
+    {
+        while (true)
+        {
+            if (sem.wait(periodMs))
+                return;
+        }
+    }
+};
+
+
+template <class INTERFACE>
+class CServiceDistributedFileBase : public CSimpleInterfaceOf<INTERFACE>
+{
+protected:
+    Linked<IDFSFile> dfsFile;
+    StringAttr logicalName;
+    Owned<IDistributedFile> legacyDFSFile;
+    Owned<IFileDescriptor> fileDesc;
+
+    class CDistributedSuperFileIterator: public CSimpleInterfaceOf<IDistributedSuperFileIterator>
+    {
+        Linked<IDFSFile> source;
+        Owned<IDistributedSuperFile> cur;
+        std::vector<std::string> owners;
+        unsigned which = 0;
+
+        void setCurrent(unsigned w)
+        {
+            VStringBuffer lfn("~remote::%s::%s", source->queryRemoteName(), owners[w].c_str());
+            Owned<IDFSFile> dfsFile = lookupDFSFile(lfn, source->queryTimeoutSecs(), keepAliveExpiryFrequency, source->queryUserDescriptor());
+            if (!dfsFile)
+                throw makeStringExceptionV(0, "Failed to open superfile %s", lfn.str());
+            if (!dfsFile->numSubFiles())
+                throwUnexpected();
+            Owned<IDistributedFile> legacyDFSFile = createLegacyDFSFile(dfsFile);
+            IDistributedSuperFile *super = legacyDFSFile->querySuperFile();
+            assertex(super);
+            cur.set(super);
+        }
+    public:
+        CDistributedSuperFileIterator(IDFSFile *_source, std::vector<std::string> _owners) : source(_source), owners(_owners)
+        {
+        }
+        virtual bool first() override
+        {
+            if (owners.empty())
+                return false;
+            which = 0;
+            setCurrent(which);
+            return true;
+        }
+        virtual bool next() override
+        {
+            if (which == (owners.size()-1))
+            {
+                cur.clear();
+                return false;
+            }
+            ++which;
+            setCurrent(which);
+            return true;
+        }
+        virtual bool isValid() override
+        {
+            return cur != nullptr;
+        }
+        virtual IDistributedSuperFile &query() override
+        {
+            return *cur;
+        }
+        virtual const char *queryName() override
+        {
+            if (!isValid())
+                return nullptr;
+            return owners[which].c_str();
+        }
+    };
+
+public:
+    CServiceDistributedFileBase(IDFSFile *_dfsFile) : dfsFile(_dfsFile)
+    {
+        logicalName.set(dfsFile->queryFileMeta()->queryProp("@name"));
+    }
+
+    virtual unsigned numParts() override { return legacyDFSFile->numParts(); }
+    virtual IDistributedFilePart &queryPart(unsigned idx) override { return legacyDFSFile->queryPart(idx); }
+    virtual IDistributedFilePart* getPart(unsigned idx) override { return legacyDFSFile->getPart(idx); }
+    virtual StringBuffer &getLogicalName(StringBuffer &name) override { return legacyDFSFile->getLogicalName(name); }
+    virtual const char *queryLogicalName() override { return legacyDFSFile->queryLogicalName(); }
+    virtual IDistributedFilePartIterator *getIterator(IDFPartFilter *filter=NULL) override { return legacyDFSFile->getIterator(filter); }
+    virtual IFileDescriptor *getFileDescriptor(const char *clustername=NULL) override { return fileDesc.getLink(); }
+    virtual const char *queryDefaultDir() override { return legacyDFSFile->queryDefaultDir(); }
+    virtual const char *queryPartMask() override { return legacyDFSFile->queryPartMask(); }
+    virtual IPropertyTree &queryAttributes() override { return legacyDFSFile->queryAttributes(); }
+    virtual bool lockProperties(unsigned timeoutms=INFINITE) override
+    {
+        // TODO: implement. But for now only foreign [read] files are supported, where updates and locking have never been implemented.
+        return true;
+    }
+    virtual void unlockProperties(DFTransactionState state=TAS_NONE) override
+    {
+        // TODO: implement. But for now only foreign [read] files are supported, where updates and locking have never been implemented.
+    }
+    virtual bool getModificationTime(CDateTime &dt) override { return legacyDFSFile->getModificationTime(dt); }
+    virtual bool getAccessedTime(CDateTime &dt) override { return legacyDFSFile->getAccessedTime(dt); }
+    virtual unsigned numCopies(unsigned partno) override { return legacyDFSFile->numCopies(partno); }
+    virtual bool existsPhysicalPartFiles(unsigned short port) override
+    {
+        return legacyDFSFile->existsPhysicalPartFiles(port);
+    }
+    virtual __int64 getFileSize(bool allowphysical, bool forcephysical) override
+    {
+        return legacyDFSFile->getFileSize(allowphysical, forcephysical);
+    }
+    virtual __int64 getDiskSize(bool allowphysical, bool forcephysical) override
+    {
+        return legacyDFSFile->getDiskSize(allowphysical, forcephysical);
+    }
+    virtual bool getFileCheckSum(unsigned &checksum) override { return legacyDFSFile->getFileCheckSum(checksum); }
+    virtual unsigned getPositionPart(offset_t pos,offset_t &base) override { return legacyDFSFile->getPositionPart(pos,base); }
+    virtual IDistributedSuperFile *querySuperFile() override
+    {
+        return nullptr;
+    }
+    virtual IDistributedSuperFileIterator *getOwningSuperFiles(IDistributedFileTransaction *transaction=NULL) override
+    {
+        if (transaction)
+            throwUnexpected();
+        Owned<IPropertyTreeIterator> iter = dfsFile->queryFileMeta()->getElements("SuperFile/SuperOwner");
+        std::vector<std::string> superOwners;
+        StringBuffer pname;
+        ForEach(*iter)
+        {
+            iter->query().getProp("@name",pname.clear());
+            if (pname.length())
+                superOwners.push_back(pname.str());
+        }
+
+        return new CDistributedSuperFileIterator(dfsFile, superOwners);
+    }
+    virtual bool isCompressed(bool *blocked=NULL) override { return legacyDFSFile->isCompressed(blocked); }
+    virtual StringBuffer &getClusterName(unsigned clusternum,StringBuffer &name) override { return legacyDFSFile->getClusterName(clusternum,name); }
+    virtual unsigned getClusterNames(StringArray &clusters) override { return legacyDFSFile->getClusterNames(clusters); }                                                                                      // (use findCluster)
+    virtual unsigned numClusters() override { return legacyDFSFile->numClusters(); }
+    virtual unsigned findCluster(const char *clustername) override { return legacyDFSFile->findCluster(clustername); }
+    virtual ClusterPartDiskMapSpec &queryPartDiskMapping(unsigned clusternum) override { return legacyDFSFile->queryPartDiskMapping(clusternum); }
+    virtual IGroup *queryClusterGroup(unsigned clusternum) override { return legacyDFSFile->queryClusterGroup(clusternum); }
+    virtual StringBuffer &getClusterGroupName(unsigned clusternum, StringBuffer &name) override
+    {
+        return fileDesc->getClusterGroupName(clusternum, name);
+    }
+    virtual StringBuffer &getECL(StringBuffer &buf) override { return legacyDFSFile->getECL(buf); }
+
+    virtual bool canModify(StringBuffer &reason) override
+    {
+        return false;
+    }
+    virtual bool canRemove(StringBuffer &reason,bool ignoresub=false) override
+    {
+        return false;
+    }
+    virtual bool checkClusterCompatible(IFileDescriptor &fdesc, StringBuffer &err) override { return legacyDFSFile->checkClusterCompatible(fdesc,err); }
+
+    virtual bool getFormatCrc(unsigned &crc) override { return legacyDFSFile->getFormatCrc(crc); }
+    virtual bool getRecordSize(size32_t &rsz) override { return legacyDFSFile->getRecordSize(rsz); }
+    virtual bool getRecordLayout(MemoryBuffer &layout, const char *attrname) override { return legacyDFSFile->getRecordLayout(layout,attrname); }
+    virtual StringBuffer &getColumnMapping(StringBuffer &mapping) override { return legacyDFSFile->getColumnMapping(mapping); }
+
+    virtual bool isRestrictedAccess() override { return legacyDFSFile->isRestrictedAccess(); }
+    virtual unsigned setDefaultTimeout(unsigned timems) override { return legacyDFSFile->setDefaultTimeout(timems); }
+
+    virtual void validate() override { legacyDFSFile->validate(); }
+
+    virtual IPropertyTree *queryHistory() const override { return legacyDFSFile->queryHistory(); }
+    virtual bool isExternal() const override { return false; }
+    virtual bool getSkewInfo(unsigned &maxSkew, unsigned &minSkew, unsigned &maxSkewPart, unsigned &minSkewPart, bool calculateIfMissing) override { return legacyDFSFile->getSkewInfo(maxSkew, minSkew, maxSkewPart, minSkewPart, calculateIfMissing); }
+    virtual int getExpire() override { return legacyDFSFile->getExpire(); }
+    virtual double getCost(const char * cluster) override { return legacyDFSFile->getCost(cluster); }
+
+
+
+// setters (change file meta data)
+    virtual void setPreferredClusters(const char *clusters) override { legacyDFSFile->setPreferredClusters(clusters); }
+    virtual void setSingleClusterOnly() override { legacyDFSFile->setSingleClusterOnly(); }
+    virtual void addCluster(const char *clustername,const ClusterPartDiskMapSpec &mspec) override { legacyDFSFile->addCluster(clustername, mspec); }
+    virtual bool removeCluster(const char *clustername) override { return legacyDFSFile->removeCluster(clustername); }
+    virtual void updatePartDiskMapping(const char *clustername,const ClusterPartDiskMapSpec &spec) override { legacyDFSFile->updatePartDiskMapping(clustername, spec); }
+
+/* NB/TBD: these modifications are only effecting this instance, the changes are not propagated to Dali
+ * This is the same behaviour when foreign files are used, but will need addressing in future.
+ */
+    virtual void setModificationTime(const CDateTime &dt) override
+    {
+        legacyDFSFile->setModificationTime(dt);
+    }
+    virtual void setModified() override
+    {
+        legacyDFSFile->setModified();
+    }
+    virtual void setAccessedTime(const CDateTime &dt) override
+    {
+        legacyDFSFile->setAccessedTime(dt);
+    }
+    virtual void setAccessed() override
+    {
+        legacyDFSFile->setAccessed();
+    }
+    virtual void addAttrValue(const char *attr, unsigned __int64 value) override
+    {
+        legacyDFSFile->addAttrValue(attr, value);
+    }
+    virtual void setExpire(int expireDays) override
+    {
+        legacyDFSFile->setExpire(expireDays);
+    }
+    virtual void setECL(const char *ecl) override
+    {
+        legacyDFSFile->setECL(ecl);
+    }
+    virtual void resetHistory() override
+    {
+        legacyDFSFile->resetHistory();
+    }
+    virtual void setProtect(const char *callerid, bool protect=true, unsigned timeoutms=INFINITE) override
+    {
+        legacyDFSFile->setProtect(callerid, protect, timeoutms);
+    }
+    virtual void setColumnMapping(const char *mapping) override
+    {
+        legacyDFSFile->setColumnMapping(mapping);
+    }
+    virtual void setRestrictedAccess(bool restricted) override
+    {
+        legacyDFSFile->setRestrictedAccess(restricted);
+    }
+    virtual bool renamePhysicalPartFiles(const char *newlfn,const char *cluster=NULL,IMultiException *exceptions=NULL,const char *newbasedir=NULL) override
+    {
+        UNIMPLEMENTED_X("CServiceDistributedFileBase::renamePhysicalPartFiles");
+    }
+    virtual void rename(const char *logicalname,IUserDescriptor *user) override
+    {
+        UNIMPLEMENTED_X("CServiceDistributedFileBase::rename");
+    }
+    virtual void attach(const char *logicalname,IUserDescriptor *user) override
+    {
+        UNIMPLEMENTED_X("CServiceDistributedFileBase::rename");
+    }
+    virtual void detach(unsigned timeoutms=INFINITE, ICodeContext *ctx=NULL) override
+    {
+        UNIMPLEMENTED_X("CServiceDistributedFileBase::detach");
+    }
+    virtual void enqueueReplicate() override
+    {
+        UNIMPLEMENTED_X("CServiceDistributedFileBase::enqueueReplicate");
+    }
+};
+
+class CServiceDistributedFile : public CServiceDistributedFileBase<IDistributedFile>
+{
+    typedef CServiceDistributedFileBase<IDistributedFile> PARENT;
+public:
+    CServiceDistributedFile(IDFSFile *_dfsFile) : PARENT(_dfsFile)
+    {
+        IPropertyTree *file = dfsFile->queryFileMeta()->queryPropTree("File");
+        const char *remotePlaneName = file->queryProp("@group");
+        VStringBuffer planeXPath("planes[@name=\"%s\"]", remotePlaneName);
+        IPropertyTree *filePlane = dfsFile->queryCommonMeta()->queryPropTree(planeXPath);
+        assertex(filePlane);
+        const char *remoteName = dfsFile->queryRemoteName(); // NB: null if local
+
+        if (!isEmptyString(remoteName))
+        {
+            // Path translation is necessary, because the local plane will not necessarily have the same
+            // prefix. In particular, both a local and remote plane may want to use the same prefix/mount.
+            // So, the local plane will be defined with a unique prefix locally.
+            // Files backed by URL's or hostGroups will be access directly, are not mounted, and do not require
+            // this translation.
+            const char *filePlanePrefix = filePlane->queryProp("@prefix");
+            if (isAbsolutePath(filePlanePrefix) && !filePlane->hasProp("@hosts")) // otherwise assume url
+            {
+#ifndef _CONTAINERIZED
+                throw makeStringException(0, "Bare metal does not support remote file access to planes without hosts");
+#endif
+                // A external plane within another environment backed by a PVC, will need a pre-existing
+                // corresponding plane and PVC in the local environment.
+                // The local plane will be associated with the remote environment, via a storage/remote mapping.
+
+                Owned<IPropertyTree> remoteStorage = getRemoteStorage(remoteName);
+                if (!remoteStorage)
+                    throw makeStringExceptionV(0, "Remote storage '%s' not found", remoteName);
+                VStringBuffer remotePlaneXPath("planes[@remote='%s']/@local", remotePlaneName);
+                const char *localMappedPlaneName = remoteStorage->queryProp(remotePlaneXPath);
+                if (isEmptyString(localMappedPlaneName))
+                    throw makeStringExceptionV(0, "Remote plane '%s' not found in remote storage definition '%s'", remotePlaneName, remoteName);
+
+                Owned<IStoragePlane> localPlane = getRemoteStoragePlane(localMappedPlaneName, false);
+                if (!localPlane)
+                    throw makeStringExceptionV(0, "Local plane not found, mapped to by remote storage '%s' (%s->%s)", remoteName, remotePlaneName, localMappedPlaneName);
+
+                DBGLOG("Remote logical file '%s' using remote storage '%s', mapping remote plane '%s' to local plane '%s'", logicalName.str(), remoteName, remotePlaneName, localMappedPlaneName);
+
+                StringBuffer filePlanePrefix;
+                filePlane->getProp("@prefix", filePlanePrefix);
+                if (filePlane->hasProp("@subPath"))
+                    filePlanePrefix.append('/').append(filePlane->queryProp("@subPath"));
+
+                // the plane prefix should match the base of file's base directory
+                // Q: what if the plane has been redefined since the files were created?
+
+                VStringBuffer clusterXPath("Cluster[@name=\"%s\"]", remotePlaneName);
+                IPropertyTree *cluster = file->queryPropTree(clusterXPath);
+                assertex(cluster);
+                const char *clusterDir = cluster->queryProp("@defaultBaseDir");
+                assertex(startsWith(clusterDir, filePlanePrefix));
+                clusterDir += filePlanePrefix.length();
+                StringBuffer newPath(localPlane->queryPrefix());
+                if (strlen(clusterDir))
+                    newPath.append(clusterDir); // add remaining tail of path
+                cluster->setProp("@defaultBaseDir", newPath.str());
+
+                const char *dir = file->queryProp("@directory");
+                assertex(startsWith(dir, filePlanePrefix));
+                dir += filePlanePrefix.length();
+                newPath.clear().append(localPlane->queryPrefix());
+                if (strlen(dir))
+                    newPath.append(dir); // add remaining tail of path
+                DBGLOG("Remapping logical file directory to '%s'", newPath.str());
+                file->setProp("@directory", newPath.str());
+            }
+        }
+        fileDesc.setown(deserializeFileDescriptorTree(file));
+        if (fileDesc)
+            fileDesc->setTraceName(logicalName);
+
+        legacyDFSFile.setown(queryDistributedFileDirectory().createNew(fileDesc, logicalName));
+    }
+};
+
+class CServiceSuperDistributedFile : public CServiceDistributedFileBase<IDistributedSuperFile>
+{
+    typedef CServiceDistributedFileBase<IDistributedSuperFile> PARENT;
+    Owned<IDistributedSuperFile> legacyDFSSuperFile;
+
+public:
+    CServiceSuperDistributedFile(IDFSFile *_dfsFile) : PARENT(_dfsFile)
+    {
+        IArrayOf<IDistributedFile> subFiles;
+        unsigned subs = dfsFile->numSubFiles();
+        for (unsigned s=0; s<subs; s++)
+        {
+            Owned<IDFSFile> subFile = dfsFile->getSubFile(s);
+            Owned<IDistributedFile> legacyDFSFile = createLegacyDFSFile(subFile);
+            subFiles.append(*legacyDFSFile.getClear());
+        }
+        legacyDFSSuperFile.setown(queryDistributedFileDirectory().createNewSuperFile(dfsFile->queryFileMeta()->queryPropTree("SuperFile"), logicalName, &subFiles));
+        legacyDFSFile.set(legacyDFSSuperFile);
+        fileDesc.setown(legacyDFSSuperFile->getFileDescriptor());
+    }
+// IDistributedFile overrides
+    virtual IDistributedSuperFile *querySuperFile() override
+    {
+        return this;
+    }
+
+// IDistributedSuperFile overrides
+    virtual IDistributedFile &querySubFile(unsigned idx,bool sub) override
+    {
+        return legacyDFSSuperFile->querySubFile(idx, sub);
+    }
+    virtual IDistributedFile *querySubFileNamed(const char *name, bool sub) override
+    {
+        return legacyDFSSuperFile->querySubFileNamed(name, sub);
+    }
+    virtual IDistributedFile *getSubFile(unsigned idx,bool sub) override
+    {
+        return legacyDFSSuperFile->getSubFile(idx, sub);
+    }
+    virtual unsigned numSubFiles(bool sub) override
+    {
+        return legacyDFSSuperFile->numSubFiles(sub);
+    }
+    virtual bool isInterleaved() override
+    {
+        return legacyDFSSuperFile->isInterleaved();
+    }
+    virtual IDistributedFile *querySubPart(unsigned partidx,unsigned &subfileidx) override
+    {
+        return legacyDFSSuperFile->querySubPart(partidx, subfileidx);
+    }
+    virtual unsigned getPositionPart(offset_t pos, offset_t &base) override
+    {
+        return legacyDFSSuperFile->getPositionPart(pos, base);
+    }
+    virtual IDistributedFileIterator *getSubFileIterator(bool supersub=false) override
+    {
+        return legacyDFSSuperFile->getSubFileIterator(supersub);
+    }
+    virtual void validate() override
+    {
+        if (!legacyDFSSuperFile->existsPhysicalPartFiles(0))
+        {
+            const char * logicalName = queryLogicalName();
+            throw MakeStringException(-1, "Some physical parts do not exists, for logical file : %s",(isEmptyString(logicalName) ? "[unattached]" : logicalName));
+        }
+    }
+
+// IDistributedSuperFile
+    virtual void addSubFile(const char *subfile, bool before=false, const char *other=NULL, bool addcontents=false, IDistributedFileTransaction *transaction=NULL) override
+    {
+        UNIMPLEMENTED_X("CServiceSuperDistributedFile::addSubFile");
+    }
+    virtual bool removeSubFile(const char *subfile, bool remsub, bool remcontents=false, IDistributedFileTransaction *transaction=NULL) override
+    {
+        UNIMPLEMENTED_X("CServiceSuperDistributedFile::removeSubFile");
+    }
+    virtual bool removeOwnedSubFiles(bool remsub, IDistributedFileTransaction *transaction=NULL) override
+    {
+        UNIMPLEMENTED_X("CServiceSuperDistributedFile::removeOwnedSubFiles");
+    }
+    virtual bool swapSuperFile( IDistributedSuperFile *_file, IDistributedFileTransaction *transaction) override
+    {
+        UNIMPLEMENTED_X("CServiceSuperDistributedFile::swapSuperFile");
+    }
+};
+
+static IDFSFile *createDFSFile(IPropertyTree *commonMeta, IPropertyTree *fileMeta, const char *remoteName, unsigned timeoutSecs, IUserDescriptor *userDesc);
+class CDFSFile : public CSimpleInterfaceOf<IDFSFile>
+{
+    Linked<IPropertyTree> commonMeta; // e.g. share info between IFDSFiles, e.g. common plane info between subfiles
+    Linked<IPropertyTree> fileMeta;
+    unsigned __int64 lockId;
+    std::vector<Owned<IDFSFile>> subFiles;
+    StringAttr remoteName;
+    unsigned timeoutSecs;
+    Linked<IUserDescriptor> userDesc;
+
+public:
+    CDFSFile(IPropertyTree *_commonMeta, IPropertyTree *_fileMeta, const char *_remoteName, unsigned _timeoutSecs, IUserDescriptor *_userDesc)
+        : commonMeta(_commonMeta), fileMeta(_fileMeta), remoteName(_remoteName), timeoutSecs(_timeoutSecs), userDesc(_userDesc)
+    {
+        lockId = fileMeta->getPropInt64("@lockId");
+        if (fileMeta->getPropBool("@isSuper"))
+        {
+            Owned<IPropertyTreeIterator> iter = fileMeta->getElements("FileMeta");
+            ForEach(*iter)
+            {
+                IPropertyTree &subMeta = iter->query();
+                subFiles.push_back(createDFSFile(commonMeta, &subMeta, remoteName, timeoutSecs, userDesc));
+            }
+        }
+    }
+    virtual IPropertyTree *queryFileMeta() const override
+    {
+        return fileMeta;
+    }
+    virtual IPropertyTree *queryCommonMeta() const override
+    {
+        return commonMeta;
+    }
+    virtual unsigned __int64 getLockId() const override
+    {
+        return lockId;
+    }
+    virtual unsigned numSubFiles() const override
+    {
+        return (unsigned)subFiles.size();
+    }
+    virtual IDFSFile *getSubFile(unsigned idx) const override
+    {
+        return LINK(subFiles[idx]);
+    }
+    virtual const char *queryRemoteName() const override
+    {
+        return remoteName;
+    }
+    virtual IUserDescriptor *queryUserDescriptor() const override
+    {
+        return userDesc.getLink();
+    }
+    virtual unsigned queryTimeoutSecs() const override
+    {
+        return timeoutSecs;
+    }
+};
+
+static IDFSFile *createDFSFile(IPropertyTree *commonMeta, IPropertyTree *fileMeta, const char *remoteName, unsigned timeoutSecs, IUserDescriptor *userDesc)
+{
+    return new CDFSFile(commonMeta, fileMeta, remoteName, timeoutSecs, userDesc);
+}
+
+IClientWsDfs *getDfsClient(const char *serviceUrl, IUserDescriptor *userDesc)
+{
+    // JCSMORE - can I reuse these, are they thread safe (AFishbeck?)
+
+    VStringBuffer dfsUrl("%s/WsDfs", serviceUrl);
+    Owned<IClientWsDfs> dfsClient = createWsDfsClient();
+    dfsClient->addServiceUrl(dfsUrl);
+    StringBuffer user, token;
+    userDesc->getUserName(user),
+    userDesc->getPassword(token);
+    dfsClient->setUsernameToken(user, token, "");
+    return dfsClient.getClear();
+}
+
+static CriticalSection serviceLeaseMapCS;
+static std::unordered_map<std::string, unsigned __int64> serviceLeaseMap;
+unsigned __int64 ensureClientLease(const char *service, IUserDescriptor *userDesc)
+{
+    CriticalBlock block(serviceLeaseMapCS);
+    auto r = serviceLeaseMap.find(service);
+    if (r != serviceLeaseMap.end())
+        return r->second;
+
+    Owned<IClientWsDfs> dfsClient = getDfsClient(service, userDesc);
+
+    Owned<IClientLeaseResponse> leaseResp;
+
+    unsigned timeoutSecs = 60;
+    CTimeMon tm(timeoutSecs*1000);
+    while (true)
+    {
+        try
+        {
+            Owned<IClientLeaseRequest> leaseReq = dfsClient->createGetLeaseRequest();
+            leaseReq->setKeepAliveExpiryFrequency(keepAliveExpiryFrequency);
+            leaseResp.setown(dfsClient->GetLease(leaseReq));
+
+            unsigned __int64 leaseId = leaseResp->getLeaseId();
+            serviceLeaseMap[service] = leaseId;
+            return leaseId;
+        }
+        catch (IException *e)
+        {
+            /* NB: there should really be a different IException class and a specific error code
+            * The server knows it's an unsupported method.
+            */
+            if (SOAP_SERVER_ERROR != e->errorCode())
+                throw;
+            e->Release();
+        }
+
+        if (tm.timedout())
+            throw makeStringExceptionV(0, "GetLease timed out: timeoutSecs=%u", timeoutSecs);
+        Sleep(5000); // sanity sleep
+    }
+}
+
+
+#ifndef _CONTAINERIZED
+static std::vector<std::string> dfsServiceUrls;
+static CriticalSection dfsServiceUrlCrit;
+static std::atomic<unsigned> currentDfsServiceUrl{0};
+static bool dfsServiceUrlsDiscovered = false;
+#endif
+
+IDFSFile *lookupDFSFile(const char *logicalName, unsigned timeoutSecs, unsigned keepAliveExpiryFrequency, IUserDescriptor *userDesc)
+{
+    CDfsLogicalFileName lfn;
+    lfn.set(logicalName);
+    StringBuffer remoteName, remoteLogicalFileName;
+    StringBuffer serviceUrl;
+    if (lfn.isRemote())
+    {
+        verifyex(lfn.getRemoteSpec(remoteName, remoteLogicalFileName));
+
+        if (!strieq(remoteName, "local")) // "local" is a reserve remote name, used to mean the local environment.
+        {
+            Owned<IPropertyTree> remoteStorage = getRemoteStorage(remoteName.str());
+            if (!remoteStorage)
+                throw makeStringExceptionV(0, "Remote storage '%s' not found", remoteName.str());
+            serviceUrl.set(remoteStorage->queryProp("@service"));
+            logicalName = remoteLogicalFileName;
+        }
+    }
+    if (!serviceUrl.length())
+    {
+#ifdef _CONTAINERIZED
+        // NB: only expected to be here if experimental option #option('dfsesp-localfiles', true); is in use.
+        // This finds and uses local eclwatch service for local read lookukup.
+        Owned<IPropertyTreeIterator> eclWatchServices = getGlobalConfigSP()->getElements("services[@type='eclwatch']");
+        if (!eclWatchServices->first())
+            throw makeStringException(-1, "No eclwatch service not defined");
+        const IPropertyTree &eclWatch = eclWatchServices->query();
+        StringBuffer eclWatchName;
+        eclWatch.getProp("@name", eclWatchName);
+        auto result = getExternalService(eclWatchName);
+        if (result.first.empty())
+            throw makeStringExceptionV(-1, "eclwatch '%s': service not found", eclWatchName.str());
+        if (0 == result.second)
+            throw makeStringExceptionV(-1, "eclwatch '%s': service port not defined", eclWatchName.str());
+        const char *protocol = eclWatch.getPropBool("@tls") ? "https" : "http";
+        serviceUrl.appendf("%s://%s:%u", protocol, result.first.c_str(), result.second);
+#else
+        {
+            CriticalBlock b(dfsServiceUrlCrit);
+            if (!dfsServiceUrlsDiscovered)
+            {
+                dfsServiceUrlsDiscovered = true;
+                getAccessibleServiceURLList("WsSMC", dfsServiceUrls);
+                if (0 == dfsServiceUrls.size())
+                    throw makeStringException(-1, "Could not find any DFS services in the target HPCC configuration.");
+            }
+        }
+        serviceUrl.append(dfsServiceUrls[currentDfsServiceUrl++].c_str());
+        logicalName = remoteLogicalFileName;
+        remoteName.clear(); // local
+#endif
+    }
+
+    DBGLOG("Looking up file '%s' on '%s'", logicalName, serviceUrl.str());
+    Owned<IClientWsDfs> dfsClient = getDfsClient(serviceUrl, userDesc);
+
+    Owned<IClientDFSFileLookupResponse> dfsResp;
+
+    CTimeMon tm(timeoutSecs*1000); // NB: this timeout loop is to cater for *a* esp disappearing.
+    while (true)
+    {
+        try
+        {
+            Owned<IClientDFSFileLookupRequest> dfsReq = dfsClient->createDFSFileLookupRequest();
+
+            dfsReq->setName(logicalName);
+            unsigned remaining;
+            if (tm.timedout(&remaining))
+                break;
+            dfsReq->setRequestTimeout(remaining/1000);
+            unsigned __int64 clientLeaseId = ensureClientLease(serviceUrl, userDesc);
+            dfsReq->setLeaseId(clientLeaseId);
+
+            dfsResp.setown(dfsClient->DFSFileLookup(dfsReq));
+
+            const IMultiException *excep = &dfsResp->getExceptions(); // NB: warning despite getXX name, this does not Link
+            if (excep->ordinality() > 0)
+                throw LINK((IMultiException *)excep); // NB - const IException.. not caught in general..
+
+            const char *base64Resp = dfsResp->getMeta();
+            MemoryBuffer compressedRespMb;
+            JBASE64_Decode(base64Resp, compressedRespMb);
+            MemoryBuffer decompressedRespMb;
+            fastLZDecompressToBuffer(decompressedRespMb, compressedRespMb);
+            Owned<IPropertyTree> meta = createPTree(decompressedRespMb);
+            IPropertyTree *fileMeta = meta->queryPropTree("FileMeta");
+            if (!fileMeta) // file not found
+                return nullptr;
+            // remoteName empty if local
+            return createDFSFile(meta, fileMeta, remoteName.length()?remoteName.str():nullptr, timeoutSecs, userDesc);
+        }
+        catch (IException *e)
+        {
+            /* NB: there should really be a different IException class and a specific error code
+            * The server knows it's an unsupported method.
+            */
+            if (SOAP_SERVER_ERROR != e->errorCode())
+                throw;
+            e->Release();
+        }
+
+        if (tm.timedout())
+            throw makeStringExceptionV(0, "DFSFileLookup timed out: file=%s, timeoutSecs=%u", logicalName, timeoutSecs);
+        Sleep(5000); // sanity sleep
+    }
+    return nullptr; // should never be able to reach here, but keeps compiler happy
+}
+
+IDistributedFile *createLegacyDFSFile(IDFSFile *dfsFile)
+{
+    if (dfsFile->queryFileMeta()->getPropBool("@isSuper"))
+        return new CServiceSuperDistributedFile(dfsFile);
+    else
+        return new CServiceDistributedFile(dfsFile);
+}
+
+IDistributedFile *lookupLegacyDFSFile(const char *logicalName, unsigned timeoutSecs, unsigned keepAliveExpiryFrequency, IUserDescriptor *userDesc)
+{
+    Owned<IDFSFile> dfsFile = lookupDFSFile(logicalName, timeoutSecs, keepAliveExpiryFrequency, userDesc);
+    if (!dfsFile)
+        return nullptr;
+    return createLegacyDFSFile(dfsFile);
+}
+
+
+} // namespace wsdfs
+

+ 56 - 0
esp/clients/ws_dfsclient/ws_dfsclient.hpp

@@ -0,0 +1,56 @@
+/*##############################################################################
+
+    HPCC SYSTEMS software Copyright (C) 2022 HPCC Systems®.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+############################################################################## */
+
+#ifndef _WS_DFSCLIENT_HPP
+#define _WS_DFSCLIENT_HPP
+
+#ifndef WS_DFSCLIENT_API
+
+#ifdef WS_DFSCLIENT_EXPORTS
+#define WS_DFSCLIENT_API DECL_EXPORT
+#else
+#define WS_DFSCLIENT_API DECL_IMPORT
+#endif
+
+#endif
+
+namespace wsdfs
+{
+
+static constexpr unsigned keepAliveExpiryFrequency = 30; // may want to make configurable at some point
+
+interface IDFSFile : extends IInterface
+{
+    virtual IPropertyTree *queryFileMeta() const = 0;
+    virtual IPropertyTree *queryCommonMeta() const = 0;
+    virtual unsigned __int64 getLockId() const = 0;
+    virtual unsigned numSubFiles() const = 0; // >0 implies this is a superfile
+    virtual IDFSFile *getSubFile(unsigned idx) const = 0;
+
+// there are here in case a client wants to use them to lookup a related file (e.g. subfiles of a super)
+    virtual const char *queryRemoteName() const = 0;
+    virtual IUserDescriptor *queryUserDescriptor() const = 0;
+    virtual unsigned queryTimeoutSecs() const = 0;
+};
+
+WS_DFSCLIENT_API IDFSFile *lookupDFSFile(const char *logicalName, unsigned timeoutSecs, unsigned keepAliveExpiryFrequency, IUserDescriptor *userDesc);
+WS_DFSCLIENT_API IDistributedFile *createLegacyDFSFile(IDFSFile *dfsFile);
+WS_DFSCLIENT_API IDistributedFile *lookupLegacyDFSFile(const char *logicalName, unsigned timeoutSecs, unsigned keepAliveExpiryFrequency, IUserDescriptor *userDesc);
+
+} // end of namespace wsdfs
+
+#endif // _WS_DFSCLIENT_HPP

+ 1 - 0
esp/scm/smcscm.cmake

@@ -26,6 +26,7 @@ set ( ESPSCM_GENERATED_DIR ${CMAKE_BINARY_DIR}/generated )
 
 set ( ESPSCM_SRCS
       common.ecm
+      ws_dfs.ecm
       ws_dfu.ecm
       ws_dfu_common.ecm
       ws_dfuXref.ecm

+ 64 - 0
esp/scm/ws_dfs.ecm

@@ -0,0 +1,64 @@
+/*##############################################################################
+
+    HPCC SYSTEMS software Copyright (C) 2022 HPCC Systems®.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+############################################################################## */
+
+
+ESPrequest LeaseRequest
+{
+    int KeepAliveExpiryFrequency(10);   // If client has not 'pinged' the lease in this period (secs), associated locks will expire
+};
+
+ESPresponse [exceptions_inline] LeaseResponse
+{
+    int64 LeaseId;
+};
+
+ESPrequest DFSFileLookupRequest
+{
+    string Name;                        // the logical file name
+    int64 LeaseId;                      // lease to associate any locks to
+    int RequestTimeout(300);            // Max seconds to block waiting (default = 5 mins)
+};
+
+ESPresponse [exceptions_inline] DFSFileLookupResponse
+{
+    string Meta; // json
+};
+
+ESPrequest KeepAliveRequest
+{
+    int64 LeaseId;
+};
+
+ESPresponse [exceptions_inline] KeepAliveResponse
+{
+};
+
+//  ===========================================================================
+ESPservice [
+    auth_feature("DEFERRED"),
+    version("1.01"),
+    default_client_version("1.01"),
+    noforms,
+    exceptions_inline("./smc_xslt/exceptions.xslt")] WsDfs
+{
+    ESPmethod [auth_feature("DfuAccess:READ"), min_ver("1.01")] GetLease(LeaseRequest, LeaseResponse);
+    ESPmethod [auth_feature("DfuAccess:READ"), min_ver("1.01")] KeepAlive(KeepAliveRequest, KeepAliveResponse);
+    ESPmethod [auth_feature("DfuAccess:READ"), min_ver("1.01")] DFSFileLookup(DFSFileLookupRequest, DFSFileLookupResponse);
+};
+
+SCMexportdef(WSDFS);
+SCMapi(WSDFS) IClientWsDfs *createWsDfsClient();

+ 1 - 0
esp/services/CMakeLists.txt

@@ -19,6 +19,7 @@ IF (USE_OPENLDAP)
 ENDIF(USE_OPENLDAP)
 HPCC_ADD_SUBDIRECTORY (ws_account "PLATFORM")
 HPCC_ADD_SUBDIRECTORY (ws_dfu "PLATFORM")
+HPCC_ADD_SUBDIRECTORY (ws_dfsservice "PLATFORM")
 HPCC_ADD_SUBDIRECTORY (ws_ecl "PLATFORM")
 HPCC_ADD_SUBDIRECTORY (ws_fileio "PLATFORM")
 HPCC_ADD_SUBDIRECTORY (ws_fs "PLATFORM")

+ 83 - 0
esp/services/ws_dfsservice/CMakeLists.txt

@@ -0,0 +1,83 @@
+################################################################################
+#    HPCC SYSTEMS software Copyright (C) 2022 HPCC Systems®.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+################################################################################
+
+
+# Component: ws_dfsservice
+#####################################################
+# Description:
+# ------------
+#    Cmake Input File for ws_dfsservice
+#####################################################
+
+project( ws_dfsservice )
+
+include(${HPCC_SOURCE_DIR}/esp/scm/smcscm.cmake)
+
+set (    SRCS
+         ${ESPSCM_GENERATED_DIR}/ws_dfs_esp.cpp
+         ${HPCC_SOURCE_DIR}/esp/scm/ws_dfs.ecm
+         ws_dfsservice.cpp
+         ws_dfsplugin.cpp
+    )
+
+include_directories (
+         ${CMAKE_BINARY_DIR}
+         ${CMAKE_BINARY_DIR}/oss
+         ${HPCC_SOURCE_DIR}/system/jlib
+         ${HPCC_SOURCE_DIR}/system/mp
+         ${HPCC_SOURCE_DIR}/system/xmllib
+         ${HPCC_SOURCE_DIR}/system/security/securesocket
+         ${HPCC_SOURCE_DIR}/system/security/shared
+         ${HPCC_SOURCE_DIR}/system/security/cryptohelper
+         ${HPCC_SOURCE_DIR}/system/include
+         ${HPCC_SOURCE_DIR}/esp/platform
+         ${HPCC_SOURCE_DIR}/esp/services
+         ${HPCC_SOURCE_DIR}/esp/bindings
+         ${HPCC_SOURCE_DIR}/esp/smc/SMCLib
+         ${HPCC_SOURCE_DIR}/esp/bindings/SOAP/xpp
+         ${HPCC_SOURCE_DIR}/esp/clients
+         ${HPCC_SOURCE_DIR}/esp/clients/ws_dfsclient
+         ${HPCC_SOURCE_DIR}/esp/services/common
+         ${HPCC_SOURCE_DIR}/dali/base
+         ${HPCC_SOURCE_DIR}/dali/dfu
+         ${HPCC_SOURCE_DIR}/dali/sasha
+         ${HPCC_SOURCE_DIR}/common/thorhelper
+         ${HPCC_SOURCE_DIR}/common/workunit
+         ${HPCC_SOURCE_DIR}/rtl/include
+         ${HPCC_SOURCE_DIR}/rtl/eclrtl
+    )
+
+ADD_DEFINITIONS( -D_USRDLL -DWS_DFS_EXPORTS )
+
+HPCC_ADD_LIBRARY( ws_dfsservice SHARED ${SRCS} )
+add_dependencies ( ws_dfsservice espscm )
+install ( TARGETS ws_dfsservice RUNTIME DESTINATION ${EXEC_DIR} LIBRARY DESTINATION ${LIB_DIR} )
+target_link_libraries ( ws_dfsservice
+         jlib
+         mp
+         dalibase
+         dllserver
+         workunit
+         xmllib
+         esphttp
+         SMCLib
+    )
+
+IF (USE_OPENSSL)
+    target_link_libraries ( ws_dfsservice
+        securesocket
+    )
+ENDIF()

+ 65 - 0
esp/services/ws_dfsservice/ws_dfsplugin.cpp

@@ -0,0 +1,65 @@
+/*##############################################################################
+
+    HPCC SYSTEMS software Copyright (C) 2022 HPCC Systems®.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+############################################################################## */
+
+#pragma warning (disable : 4786)
+
+#include "ws_dfs_esp.ipp"
+
+//ESP Bindings
+#include "http/platform/httpprot.hpp"
+
+//ESP Service
+#include "ws_dfsservice.hpp"
+
+#include "espplugin.hpp"
+
+extern "C"
+{
+
+//when we aren't loading dynamically
+// Change the function names when we stick with dynamic loading.
+ESP_FACTORY IEspService * esp_service_factory(const char *name, const char* type, IPropertyTree *cfg, const char *process)
+{
+    if (strieq(type, "ws_dfsservice"))
+    {
+        CWsDfsEx* service = new CWsDfsEx;
+        service->init(cfg, process, name);
+        return service;
+    }
+    return nullptr;
+}
+
+ESP_FACTORY IEspRpcBinding* esp_binding_factory(const char* name, const char* type, IPropertyTree* cfg, const char* process)
+{
+    //binding names of the form <servicetype>_http are being added so the names can be made more consistent and can therefore be automatically generated
+    //  the name also better reflects that these bindings are for all HTTP based protocols, not just SOAP
+    //  both "SoapBinding" and "_http" names instantiate the same objects.
+    if (strieq(type, "ws_dfsserviceSoapBinding") || strieq(type, "WsDfs_http"))
+    {
+        return new CWsDfsSoapBinding(cfg, name, process);
+    }
+
+    return nullptr;
+}
+
+
+ESP_FACTORY IEspProtocol * esp_protocol_factory(const char *name, const char* type, IPropertyTree *cfg, const char *process)
+{
+    return http_protocol_factory(name, type, cfg, process);
+}
+
+};

+ 188 - 0
esp/services/ws_dfsservice/ws_dfsservice.cpp

@@ -0,0 +1,188 @@
+/*##############################################################################
+
+    HPCC SYSTEMS software Copyright (C) 2022 HPCC Systems®.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+############################################################################## */
+
+#pragma warning (disable : 4786)
+
+#include "jflz.hpp"
+#include "jstring.hpp"
+
+#include "daaudit.hpp"
+#include "dautils.hpp"
+#include "dadfs.hpp"
+#include "dafdesc.hpp"
+#include "esp.hpp"
+#include "exception_util.hpp"
+#include "package.h"
+
+#include "ws_dfsclient.hpp"
+#include "ws_dfsservice.hpp"
+
+using namespace wsdfs;
+
+// all fake for now
+// this will be implemened in Dali.
+static std::atomic<unsigned __int64> nextLockID{0};
+static unsigned __int64 getLockId(unsigned __int64 leaseId)
+{
+    // associated new locks with lease
+    return ++nextLockID;
+}
+
+static void populateLFNMeta(const char *logicalName, unsigned __int64 leaseId, IPropertyTree *metaRoot, IPropertyTree *meta)
+{
+    Owned<IPropertyTree> tree = queryDistributedFileDirectory().getFileTree(logicalName, nullptr);
+    if (!tree)
+        return;
+    CDfsLogicalFileName lfn;
+    lfn.set(logicalName);
+    if (lfn.isForeign())
+        ThrowStringException(-1, "foreign file %s. Not supported", logicalName);
+
+    assertex(!lfn.isMulti()); // not supported, don't think needs to be/will be.
+
+    bool isSuper = streq(tree->queryName(), queryDfsXmlBranchName(DXB_SuperFile));
+
+    IPropertyTree *fileMeta = meta->addPropTree("FileMeta");
+    fileMeta->setProp("@name", logicalName);
+
+    // 1) establish lock 1st (from Dali)
+    // TBD
+    unsigned __int64 lockId = getLockId(leaseId);
+    fileMeta->setPropInt64("@lockId", lockId);
+
+    if (isSuper)
+    {
+        fileMeta->setPropBool("@isSuper", true);
+        unsigned n = tree->getPropInt("@numsubfiles");
+        if (n)
+        {
+            Owned<IPropertyTreeIterator> subit = tree->getElements("SubFile");
+            // Adding a sub 'before' another get the list out of order (but still valid)
+            OwnedMalloc<IPropertyTree *> orderedSubFiles(n, true);
+            ForEach (*subit)
+            {
+                IPropertyTree &sub = subit->query();
+                unsigned sn = sub.getPropInt("@num",0);
+                if (sn == 0)
+                    ThrowStringException(-1, "CDistributedSuperFile: SuperFile %s: bad subfile part number %d of %d", logicalName, sn, n);
+                if (sn > n)
+                    ThrowStringException(-1, "CDistributedSuperFile: SuperFile %s: out-of-range subfile part number %d of %d", logicalName, sn, n);
+                if (orderedSubFiles[sn-1])
+                    ThrowStringException(-1, "CDistributedSuperFile: SuperFile %s: duplicated subfile part number %d of %d", logicalName, sn, n);
+                orderedSubFiles[sn-1] = &sub;
+            }
+            for (unsigned i=0; i<n; i++)
+            {
+                if (!orderedSubFiles[i])
+                    ThrowStringException(-1, "CDistributedSuperFile: SuperFile %s: missing subfile part number %d of %d", logicalName, i+1, n);
+            }
+            StringBuffer subname;
+            for (unsigned f=0; f<n; f++)
+            {
+                IPropertyTree &sub = *(orderedSubFiles[f]);
+                sub.getProp("@name", subname.clear());
+                populateLFNMeta(subname, leaseId, metaRoot, fileMeta);
+            }
+        }
+    }
+    fileMeta->setPropTree(tree->queryName(), tree.getLink());
+    Owned<IPropertyTreeIterator> clusterIter = tree->getElements("Cluster");
+    ForEach(*clusterIter)
+    {
+        IPropertyTree &cluster = clusterIter->query();
+        const char *planeName = cluster.queryProp("@name");
+        VStringBuffer planeXPath("planes[@name='%s']", planeName);
+        if (!metaRoot->hasProp(planeXPath))
+        {
+            VStringBuffer storagePlaneXPath("storage/%s", planeXPath.str());
+            Owned<IPropertyTree> dataPlane = getGlobalConfigSP()->getPropTree(storagePlaneXPath);
+            metaRoot->addPropTree("planes", dataPlane.getClear());
+        }
+    }
+}
+
+
+void CWsDfsEx::init(IPropertyTree *cfg, const char *process, const char *service)
+{
+    DBGLOG("Initializing %s service [process = %s]", service, process);
+}
+
+bool CWsDfsEx::onGetLease(IEspContext &context, IEspLeaseRequest &req, IEspLeaseResponse &resp)
+{
+    unsigned timeoutSecs = req.getKeepAliveExpiryFrequency();
+
+    // TBD will get from Dali.
+    resp.setLeaseId(1);
+    return true;
+}
+
+bool CWsDfsEx::onKeepAlive(IEspContext &context, IEspKeepAliveRequest &req, IEspKeepAliveResponse &resp)
+{
+    return true;
+}
+
+bool CWsDfsEx::onDFSFileLookup(IEspContext &context, IEspDFSFileLookupRequest &req, IEspDFSFileLookupResponse &resp)
+{
+    try
+    {
+        const char *logicalName = req.getName();
+
+        StringBuffer userID;
+        context.getUserID(userID);
+        Owned<IUserDescriptor> userDesc;
+        if (!userID.isEmpty())
+        {
+            userDesc.setown(createUserDescriptor());
+            userDesc->set(userID.str(), context.queryPassword(), context.querySignature());
+        }
+
+        // LDAP scope check
+        checkLogicalName(logicalName, userDesc, true, false, false, nullptr); // check for read permissions
+
+        unsigned timeoutSecs = req.getRequestTimeout();
+        unsigned __int64 leaseId = req.getLeaseId();
+
+        // populate file meta data and lock id's
+        Owned<IPropertyTree> responseTree = createPTree();
+        populateLFNMeta(logicalName, leaseId, responseTree, responseTree);
+
+        // serialize response
+        MemoryBuffer respMb, compressedRespMb;
+        responseTree->serialize(respMb);
+        fastLZCompressToBuffer(compressedRespMb, respMb.length(), respMb.bytes());
+        StringBuffer respStr;
+        JBASE64_Encode(compressedRespMb.bytes(), compressedRespMb.length(), respStr, false);
+        resp.setMeta(respStr.str());
+
+        if (responseTree->hasProp("FileMeta")) // otherwise = not found
+        {
+            // update file access.
+            //    Really this should be done at end (or at end as well), but this is same as existing DFS lookup.
+            CDateTime dt;
+            dt.setNow();
+            queryDistributedFileDirectory().setFileAccessed(logicalName, dt);
+
+            LOG(MCauditInfo,",FileAccess,EspProcess,READ,%s,%u,%s", logicalName, timeoutSecs, userID.str());
+        }
+    }
+    catch (IException *e)
+    {
+        FORWARDEXCEPTION(context, e,  ECLWATCH_INTERNAL_ERROR);
+    }
+    return true;
+}
+

+ 40 - 0
esp/services/ws_dfsservice/ws_dfsservice.hpp

@@ -0,0 +1,40 @@
+/*##############################################################################
+
+    HPCC SYSTEMS software Copyright (C) 2022 HPCC Systems®.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+############################################################################## */
+
+#ifndef _WS_DFSSERVICE_HPP__
+#define _WS_DFSSERVICE_HPP__
+
+#include "ws_dfs_esp.ipp"
+
+#include "dadfs.hpp"
+#include <atomic>
+
+
+class CWsDfsEx : public CWsDfs
+{
+public:
+    virtual ~CWsDfsEx() {}
+    virtual void init(IPropertyTree *cfg, const char *process, const char *service);
+    virtual bool onGetLease(IEspContext &context, IEspLeaseRequest &req, IEspLeaseResponse &resp);
+    virtual bool onKeepAlive(IEspContext &context, IEspKeepAliveRequest &req, IEspKeepAliveResponse &resp);
+    virtual bool onDFSFileLookup(IEspContext &context, IEspDFSFileLookupRequest &req, IEspDFSFileLookupResponse &resp);
+};
+
+
+
+#endif //_WS_DFSSERVICE_HPP__
+

+ 4 - 0
helm/hpcc/templates/_helpers.tpl

@@ -202,6 +202,10 @@ storage:
   hostGroups:
 {{ toYaml $storage.hostGroups | indent 2 }}
 {{- end }}
+{{- if hasKey $storage "remote" }}
+  remote:
+{{ toYaml $storage.remote | indent 2 }}
+{{- end }}
   dataPlane: {{ include "hpcc.getDefaultDataPlane" . }}
   planes:
 {{- /*Generate entries for each data plane (removing the pvc).  Exclude the planes used for dlls and dali.*/ -}}

+ 1 - 1
helm/hpcc/templates/eclagent.yaml

@@ -112,7 +112,7 @@ data:
 {{- $env := concat ($.Values.global.env | default list) (.env | default list) -}}
 {{- $apptype := .type | default "hthor" -}}
 {{- $secretsCategories := list "system" "ecl-user" "ecl" "storage" }}
-{{- $commonCtx := dict "root" $ "me" . "secretsCategories" $secretsCategories "includeCategories" (list "lz" "data" "spill" "dll") "env" $env }}
+{{- $commonCtx := dict "root" $ "me" . "secretsCategories" $secretsCategories "includeCategories" (list "lz" "data" "remote" "spill" "dll") "env" $env }}
 {{- $configSHA := include "hpcc.getConfigSHA" ($commonCtx | merge (dict "configMapHelper" "hpcc.agentConfigMap" "component" "eclagent" "excludeKeys" (print "global," $apptype ".replicas"))) }}
 {{- include "hpcc.checkDefaultStoragePlane" $commonCtx }}
 apiVersion: apps/v1

+ 6 - 1
helm/hpcc/templates/esp.yaml

@@ -165,7 +165,12 @@ kind: ConfigMap
 ---
 {{- if hasKey . "service" -}}
 {{- if .service.servicePort -}}
-{{ include "hpcc.addService" ( dict "root" $ "name" .name "service" .service "selector" .name "defaultPort" 8880 ) }}
+{{- $service := deepCopy .service -}}
+{{- if not (hasKey $service "labels") -}}
+ {{- $_ := set $service "labels" dict -}}
+{{- end -}}
+{{- $_ := set $service "labels" (merge $service.labels (dict "server" $application)) -}}
+{{ include "hpcc.addService" ( dict "root" $ "name" .name "service" $service "selector" .name "defaultPort" 8880 ) }}
 ---
 {{- end }}
 {{- end }}

+ 1 - 1
helm/hpcc/templates/localroxie.yaml

@@ -44,7 +44,7 @@ data:
 {{- if not $roxie.disabled  -}}
 {{- $env := concat ($.Values.global.env | default list) (.env | default list) -}}
 {{- $secretsCategories := list "system" "ecl-user" "ecl" "storage" }}
-{{- $commonCtx := dict "root" $ "me" $roxie "includeCategories" (list "lz" "data" "spill" "dll") "secretsCategories" $secretsCategories "env" $env }}
+{{- $commonCtx := dict "root" $ "me" $roxie "includeCategories" (list "lz" "data" "remote" "spill" "dll") "secretsCategories" $secretsCategories "env" $env }}
 {{- $configSHA := include "hpcc.getConfigSHA" ($commonCtx | merge (dict "configMapHelper" "hpcc.localroxieConfigMap" "component" "roxie" "excludeKeys" "global")) }}
 {{- include "hpcc.checkDefaultStoragePlane" $commonCtx }}
 {{- if $roxie.localAgent -}}

+ 1 - 1
helm/hpcc/templates/roxie.yaml

@@ -82,7 +82,7 @@ data:
 {{- $env := concat ($.Values.global.env | default list) (.env | default list) -}}
 {{- $secretsCategories := list "system" "ecl-user" "ecl" "storage" }}
 {{- $toposerver := ($roxie.topoServer | default dict) -}}
-{{- $commonCtx := dict "root" $ "me" $roxie "includeCategories" (list "lz" "data" "spill" "dll") "secretsCategories" $secretsCategories "toposerver" $toposerver "env" $env }}
+{{- $commonCtx := dict "root" $ "me" $roxie "includeCategories" (list "lz" "data" "remote" "spill" "dll") "secretsCategories" $secretsCategories "toposerver" $toposerver "env" $env }}
 {{- $_ := set $commonCtx "toponame" (printf "%s-toposerver" $roxie.name) -}}
 {{- $_ := set $commonCtx "numChannels" ($roxie.numChannels | int | default 1) -}}
 {{- $_ := set $commonCtx "topoport" ($toposerver.port | int | default 9004) -}}

+ 1 - 1
helm/hpcc/templates/thor.yaml

@@ -285,7 +285,7 @@ data:
 {{- if not .disabled -}}
 {{- $env := concat ($.Values.global.env | default list) (.env | default list) -}}
 {{- $secretsCategories := list "system" "ecl-user" "ecl" "storage" }}
-{{- $commonCtx := dict "root" $ "me" . "includeCategories" (list "lz" "data" "spill" "dll") "secretsCategories" $secretsCategories "env" $env -}}
+{{- $commonCtx := dict "root" $ "me" . "includeCategories" (list "lz" "data" "remote" "spill" "dll") "secretsCategories" $secretsCategories "env" $env -}}
 {{- $_ := set $commonCtx "eclAgentName" (printf "%s-eclagent" .name) -}}
 {{- $_ := set $commonCtx "thorAgentName" (printf "%s-thoragent" .name) -}}
 {{- $_ := set $commonCtx "eclAgentUseChildProcesses" (hasKey . "eclAgentUseChildProcesses" | ternary .eclAgentUseChildProcesses true) }}

+ 42 - 1
helm/hpcc/values.schema.json

@@ -31,6 +31,9 @@
         },
         "planes": {
           "$ref": "#/definitions/storagePlanes"
+        },
+        "remote": {
+          "$ref": "#/definitions/remoteStorage"
         }
       },
       "additionalProperties": false
@@ -467,7 +470,7 @@
         "category": {
           "description": "the category this plane is usd for, e.g. lz, data",
           "type": "string",
-          "enum": ["data", "lz", "dali", "sasha", "dll", "spill", "temp", "git" ]
+          "enum": ["data", "lz", "dali", "sasha", "dll", "spill", "temp", "git", "remote" ]
         },
         "umask" : {
           "description": "file creation mask (used by despray)",
@@ -508,6 +511,44 @@
       "required": [ "name", "prefix", "category" ],
       "additionalProperties": false
     },
+    "remoteStorage": {
+      "description": "remote storage definitions",
+      "type": "array",
+      "items": { "$ref": "#/definitions/remoteStorageEntry" }
+    },
+    "remoteStorageEntry": {
+      "description": "information about an individual remote storage definition",
+      "type": "object",
+      "properties": {
+        "name": {
+          "description": "the name of the remote storage definition",
+          "type": "string"
+        },
+        "service": {
+          "description": "the remote DFS service",
+          "type": "string"
+        },
+        "planes": {
+          "description": "mapping of remote planes to local planes",
+          "type": "array",
+          "items": {
+            "type": "object",
+            "properties": {
+              "remote": {
+                "type": "string",
+                "description": "The name of the remote plane to map from"
+              },
+              "local": {
+                "type": "string",
+                "description": "The name of the local plane to map to"
+              }
+            }
+          }
+        }
+      },
+      "required": [ "name", "service", "planes" ],
+      "additionalProperties": false
+    },
     "resources": {
       "type": "object"
     },

+ 1 - 0
initfiles/componentfiles/configschema/xsd/buildset.xml

@@ -75,6 +75,7 @@
           <AuthenticateFeature description="Access to WS Decoupled Log service" path="WsDecoupledLogAccess" resource="WsDecoupledLogAccess" service="ws_decoupledlogging"/>
           <AuthenticateFeature description="Access to sign ECL code" path="CodeSignAccess" resource="CodeSignAccess" service="ws_codesign"/>
           <AuthenticateFeature description="Access to LogAccess service" path="WsLogAccess" resource="WsLogAccess" service="ws_logaccess"/>
+          <AuthenticateFeature description="Access to DFS service" path="WsDfsAccess" resource="WsDfsAccess" service="ws_dfsservice"/>
           <ProcessFilters>
             <Platform name="Windows">
               <ProcessFilter name="any">

+ 30 - 0
initfiles/componentfiles/configxml/@temp/esp_service_WsSMC.xsl

@@ -141,6 +141,10 @@ This is required by its binding with ESP service '<xsl:value-of select="$espServ
             <xsl:with-param name="bindingNode" select="$bindingNode"/>
             <xsl:with-param name="authNode" select="$authNode"/>
         </xsl:apply-templates>
+        <xsl:apply-templates select="." mode="ws_dfsservice">
+            <xsl:with-param name="bindingNode" select="$bindingNode"/>
+            <xsl:with-param name="authNode" select="$authNode"/>
+        </xsl:apply-templates>
     </xsl:template>
 
     <!-- WS-SMC -->
@@ -768,6 +772,32 @@ This is required by its binding with ESP service '<xsl:value-of select="$espServ
       </xsl:copy>
    </xsl:template>
 
+    <!-- ws_dfsservice -->
+    <xsl:template match="EspService" mode="ws_dfsservice">
+        <xsl:param name="bindingNode"/>
+        <xsl:param name="authNode"/>
+
+        <xsl:variable name="serviceType" select="'ws_dfsservice'"/>
+        <xsl:variable name="serviceName" select="concat($serviceType, '_', @name, '_', $process)"/>
+        <xsl:variable name="bindName" select="concat($serviceType, '_', $bindingNode/@name, '_', $process)"/>
+        <xsl:variable name="bindType" select="'ws_dfsserviceSoapBinding'"/>
+        <xsl:variable name="servicePlugin">
+            <xsl:call-template name="defineServicePlugin">
+                <xsl:with-param name="plugin" select="'ws_dfsservice'"/>
+            </xsl:call-template>
+        </xsl:variable>
+        <EspService name="{$serviceName}" type="{$serviceType}" plugin="{$servicePlugin}">
+        </EspService>
+        <EspBinding name="{$bindName}" service="{$serviceName}" protocol="{$bindingNode/@protocol}" type="{$bindType}"
+            plugin="{$servicePlugin}" netAddress="0.0.0.0" port="{$bindingNode/@port}">
+            <xsl:call-template name="bindAuthentication">
+                <xsl:with-param name="bindingNode" select="$bindingNode"/>
+                <xsl:with-param name="authMethod" select="$authNode/@method"/>
+                <xsl:with-param name="service" select="'ws_dfsservice'"/>
+            </xsl:call-template>
+        </EspBinding>
+    </xsl:template>
+
     <!-- ws_logAccess -->
     <xsl:template match="EspService" mode="ws_logaccess">
         <xsl:param name="bindingNode"/>

+ 4 - 0
initfiles/componentfiles/configxml/buildsetCC.xml.in

@@ -229,6 +229,10 @@
                         path="WsLogAccess"
                         resource="WsLogAccess"
                         service="ws_logaccess"/>
+                    <AuthenticateFeature description="Access to DFS service"
+                        path="WsDfsAccess"
+                        resource="WsDfsAccess"
+                        service="ws_dfsservice"/>
                     <ProcessFilters>
                         <Platform name="Windows">
                             <ProcessFilter name="any">

+ 19 - 4
initfiles/etc/DIR_NAME/environment.xml.in

@@ -433,6 +433,11 @@
                          path="WsLogAccess"
                          resource="WsLogAccess"
                          service="ws_logaccess"/>
+    <AuthenticateFeature authenticate="Yes"
+                         description="Access to DFS service"
+                         path="WsDfsAccess"
+                         resource="WsDfsAccess"
+                         service="ws_dfsservice"/>
     <Authenticate access="Read"
                   description="Root access to SMC service"
                   path="/"
@@ -778,6 +783,11 @@
                          path="WsLogAccess"
                          resource="WsLogAccess"
                          service="ws_logaccess"/>
+    <AuthenticateFeature authenticate="Yes"
+                         description="Access to DFS service"
+                         path="WsDfsAccess"
+                         resource="WsDfsAccess"
+                         service="ws_dfsservice"/>
     <ProcessFilters>
      <Platform name="Windows">
       <ProcessFilter name="any">
@@ -1175,10 +1185,15 @@
                           resource="WsDecoupledLogAccess"
                           service="ws_decoupledlogging"/>
      <AuthenticateFeature authenticate="Yes"
-                         description="Access to Log Access service"
-                         path="WsLogAccess"
-                         resource="WsLogAccess"
-                         service="ws_logaccess"/>
+                          description="Access to Log Access service"
+                          path="WsLogAccess"
+                          resource="WsLogAccess"
+                          service="ws_logaccess"/>
+     <AuthenticateFeature authenticate="Yes"
+                          description="Access to DFS service"
+                          path="WsDfsAccess"
+                          resource="WsDfsAccess"
+                          service="ws_dfsservice"/>
      <ProcessFilters>
       <Platform name="Windows">
        <ProcessFilter name="any">

+ 7 - 0
system/jlib/jfile.cpp

@@ -7428,3 +7428,10 @@ IPropertyTree * getStoragePlane(const char * name)
     Owned<IPropertyTree> global = getGlobalConfig();
     return global->getPropTree(xpath);
 }
+
+IPropertyTree * getRemoteStorage(const char * name)
+{
+    VStringBuffer xpath("storage/remote[@name='%s']", name);
+    Owned<IPropertyTree> global = getGlobalConfig();
+    return global->getPropTree(xpath);
+}

+ 1 - 0
system/jlib/jfile.hpp

@@ -747,5 +747,6 @@ jlib_decl IFileEventWatcher *createFileEventWatcher(FileWatchFunc callback);
 
 extern jlib_decl IPropertyTree * getHostGroup(const char * name, bool required);
 extern jlib_decl IPropertyTree * getStoragePlane(const char * name);
+extern jlib_decl IPropertyTree * getRemoteStorage(const char * name);
 
 #endif

+ 13 - 0
testing/regress/environment.xml.in

@@ -403,6 +403,11 @@
                          path="CodeSignAccess"
                          resource="CodeSignAccess"
                          service="ws_codesign"/>
+    <AuthenticateFeature authenticate="Yes"
+                         description="Access to DFS service"
+                         path="WsDfsAccess"
+                         resource="WsDfsAccess"
+                         service="ws_dfsservice"/>
     <Authenticate access="Read"
                   description="Root access to SMC service"
                   path="/"
@@ -647,6 +652,10 @@
                          path="CodeSignAccess"
                          resource="CodeSignAccess"
                          service="ws_codesign"/>
+    <AuthenticateFeature description="Access to DFS service"
+                         path="WsDfsAccess"
+                         resource="WsDfsAccess"
+                         service="ws_dfsservice"/>
     <ProcessFilters>
      <Platform name="Windows">
       <ProcessFilter name="any">
@@ -946,6 +955,10 @@
                           path="CodeSignAccess"
                           resource="CodeSignAccess"
                           service="ws_codesign"/>
+    <AuthenticateFeature  description="Access to DFS service"
+                          path="WsDfsAccess"
+                          resource="WsDfsAccess"
+                          service="ws_dfsservice"/>
      <ProcessFilters>
       <Platform name="Windows">
        <ProcessFilter name="any">

+ 32 - 30
thorlcr/mfilemanager/CMakeLists.txt

@@ -29,27 +29,28 @@ set (    SRCS
     )
 
 include_directories ( 
-         ./../thorutil 
-         ./../../common/remote 
-         ./../../system/mp 
-         ./../master 
-         ./../../common/workunit 
-         ./../shared 
-         ./../graph 
-         ./../../common/environment 
-         ./../../dali/ft 
-         ./../../common/deftype 
-         ./../../system/include 
-         ./../../dali/base 
-         ./../../rtl/include 
-         ./../../rtl/eclrtl 
-         ./../../common/dllserver 
-         ./../../system/jlib 
-         ./../thorcodectx 
-         ./../mfilemanager 
-         ./../../common/thorhelper 
-         ./../../roxie/roxiemem
-         ./../../system/security/shared
+         ${HPCC_SOURCE_DIR}/common/deftype
+         ${HPCC_SOURCE_DIR}/common/dllserver
+         ${HPCC_SOURCE_DIR}/common/environment
+         ${HPCC_SOURCE_DIR}/common/remote
+         ${HPCC_SOURCE_DIR}/common/thorhelper
+         ${HPCC_SOURCE_DIR}/common/workunit
+         ${HPCC_SOURCE_DIR}/dali/base
+         ${HPCC_SOURCE_DIR}/dali/ft
+         ${HPCC_SOURCE_DIR}/esp/clients/ws_dfsclient
+         ${HPCC_SOURCE_DIR}/roxie/roxiemem
+         ${HPCC_SOURCE_DIR}/rtl/eclrtl
+         ${HPCC_SOURCE_DIR}/rtl/include
+         ${HPCC_SOURCE_DIR}/system/include
+         ${HPCC_SOURCE_DIR}/system/jlib
+         ${HPCC_SOURCE_DIR}/system/mp
+         ${HPCC_SOURCE_DIR}/system/security/shared
+         ${HPCC_SOURCE_DIR}/thorlcr/graph
+         ${HPCC_SOURCE_DIR}/thorlcr/master
+         ${HPCC_SOURCE_DIR}/thorlcr/mfilemanager
+         ${HPCC_SOURCE_DIR}/thorlcr/thorutil
+         ${HPCC_SOURCE_DIR}/thorlcr/shared
+         ${HPCC_SOURCE_DIR}/thorlcr/thorcodectx
     )
 
 ADD_DEFINITIONS( -D_USRDLL -DMFILEMANAGER_EXPORTS )
@@ -57,16 +58,17 @@ ADD_DEFINITIONS( -D_USRDLL -DMFILEMANAGER_EXPORTS )
 HPCC_ADD_LIBRARY( mfilemanager_lcr SHARED ${SRCS} )
 install ( TARGETS mfilemanager_lcr RUNTIME DESTINATION ${EXEC_DIR} LIBRARY DESTINATION ${LIB_DIR} )
 target_link_libraries ( mfilemanager_lcr
+         dalibase
+         deftype
+         dllserver
+         eclrtl
+         graph_lcr
+         jhtree
          jlib
-         remote 
-         dalibase 
-         dllserver 
-         nbcd 
-         eclrtl 
-         deftype 
-         workunit 
-         jhtree 
-         graph_lcr 
+         nbcd
+         remote
+         workunit
+         ws_dfsclient
     )
 
 if (NOT CONTAINERIZED)

+ 14 - 0
thorlcr/mfilemanager/thmfilemanager.cpp

@@ -38,6 +38,8 @@
 
 #include "workunit.hpp"
 
+#include "ws_dfsclient.hpp"
+
 #define CHECKPOINTSCOPE "checkpoints"
 #define TMPSCOPE "temporary"
 
@@ -312,6 +314,18 @@ public:
     IDistributedFile *timedLookup(CJobBase &job, CDfsLogicalFileName &lfn, bool write, bool privilegedUser=false, unsigned timeout=INFINITE)
     {
         VStringBuffer blockedMsg("lock file '%s' for %s access", lfn.get(), write ? "WRITE" : "READ");
+        if (!write)
+        {
+            if (lfn.isRemote() || (!lfn.isExternal() && job.getOptBool("dfsesp-localfiles")))
+            {
+                auto func = [&job, &lfn, write, privilegedUser](unsigned timeout)
+                {
+                    return wsdfs::lookupLegacyDFSFile(lfn.get(), timeout, wsdfs::keepAliveExpiryFrequency, job.queryUserDescriptor());
+                };
+                return blockReportFunc<IDistributedFile *>(job, func, timeout, blockedMsg);
+            }
+        }
+        // NB: if we're here, we're not using DFSESP
         auto func = [&job, &lfn, write, privilegedUser](unsigned timeout) { return queryDistributedFileDirectory().lookup(lfn, job.queryUserDescriptor(), write, false, false, nullptr, privilegedUser, timeout); };
         return blockReportFunc<IDistributedFile *>(job, func, timeout, blockedMsg);
     }