瀏覽代碼

gh-1412 - Rework swapnode for OSS

+Change swapnode, so that it works on the underlying groups that
are generated from the environment clusters.
+Also introduce an implicit "spares" group
+Add various cmdline options to manipulate spares and swaps
+NB: an environment update that updates the cluster, will not override
the 'live' groups, warnings will ensure instead.
The environment can be forced via updtdalienv to create the groups if
desired.

Signed-off-by: Jake Smith <jake.smith@lexisnexis.com>
Jake Smith 13 年之前
父節點
當前提交
03cf67c256

+ 28 - 36
common/remote/rmtfile.cpp

@@ -421,7 +421,7 @@ public:
     }
 };
 
-unsigned validateNodes(const SocketEndpointArray &eps,bool chkc,bool chkd,bool chkver,const char *script,unsigned scripttimeout,SocketEndpointArray &failures,UnsignedArray &failedcodes,StringArray &failedmessages, const char *filename)
+unsigned validateNodes(const SocketEndpointArray &eps,const char *dataDir, const char *mirrorDir, bool chkver, const char *script, unsigned scripttimeout, SocketEndpointArray &failures, UnsignedArray &failedcodes, StringArray &failedmessages, const char *filename)
 {
     // used for detecting duff nodes
     PointerIArrayOf<ISocket> sockets;
@@ -449,19 +449,16 @@ unsigned validateNodes(const SocketEndpointArray &eps,bool chkc,bool chkd,bool c
         StringArray &failedmessages;
         UnsignedArray &failedcodes;
         CriticalSection &sect;
-        bool chkc;
-        bool chkd;
+        StringAttr dataDir, mirrorDir;
         bool chkv;
         const char *filename;
         const char *script;
         unsigned scripttimeout;
 public:
-        casyncfor(const SocketEndpointArray &_eps,const PointerIArrayOf<ISocket> &_sockets,bool _chkc,bool _chkd,bool _chkv, const char *_script, unsigned _scripttimeout, const char *_filename,SocketEndpointArray &_failures, StringArray &_failedmessages,UnsignedArray &_failedcodes,CriticalSection &_sect) 
-            : eps(_eps), sockets(_sockets), 
+        casyncfor(const SocketEndpointArray &_eps,const PointerIArrayOf<ISocket> &_sockets,const char *_dataDir,const char *_mirrorDir,bool _chkv, const char *_script, unsigned _scripttimeout, const char *_filename,SocketEndpointArray &_failures, StringArray &_failedmessages,UnsignedArray &_failedcodes,CriticalSection &_sect)
+            : eps(_eps), sockets(_sockets), dataDir(_dataDir), mirrorDir(_mirrorDir),
               failures(_failures), failedmessages(_failedmessages), failedcodes(_failedcodes), sect(_sect)
         { 
-            chkc = _chkc;
-            chkd = _chkd;
             chkv = _chkv;
             filename = _filename;
             script = _script;
@@ -469,9 +466,6 @@ public:
         }
         void Do(unsigned i)
         {
-#ifdef NIGEL_TESTING            
-            IpAddress badip("10.173.34.70");
-#endif
             ISocket *sock = sockets.item(i);
             if (!sock)
                 return;
@@ -519,23 +513,25 @@ public:
                     }
                 }
             }
-            if (!code&&(chkc||chkd)) {
+            if (!code&&(dataDir.get()||mirrorDir.get())) {
                 clientAddSocketToCache(ep,sock);
-                StringBuffer path;
-                if (iswin) 
-                    path.append("c:\\");
-                else
-                    path.append("/c$/");
-                if (filename)
-                    path.append(filename);
-                else {
-                    path.append("dafs_");
-                    genUUID(path);
-                    path.append(".tmp");
-                }
-                for (unsigned drive=chkc?0:1;drive<(chkd?2U:1U);drive++) {
+                const char *drivePath = NULL;
+                const char *drivePaths[2];
+                unsigned drives=0;
+                if (mirrorDir.get()) drivePaths[drives++] = mirrorDir.get();
+                if (dataDir.get()) drivePaths[drives++] = dataDir.get();
+                do
+                {
+                    StringBuffer path(drivePaths[--drives]);
+                    addPathSepChar(path);
+                    if (filename)
+                        path.append(filename);
+                    else {
+                        path.append("dafs_");
+                        genUUID(path);
+                        path.append(".tmp");
+                    }
                     RemoteFilename rfn;
-                    setPathDrive(path,drive);   
                     rfn.setPath(ep,path);
                     Owned<IFile> file = createIFile(rfn);
                     size32_t sz;
@@ -551,9 +547,9 @@ public:
                     }
                     catch (IException *e) {
                         if (e->errorCode()==DISK_FULL_EXCEPTION_CODE)
-                            code |=  (drive?DAFS_VALIDATE_DISK_FULL_C:DAFS_VALIDATE_DISK_FULL_D);
+                            code |=  (drivePath==dataDir.get()?DAFS_VALIDATE_DISK_FULL_DATA:DAFS_VALIDATE_DISK_FULL_MIRROR);
                         else
-                            code |=  (drive?DAFS_VALIDATE_WRITE_FAIL_C:DAFS_VALIDATE_WRITE_FAIL_D);
+                            code |=  (drivePath==dataDir.get()?DAFS_VALIDATE_WRITE_FAIL_DATA:DAFS_VALIDATE_WRITE_FAIL_MIRROR);
                         if (errstr.length())
                             errstr.append(',');
                         e->errorMessage(errstr);
@@ -564,18 +560,14 @@ public:
                         Owned<IFileIO> fileio = file->open(IFOread);
                         char buf[64];
                         size32_t rd = fileio->read(0,sizeof(buf)-1,buf);
-                        if ((rd!=sz)||(memcmp(buf,ds.str(),sz)!=0)
-#ifdef NIGEL_TESTING            
-                            ||(drive&&(badip.ipequals(ep)))
-#endif
-                            ) {
+                        if ((rd!=sz)||(memcmp(buf,ds.str(),sz)!=0)) {
                             StringBuffer s;
                             ep.getIpText(s);
-                            throw MakeStringException(-1,"Data discrepancy on disk read of %c$ of %s",'c'+drive,s.str());
+                            throw MakeStringException(-1,"Data discrepancy on disk read of %s of %s",path.str(),s.str());
                         }
                     }
                     catch (IException *e) {
-                        code |=  (drive?DAFS_VALIDATE_READ_FAIL_C:DAFS_VALIDATE_READ_FAIL_D);
+                        code |=  (drivePath==dataDir.get()?DAFS_VALIDATE_READ_FAIL_DATA:DAFS_VALIDATE_READ_FAIL_MIRROR);
                         if (errstr.length())
                             errstr.append(',');
                         e->errorMessage(errstr);
@@ -590,8 +582,8 @@ public:
                             e->Release();           // supress error
                         }
                     }
-
                 }
+                while (0 != drives);
             }
             if (!code&&scripttimeout) { // use a second thread to implement script timeout
                 Owned<CScriptThread> thread = new CScriptThread(ep,script);
@@ -610,7 +602,7 @@ public:
                 failedmessages.append(errstr.str());
             }
         }
-    } afor(eps,sockets,chkc,chkd,chkver,script,scripttimeout,filename,failures,failedmessages,failedcodes,sect);
+    } afor(eps,sockets,dataDir,mirrorDir,chkver,script,scripttimeout,filename,failures,failedmessages,failedcodes,sect);
     afor.For(eps.ordinality(), 10, false, true);
     return failures.ordinality();
 }

+ 7 - 7
common/remote/rmtfile.hpp

@@ -84,15 +84,15 @@ extern REMOTE_API void setRemoteFileTimeouts(unsigned maxconnecttime,unsigned ma
 
 #define DAFS_VALIDATE_CONNECT_FAIL  (0x01)
 #define DAFS_VALIDATE_BAD_VERSION   (0x02)
-#define DAFS_VALIDATE_WRITE_FAIL_C  (0x12)
-#define DAFS_VALIDATE_READ_FAIL_C   (0x14)
-#define DAFS_VALIDATE_DISK_FULL_C   (0x18)
-#define DAFS_VALIDATE_WRITE_FAIL_D  (0x22)
-#define DAFS_VALIDATE_READ_FAIL_D   (0x24)
-#define DAFS_VALIDATE_DISK_FULL_D   (0x28)
+#define DAFS_VALIDATE_WRITE_FAIL_DATA  (0x12)
+#define DAFS_VALIDATE_READ_FAIL_DATA   (0x14)
+#define DAFS_VALIDATE_DISK_FULL_DATA   (0x18)
+#define DAFS_VALIDATE_WRITE_FAIL_MIRROR  (0x22)
+#define DAFS_VALIDATE_READ_FAIL_MIRROR   (0x24)
+#define DAFS_VALIDATE_DISK_FULL_MIRROR   (0x28)
 #define DAFS_SCRIPT_FAIL            (0x40)
                                 
-extern REMOTE_API unsigned validateNodes(const SocketEndpointArray &ep,bool chkc,bool chkd,bool chkver,const char *script,unsigned scripttimeout,SocketEndpointArray &failures,UnsignedArray &failedcodes, StringArray &failedmessages, const char *filename=NULL);
+extern REMOTE_API unsigned validateNodes(const SocketEndpointArray &eps,const char *dataDir, const char *mirrorDir, bool chkver, const char *script, unsigned scripttimeout, SocketEndpointArray &failures, UnsignedArray &failedcodes, StringArray &failedmessages, const char *filename=NULL);
 
 
 #endif

+ 237 - 18
dali/base/dadfs.cpp

@@ -6272,7 +6272,7 @@ public:
         return true;
     }
 
-    void swapNode(IpAddress &from, IpAddress &to)
+    void swapNode(const IpAddress &from, const IpAddress &to)
     {
         if (from.ipequals(to))
             return;
@@ -6283,12 +6283,16 @@ public:
         to.getIpText(tos);
         Owned<IPropertyTreeIterator> pe  = connlock.conn->queryRoot()->getElements("Group");
         ForEach(*pe) {
+            IPropertyTree &group = pe->query();
+            const char *kind = group.queryProp("@kind");
+            if (kind && streq("Spare", kind))
+                continue;
             StringBuffer name;
-            pe->query().getProp("@name",name);
+            group.getProp("@name",name);
             StringBuffer xpath("Node[@ip = \"");
             xpath.append(froms).append("\"]");
             for (unsigned guard=0; guard<1000; guard++) {
-                Owned<IPropertyTreeIterator> ne = pe->query().getElements(xpath.str());
+                Owned<IPropertyTreeIterator> ne = group.getElements(xpath.str());
                 if (!ne->first()) 
                     break;
                 ne->query().setProp("@ip",tos.str());
@@ -7592,6 +7596,20 @@ struct CMachineEntry: public CInterface
 typedef CMachineEntry *CMachineEntryPtr;
 typedef MapStringTo<CMachineEntryPtr> CMachineEntryMap;
 
+StringBuffer &getClusterGroupName(IPropertyTree &cluster, StringBuffer &groupName)
+{
+    const char *name = cluster.queryProp("@name");
+    const char *nodeGroupName = cluster.queryProp("@nodeGroup");
+    if (nodeGroupName)
+        name = nodeGroupName;
+    return groupName.append(name);
+}
+
+StringBuffer &getClusterSpareGroupName(IPropertyTree &cluster, StringBuffer &groupName)
+{
+    return getClusterGroupName(cluster, groupName).append("_spares");
+}
+
 class CInitGroups
 {
     CMachineEntryMap machinemap;
@@ -7600,8 +7618,11 @@ class CInitGroups
     StringArray clusternames;
     unsigned defaultTimeout;
 
+    IPropertyTree *createTypedGroup(IGroup *group, const char *name, bool cluster, const char *kind, const char *dir)
+    {
+    }
 
-    bool addClusterGroup(const char *name,IGroup *group,const char *kind, bool realcluster, const char *dir,bool force)
+    bool addClusterGroup(const char *name, IGroup *group, const char *kind, bool realcluster, const char *dir, bool force)
     {
         StringBuffer lcname(name);
         name = lcname.trim().toLowerCase().str();
@@ -7609,8 +7630,11 @@ class CInitGroups
         StringBuffer prop;
         prop.appendf("Group[@name=\"%s\"]",name);
         IPropertyTree *old = root->queryPropTree(prop.str());
-        if (group)
-            clusternames.append(name);
+        if (group) // Not sure this should happend if !realcluster
+        {
+            if (!kind || !*kind || !streq("Spare", kind)) // I think it should need this if !realcluster (as it won't be)
+                clusternames.append(name);
+        }
         bool differs=false;
         if (old) {
             // see if identical
@@ -7647,6 +7671,8 @@ class CInitGroups
                     }
                     i++;
                 }
+                if (i<group->ordinality())
+                    differs = true;
                 if (!differs&&(i==group->ordinality())) {
                     if (old->getPropBool("@cluster")!=realcluster)
                         if (realcluster)
@@ -7673,15 +7699,14 @@ class CInitGroups
             val->setProp("@kind",kind);
         if (dir)
             val->setProp("@dir",dir);
-        INodeIterator &gi = *group->getIterator();
+        Owned<INodeIterator> iter = group->getIterator();
         StringBuffer str;
-        ForEach(gi) {
+        ForEach(*iter) {
             IPropertyTree *n = createPTree("Node");
             n = val->addPropTree("Node",n);
-            gi.query().endpoint().getIpText(str.clear());
+            iter->query().endpoint().getIpText(str.clear());
             n->setProp("@ip",str.str());
         }
-        gi.Release();
         root->addPropTree("Group",val);
         return true;
     }
@@ -7791,7 +7816,7 @@ class CInitGroups
             if (!addClusterGroup(groupname,grp,roxie?"Roxie":"Thor",realcluster,defdir,force))
             {
                 ret = false;
-                VStringBuffer msg("New constructed group definition for cluster %s, mismatched existing group layout", groupname);
+                VStringBuffer msg("Newly constructed group definition for cluster %s, mismatched existing group layout", groupname);
                 WARNLOG("%s", msg.str());
                 messages.append(msg).newline();
             }
@@ -7816,7 +7841,7 @@ class CInitGroups
                 if (!addClusterGroup(groupname.str(),grp,"RoxieFarm",true,farm.queryProp("@dataDirectory"),force))
                 {
                     ret = false;
-                    VStringBuffer msg("New constructed group definition for cluster %s, mismatched existing group layout", groupname.str());
+                    VStringBuffer msg("Newly constructed group definition for cluster %s, mismatched existing group layout", groupname.str());
                     WARNLOG("%s", msg.str());
                     messages.append(msg).newline();
                 }
@@ -7826,7 +7851,39 @@ class CInitGroups
         return ret;
     }
 
+    bool constructThorSpareGroup(IPropertyTree &cluster, bool force, StringBuffer &messages)
+    {
+        // construct spare group
+        StringBuffer spareGroupName;
+        getClusterSpareGroupName(cluster, spareGroupName);
+        SocketEndpointArray spareEps;
+        Owned<IPropertyTreeIterator> spares = cluster.getElements("ThorSpareProcess");
+        ForEach(*spares) {
+            IPropertyTree &spare = spares->query();
+            const char *computer = spare.queryProp("@computer");
+            CMachineEntryPtr *m = machinemap.getValue(computer);
+            if (!m) {
+                VStringBuffer msg("Cannot construct spare group %s, computer name %s not found\n", spareGroupName.str(), computer);
+                WARNLOG("%s", msg.str());
+                messages.append(msg).newline();
+                return false;
+            }
+            SocketEndpoint ep = (*m)->ep;
+            spareEps.append(ep);
+        }
+        if (!spareEps.ordinality())
+            return true; // nothing to do
+        Owned<IGroup> spareGroup = createIGroup(spareEps);
+        if (!addClusterGroup(spareGroupName.str(), spareGroup, "Spare", false, NULL, force)) {
+            VStringBuffer msg("Newly constructed spare group definition for cluster %s, mismatched existing group layout", cluster.queryProp("@name"));
+            WARNLOG("%s", msg.str());
+            messages.append(msg).newline();
+            return false;
+        }
+        return true;
+    }
 
+    enum CgCmd { cg_null, cg_reset, cg_add, cg_remove };
 public:
 
     CInitGroups(unsigned _defaultTimeout)
@@ -7835,19 +7892,160 @@ public:
         defaultTimeout = _defaultTimeout;
     }
 
+    bool doClusterGroup(CgCmd cmd, const char *clusterName, const char *type, bool spares, SocketEndpointArray *eps, StringBuffer &messages)
+    {
+        Owned<IRemoteConnection> conn = querySDS().connect("/Environment/Software", myProcessSession(), RTM_LOCK_READ, SDS_CONNECT_TIMEOUT);
+        if (!conn)
+            return false;
+        if (!clusterName || !*clusterName)
+            return false;
+        if (!type || !*type)
+            return false;
+        bool ret = true;
+        IPropertyTree* root = conn->queryRoot();
+        Owned<IPropertyTreeIterator> clusters;
+        StringBuffer errMsg;
+        const char *clusterType = type;
+        if (loadMachineMap()) {
+            VStringBuffer xpath("%s[@name=\"%s\"]", type, clusterName);
+            clusters.setown(root->getElements(xpath.str()));
+            if (!clusters || !clusters->first()) {
+                VStringBuffer errMsg("Could not find type %s, %s cluster", type, clusterName);
+                WARNLOG("%s", errMsg.str());
+                messages.append(errMsg).newline();
+                ret = false;
+            }
+            else {
+                if (!streq("ThorCluster", type))
+                    return false; // currently only Thor supported here.
+                IPropertyTree &cluster = clusters->query();
+
+                switch (cmd)
+                {
+                    case cg_reset:
+                    {
+                        if (spares)
+                        {
+                            if (!constructThorSpareGroup(cluster,true,messages))
+                                ret = false;
+                        }
+                        else
+                        {
+                            if (!constructGroup(cluster,false,"ThorSlaveProcess",NULL,true,messages))
+                                ret = false;
+                        }
+                        break;
+                    }
+                    case cg_add:
+                    {
+                        assertex(eps);
+                        StringBuffer groupName;
+                        getClusterSpareGroupName(cluster, groupName);
+                        IPropertyTree *root = groupsconnlock.conn->queryRoot();
+                        VStringBuffer xpath("Group[@name=\"%s\"]",groupName.str());
+                        IPropertyTree *existing = root->queryPropTree(xpath.str());
+                        if (existing)
+                        {
+                            Owned<IPropertyTreeIterator> iter = existing->getElements("Node");
+                            ForEach(*iter)
+                            {
+                                SocketEndpoint ep(iter->query().queryProp("@ip"));
+                                if (eps->zap(ep))
+                                {
+                                    StringBuffer epStr;
+                                    VStringBuffer errMsg("addSpares: not adding: %s, already in spares", ep.getUrlStr(epStr).str());
+                                    WARNLOG("%s", errMsg.str());
+                                    messages.append(errMsg).newline();
+                                    while (eps->zap(ep)); // delete any other duplicates
+                                }
+                            }
+                        }
+                        else
+                        {
+                            existing = createPTree();
+                            existing->setProp("@name", groupName.str());
+                            existing = root->addPropTree("Group", existing);
+                        }
+                        // add remaining
+                        ForEachItemIn(e, *eps)
+                        {
+                            SocketEndpoint &ep = eps->item(e);
+                            StringBuffer ipStr;
+                            ep.getIpText(ipStr);
+                            IPropertyTree *node = createPTree();
+                            node->setProp("@ip", ipStr.str());
+                            existing->addPropTree("Node", node);
+                        }
+                        break;
+                    }
+                    case cg_remove:
+                    {
+                        assertex(eps);
+                        StringBuffer groupName;
+                        getClusterSpareGroupName(cluster, groupName);
+                        IPropertyTree *root = groupsconnlock.conn->queryRoot();
+                        VStringBuffer xpath("Group[@name=\"%s\"]",groupName.str());
+                        IPropertyTree *existing = root->queryPropTree(xpath.str());
+                        if (existing)
+                        {
+                            ForEachItemIn(e, *eps)
+                            {
+                                SocketEndpoint &ep = eps->item(e);
+                                StringBuffer ipStr;
+                                ep.getIpText(ipStr);
+                                VStringBuffer xpath("Node[@ip=\"%s\"]", ipStr.str());
+                                if (!existing->removeProp(xpath.str()))
+                                {
+                                    VStringBuffer errMsg("removeSpares: %s not found in spares", ipStr.str());
+                                    WARNLOG("%s", errMsg.str());
+                                    messages.append(errMsg).newline();
+                                    while (eps->zap(ep)); // delete any other duplicates
+                                }
+                                else
+                                    while (existing->removeProp(xpath.str())); // remove any others, shouldn't be any
+                            }
+                        }
+                        break;
+                    }
+                }
+                if (clusters->next())
+                {
+                    VStringBuffer errMsg("resetThorGroup: more than one cluster named: %s", clusterName);
+                    WARNLOG("%s", errMsg.str());
+                    messages.append(errMsg).newline();
+                    ret = false;
+                }
+            }
+        }
+        return ret;
+    }
+    bool resetClusterGroup(const char *clusterName, const char *type, bool spares, StringBuffer &messages)
+    {
+        return doClusterGroup(cg_reset, clusterName, type, spares, NULL, messages);
+    }
+    bool addSpares(const char *clusterName, const char *type, SocketEndpointArray &eps, StringBuffer &messages)
+    {
+        return doClusterGroup(cg_add, clusterName, type, true, &eps, messages);
+    }
+    bool removeSpares(const char *clusterName, const char *type, SocketEndpointArray &eps, StringBuffer &messages)
+    {
+        return doClusterGroup(cg_remove, clusterName, type, true, &eps, messages);
+    }
     bool constructGroups(bool force, StringBuffer &messages)
     {
         Owned<IRemoteConnection> conn = querySDS().connect("/Environment/Software", myProcessSession(), RTM_LOCK_READ, SDS_CONNECT_TIMEOUT);
         if (!conn)
             return false;
-        bool ret=true;
+        bool ret = true;
         IPropertyTree* root = conn->queryRoot();
         Owned<IPropertyTreeIterator> clusters;
-        if (loadMachineMap()) { 
+        if (loadMachineMap()) {
             clusters.setown(root->getElements("ThorCluster"));
-            ForEach(*clusters) 
-            {
-                if (!constructGroup(clusters->query(),false,"ThorSlaveProcess",NULL,force,messages))
+            ForEach(*clusters) {
+                IPropertyTree &cluster = clusters->query();
+                if (!constructGroup(cluster,false,"ThorSlaveProcess",NULL,force,messages))
+                    ret = false;
+                if (!constructThorSpareGroup(cluster,force,messages))
                     ret = false;
             }
             clusters.setown(root->getElements("RoxieCluster"));
@@ -7881,7 +8079,7 @@ public:
                                 if (!addClusterGroup(gname.str(),grp,"hthor",true,NULL,force))
                                 {
                                     ret = false;
-                                    VStringBuffer msg("New constructed group definition for EclAgentProcess %s, mismatched existing group layout", groupname);
+                                    VStringBuffer msg("Newly constructed group definition for EclAgentProcess %s, mismatched existing group layout", groupname);
                                     WARNLOG("%s", msg.str());
                                     messages.append(msg).newline();
                                 }
@@ -7921,6 +8119,26 @@ bool initClusterGroups(bool force, StringBuffer &response, unsigned timems)
     return init.constructGroups(force, response);
 }
 
+bool resetClusterGroup(const char *clusterName, const char *type, bool spares, StringBuffer &response, unsigned timems)
+{
+    CInitGroups init(timems);
+    return init.resetClusterGroup(clusterName, type, spares, response);
+}
+
+bool addClusterSpares(const char *clusterName, const char *type, SocketEndpointArray &eps, StringBuffer &response, unsigned timems)
+{
+    CInitGroups init(timems);
+    return init.addSpares(clusterName, type, eps, response);
+}
+
+bool removeClusterSpares(const char *clusterName, const char *type, SocketEndpointArray &eps, StringBuffer &response, unsigned timems)
+{
+    CInitGroups init(timems);
+    return init.removeSpares(clusterName, type, eps, response);
+}
+
+
+
 
 class CDaliDFSServer: public Thread, public CTransactionLogTracker, implements IDaliServer
 {  // Coven size
@@ -10163,6 +10381,7 @@ void CDistributedFileDirectory::renameFileRelationships(const char *oldname,cons
 }
 
 
+// JCSMORE what was this for, not called by anything afaics
 bool CDistributedFileDirectory::publishMetaFileXML(const CDfsLogicalFileName &logicalname,IUserDescriptor *user=NULL)
 {
     if (logicalname.isExternal()||logicalname.isForeign()||logicalname.isQuery()) 

+ 7 - 1
dali/base/dadfs.hpp

@@ -543,7 +543,7 @@ interface INamedGroupStore: implements IGroupResolver
     virtual void remove(const char *logicalgroupname) = 0;
     virtual bool find(IGroup *grp, StringBuffer &lname, bool add=false) = 0;
     virtual void addUnique(IGroup *group,StringBuffer &lname,const char *dir=NULL) = 0;
-    virtual void swapNode(IpAddress &from, IpAddress &to) = 0;
+    virtual void swapNode(const IpAddress &from, const IpAddress &to) = 0;
     virtual IGroup *getRemoteGroup(const INode *foreigndali, const char *gname, unsigned foreigndalitimeout=FOREIGN_DALI_TIMEOUT, StringBuffer *dir=NULL) = 0;
     virtual IGroup *lookup(const char *logicalgroupname, StringBuffer &dir) = 0;
     virtual unsigned setDefaultTimeout(unsigned timems) = 0;                                    // sets default timeout for SDS connections and locking                                                                                         // returns previous value
@@ -611,6 +611,12 @@ extern da_decl IDaliServer *createDaliDFSServer(IPropertyTree *config); // calle
 
 // to initialize clustergroups after clusters change in the environment
 extern da_decl bool initClusterGroups(bool force, StringBuffer &response, unsigned timems=INFINITE);
+extern da_decl bool resetClusterGroup(const char *clusterName, const char *type, bool spares, StringBuffer &response, unsigned timems=INFINITE);
+extern da_decl bool addClusterSpares(const char *clusterName, const char *type, SocketEndpointArray &eps, StringBuffer &response, unsigned timems=INFINITE);
+extern da_decl bool removeClusterSpares(const char *clusterName, const char *type, SocketEndpointArray &eps, StringBuffer &response, unsigned timems=INFINITE);
+// should poss. belong in lib workunit
+extern da_decl StringBuffer &getClusterGroupName(IPropertyTree &cluster, StringBuffer &groupName);
+extern da_decl StringBuffer &getClusterSpareGroupName(IPropertyTree &cluster, StringBuffer &groupName);
 
 extern da_decl IDistributedFileTransaction *createDistributedFileTransaction(IUserDescriptor *user=NULL);
 

+ 0 - 130
dali/base/dautils.cpp

@@ -2359,136 +2359,6 @@ IDaliMutex  *createDaliMutex(const char *name)
     return new CDaliMutex(name);
 }
 
-bool getSwapNodeInfo(IPropertyTree *options,StringAttr &grpname,Owned<IGroup> &grp,Owned<IRemoteConnection> &conn,Owned<IPropertyTree> &info, bool create)
-{
-    grpname.set(options->queryProp("@nodeGroup"));
-    if (grpname.isEmpty())
-        grpname.set(options->queryProp("@name"));
-    if (grpname.isEmpty()) {
-        ERRLOG("SWAPNODE - no group name specified in thor.xml");
-        return false;
-    }
-    grp.setown(queryNamedGroupStore().lookup(grpname));
-    if (!grp) {
-        ERRLOG("SWAPNODE: group %s not found",grpname.get());
-        return false;
-    }
-    conn.setown(querySDS().connect("/SwapNode", myProcessSession(), RTM_LOCK_WRITE|(create?RTM_CREATE_QUERY:0), 1000*60*5));
-    if (!conn) {
-        ERRLOG("SWAPNODE: could not connect to /SwapNode branch");
-        return false;
-    }
-    StringBuffer xpath;
-    xpath.appendf("Thor[@group=\"%s\"]",grpname.get());
-    info.set(conn->queryRoot()->queryPropTree(xpath.str()));
-    if (!info) {
-        if (!create) {
-            PROGLOG("SWAPNODE: no information for group %s",grpname.get());
-            return false;
-        }
-        info.set(conn->queryRoot()->addPropTree("Thor",createPTree("Thor")));
-        info->setProp("@group",grpname.get());
-    }
-    return true;
-}
-
-
-
-bool checkThorNodeSwap(IPropertyTree *options,const char *failedwuid, unsigned mininterval)
-{
-    bool ret = false;
-    if (mininterval==(unsigned)-1) { // called by thor
-        mininterval = 0;
-        if (!options||!options->getPropBool("SwapNode/@autoSwapNode"))
-            return false;
-        if ((!failedwuid||!*failedwuid)&&!options->getPropBool("SwapNode/@checkAfterEveryJob"))
-            return false;
-    }
-
-    try {
-        Owned<IGroup> grp;
-        Owned<IRemoteConnection> conn;
-        Owned<IPropertyTree> info;
-        StringAttr grpname;
-        if (getSwapNodeInfo(options,grpname,grp,conn,info,true)) {
-            PROGLOG("checkNodeSwap started");
-            StringBuffer xpath;
-            CDateTime dt;
-            StringBuffer ts;
-            // see if done less than mininterval ago
-            if (mininterval) {
-                dt.setNow();
-                dt.adjustTime(-((int)mininterval));
-                if (info->getProp("@timeChecked",ts)) {
-                    CDateTime dtc;
-                    dtc.setString(ts.str());
-                    if (dtc.compare(dt,false)>0) {
-                        PROGLOG("checkNodeSwap using cached validate from %s",ts.str());
-                        xpath.clear().appendf("BadNode[@timeChecked=\"%s\"]",ts.str());
-                        return info->hasProp(xpath.str());
-                    }
-                }
-            }
-
-            SocketEndpointArray epa;
-            grp->getSocketEndpoints(epa);
-            ForEachItemIn(i1,epa) {
-                epa.item(i1).port = getDaliServixPort();
-            }
-            SocketEndpointArray failures;
-            UnsignedArray failedcodes;
-            StringArray failedmessages;
-            unsigned start = msTick();
-            validateNodes(epa,options->getPropBool("SwapNode/@swapNodeCheckC",true),options->getPropBool("SwapNode/@swapNodeCheckD",false),false,options->queryProp("SwapNode/@swapNodeCheckScript"),options->getPropInt("SwapNode/@swapNodeCheckScriptTimeout")*1000,failures,failedcodes,failedmessages);
-            dt.setNow();
-            dt.getString(ts.clear());
-            ForEachItemIn(i,failures) {
-                SocketEndpoint ep(failures.item(i));
-                ep.port = 0;
-                StringBuffer ips;
-                ep.getIpText(ips);
-                int r = (int)grp->rank(ep);
-                if (r<0) {  // shouldn't occur
-                    ERRLOG("SWAPNODE node %s not found in group %s",ips.str(),grpname.get());
-                    continue;
-                }
-                PROGLOG("CheckSwapNode FAILED(%d) %s : %s",failedcodes.item(i),ips.str(),failedmessages.item(i));
-                // SNMP TBD?
-
-                ret = true;
-                xpath.clear().appendf("BadNode[@netAddress=\"%s\"]",ips.str());
-                IPropertyTree *bnt = info->queryPropTree(xpath.str());
-                if (!bnt) {
-                    bnt = info->addPropTree("BadNode",createPTree("BadNode"));
-                    bnt->setProp("@netAddress",ips.str());
-                }
-                bnt->setPropInt("@numTimes",bnt->getPropInt("@numTimes",0)+1);
-                bnt->setProp("@timeChecked",ts.str());
-                bnt->setProp("@time",ts.str());
-                bnt->setPropInt("@code",failedcodes.item(i));
-                bnt->setPropInt("@rank",r);
-                bnt->setProp(NULL,failedmessages.item(i));
-            }
-            if (failedwuid&&*failedwuid) {
-                xpath.clear().appendf("WorkUnit[@id=\"%s\"]",failedwuid);
-                IPropertyTree *wut = info->queryPropTree(xpath.str());
-                if (!wut) {
-                    wut = info->addPropTree("WorkUnit",createPTree("WorkUnit"));
-                    wut->setProp("@id",failedwuid);
-                }
-                wut->setProp("@time",ts.str());
-            }
-            PROGLOG("checkNodeSwap: Time taken = %dms",msTick()-start);
-            info->setProp("@timeChecked",ts.str());
-        }
-    }
-    catch (IException *e) {
-        EXCLOG(e,"checkNodeSwap");
-    }
-    return ret;
-}
-
-
 // ===============================================================================
 // File redirection
 

+ 0 - 8
dali/base/dautils.hpp

@@ -368,14 +368,6 @@ interface IDaliMutex: implements IInterface
 };
 extern da_decl IDaliMutex  *createDaliMutex(const char *name);
 
-// Called from swapnode and thor
-extern da_decl bool checkThorNodeSwap(IPropertyTree *options,   // thor.xml options
-                                      const char *failedwuid,   // failed WUID or null if none
-                                      unsigned mininterval=0    // minimal interval before redoing check (mins)
-                                      ); // if returns true swap needed
-// called by swapnode
-extern da_decl bool getSwapNodeInfo(IPropertyTree *options,StringAttr &grpname,Owned<IGroup> &grp,Owned<IRemoteConnection> &conn,Owned<IPropertyTree> &info, bool create);
-
 interface IDFSredirection;
 extern da_decl IDFSredirection *createDFSredirection(); // only called by dadfs.cpp
 

+ 1 - 1
esp/services/ws_topology/CMakeLists.txt

@@ -30,7 +30,6 @@ project( ws_topology )
 include(${HPCC_SOURCE_DIR}/esp/scm/smcscm.cmake)
 
 set (    SRCS 
-         ../../../tools/swapnode/swapnodemain.cpp 
          ${ESPSCM_GENERATED_DIR}/ws_topology_esp.cpp 
          ws_topologyPlugin.cpp 
          ws_topologyService.cpp 
@@ -80,6 +79,7 @@ target_link_libraries ( ws_topology
          SMCLib 
          LdapSecurity 
          securesocket 
+         swapnodelib
          )
          
 IF (USE_ZLIB)

+ 4 - 4
esp/services/ws_topology/ws_topologyService.cpp

@@ -28,7 +28,7 @@
 #include "dafdesc.hpp"
 #include "dasds.hpp"
 #include "danqs.hpp"
-#include "swapnodemain.hpp"
+#include "swapnodelib.hpp"
 #include "dalienv.hpp"
 #ifdef _USE_ZLIB
 #include "zcrypt.hpp"
@@ -106,9 +106,9 @@ bool CWsTopologyEx::onTpSwapNode(IEspContext &context,IEspTpSwapNodeRequest  &re
         //another client (like configenv) may have updated the constant environment so reload it
         m_envFactory->validateCache();
 
-        resp.setTpSwapNodeResult(false);
-        SwapNode(req.getCluster(),req.getOldIP(),req.getNewIP(),0);
-        resp.setTpSwapNodeResult(true);
+        bool res = swapNode(req.getCluster(),req.getOldIP(),req.getNewIP());
+
+        resp.setTpSwapNodeResult(res);
 
 
         StringBuffer path;

+ 4 - 4
initfiles/componentfiles/configxml/thor.xsd.in

@@ -164,17 +164,17 @@
               </xs:appinfo>
             </xs:annotation>
           </xs:attribute>
-          <xs:attribute name="SwapNodeCheckC" type="xs:boolean" use="optional" default="true">
+          <xs:attribute name="SwapNodeCheckPrimaryDrive" type="xs:boolean" use="optional" default="true">
             <xs:annotation>
               <xs:appinfo>
-                <tooltip>C drive is checked for read/write</tooltip>
+                <tooltip>Primary drive is checked for read/write</tooltip>
               </xs:appinfo>
             </xs:annotation>
           </xs:attribute>
-          <xs:attribute name="SwapNodeCheckD" type="xs:boolean" use="optional" default="true">
+          <xs:attribute name="SwapNodeCheckMirrorDrive" type="xs:boolean" use="optional" default="true">
             <xs:annotation>
               <xs:appinfo>
-                <tooltip>D drive is checked for read/write</tooltip>
+                <tooltip>Mirror drive is checked for read/write</tooltip>
               </xs:appinfo>
             </xs:annotation>
           </xs:attribute>

+ 0 - 7
initfiles/componentfiles/configxml/thor.xsl

@@ -153,13 +153,6 @@
           </xsl:if>
         </xsl:for-each>
       </SSH>
-      <SwapNode>
-        <xsl:for-each select="SwapNode/@*">
-          <xsl:if test="string(.) != ''">
-            <xsl:copy-of select="."/>
-          </xsl:if>
-        </xsl:for-each>
-      </SwapNode>
     </Thor>
   </xsl:template>
 

+ 10 - 0
initfiles/componentfiles/thor/run_thor

@@ -60,6 +60,16 @@ while [ 1 ]; do
 
         echo 'stopping thor(slaves) for restart'
         $deploydir/stop_thor $deploydir keep_sentinel
+
+        if [ 0 != $autoSwapNode ]; then
+            echo "Running autoswap $THORNAME"
+            compname=`basename $PWD`
+            swapnode auto $DALISERVER $compname
+            errcode=$?
+            if [ 0 != ${errcode} ]; then
+                echo auto swap node failed, errcode=${errcode}
+            fi
+        fi
     else
         echo failed to start thormaster$LCR, pausing for 30 seconds
         sleep 30

+ 2 - 0
thorlcr/master/CMakeLists.txt

@@ -54,6 +54,7 @@ include_directories (
          ./../../rtl/eclrtl 
          ./../master 
          ./../../common/thorhelper 
+         ./../../tools/swapnode
          ${CMAKE_BINARY_DIR}
          ${CMAKE_BINARY_DIR}/oss
     )
@@ -86,6 +87,7 @@ target_link_libraries (  thormaster_lcr
          mfilemanager_lcr 
          graphmaster_lcr 
          activitymasters_lcr 
+         swapnodelib
     )
 
 

+ 3 - 2
thorlcr/master/thgraphmanager.cpp

@@ -41,6 +41,7 @@
 #include "dautils.hpp"
 #include "dllserver.hpp"
 #include "eclhelper.hpp"
+#include "swapnodelib.hpp"
 #include "thactivitymaster.ipp"
 #include "thdemonserver.hpp"
 #include "thgraphmanager.hpp"
@@ -631,9 +632,9 @@ void CJobManager::reply(IConstWorkUnit *workunit, const char *wuid, IException *
     conversation.clear();
     handlingConversation = false;
 
-    if (checkThorNodeSwap(globals,e?wuid:NULL,(unsigned)-1))
+    Owned<IRemoteConnection> conn = querySDS().connect("/Environment", myProcessSession(), RTM_LOCK_READ, MEDIUMTIMEOUT);
+    if (checkThorNodeSwap(globals->queryProp("@name"),e?wuid:NULL,(unsigned)-1))
         abortThor(e,false);
-
 }
 
 bool CJobManager::executeGraph(IConstWorkUnit &workunit, const char *graphName, const SocketEndpoint &agentEp)

+ 1 - 1
thorlcr/master/thmastermain.cpp

@@ -377,7 +377,7 @@ bool checkClusterRelicateDAFS(IGroup *grp)
     SocketEndpointArray failures;
     UnsignedArray failedcodes;
     StringArray failedmessages;
-    validateNodes(epa,false,false,true,NULL,0,failures,failedcodes,failedmessages);
+    validateNodes(epa,NULL,NULL,true,NULL,0,failures,failedcodes,failedmessages);
     ForEachItemIn(i,failures) {
         SocketEndpoint ep(failures.item(i));
         ep.port = 0;

+ 6 - 34
tools/swapnode/CMakeLists.txt

@@ -15,44 +15,16 @@
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ################################################################################
 
-# Component: swapnode 
+# component: swapnode,swapnodelib
+
 #####################################################
 # Description:
 # ------------
-#    Cmake Input File for swapnode
+#    Cmake Input File swapnode and swapnodelib
 #####################################################
 
-project( swapnode ) 
-
-set (    SRCS 
-         swapnode.cpp 
-         
-    )
-
-include_directories ( 
-         ./../../common/remote 
-         ./../../system/mp 
-         ./../../system/include 
-         ./../../dali/base 
-         ./../../system/jlib 
-         ./../../common/environment 
-         ./../../common/workunit 
-    )
-
-ADD_DEFINITIONS( -D_CONSOLE -DENABLE_AUTOSWAP )
-
-add_executable ( swapnode ${SRCS} )
-install ( TARGETS swapnode DESTINATION ${OSSDIR}/bin )
-target_link_libraries ( swapnode 
-         jlib
-         remote 
-         dalibase 
-         workunit 
-         environment 
-         dllserver 
-         nbcd 
-         eclrtl 
-         deftype 
-    )
 
+project (AllProjects)
 
+include ( ${CMAKE_CURRENT_SOURCE_DIR}/swapnode.cmake)
+include ( ${CMAKE_CURRENT_SOURCE_DIR}/swapnodelib.cmake)

+ 48 - 0
tools/swapnode/swapnode.cmake

@@ -0,0 +1,48 @@
+################################################################################
+#    Copyright (C) 2011 HPCC Systems.
+#
+#    All rights reserved. This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Affero General Public License for more details.
+#
+#    You should have received a copy of the GNU Affero General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+################################################################################
+
+# Component: swapnode
+#####################################################
+# Description:
+# ------------
+#    Cmake Input File for swapnode
+#####################################################
+
+project( swapnode )
+
+set (    SRCS
+         swapnode.cpp
+    )
+
+include_directories (
+         ./../../common/remote
+         ./../../system/mp
+         ./../../system/include
+         ./../../dali/base
+         ./../../system/jlib
+         ./../../common/environment
+         ./../../common/workunit
+    )
+
+ADD_DEFINITIONS( -D_CONSOLE -DENABLE_AUTOSWAP )
+
+add_executable ( swapnode ${SRCS} )
+install ( TARGETS swapnode DESTINATION ${OSSDIR}/bin )
+target_link_libraries ( swapnode
+         jlib
+         swapnodelib
+    )

文件差異過大導致無法顯示
+ 237 - 1134
tools/swapnode/swapnode.cpp


+ 52 - 0
tools/swapnode/swapnodelib.cmake

@@ -0,0 +1,52 @@
+################################################################################
+#    Copyright (C) 2011 HPCC Systems.
+#
+#    All rights reserved. This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU Affero General Public License as
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Affero General Public License for more details.
+#
+#    You should have received a copy of the GNU Affero General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+################################################################################
+
+# Component: swapnodelib
+#####################################################
+# Description:
+# ------------
+#    Cmake Input File for swapnodelib
+#####################################################
+
+project( swapnodelib )
+
+set (    SRCS
+         swapnodelib.cpp
+    )
+
+include_directories (
+         ./../../common/remote
+         ./../../system/mp
+         ./../../system/include
+         ./../../dali/base
+         ./../../system/jlib
+         ./../../common/environment
+         ./../../common/workunit
+    )
+
+HPCC_ADD_LIBRARY( swapnodelib SHARED ${SRCS} )
+set_target_properties(swapnodelib PROPERTIES
+    COMPILE_FLAGS -D_USRDLL
+    DEFINE_SYMBOL SWAPNODELIB_EXPORTS )
+install ( TARGETS swapnodelib DESTINATION ${OSSDIR}/lib )
+target_link_libraries ( swapnodelib
+         jlib
+         remote
+         dalibase
+         workunit
+         environment
+    )

+ 812 - 0
tools/swapnode/swapnodelib.cpp

@@ -0,0 +1,812 @@
+/*##############################################################################
+
+    Copyright (C) 2011 HPCC Systems.
+
+    All rights reserved. This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+############################################################################## */
+
+#include "platform.h"
+#include "thirdparty.h"
+
+#include "jlib.hpp"
+#include "jfile.hpp"
+#include "jptree.hpp"
+#include "jprop.hpp"
+#include "jmisc.hpp"
+
+#include "mpbase.hpp"
+#include "daclient.hpp"
+#include "dadfs.hpp"
+#include "dafdesc.hpp"
+#include "dasds.hpp"
+#include "danqs.hpp"
+#include "dalienv.hpp"
+#include "rmtfile.hpp"
+#include "rmtsmtp.hpp"
+
+#include "dautils.hpp"
+#include "workunit.hpp"
+
+#include "swapnodelib.hpp"
+
+#define SDS_LOCK_TIMEOUT 30000
+#define SWAPNODE_RETRY_TIME (1000*60*60*1) // 1hr
+
+static const LogMsgJobInfo swapnodeJob(UnknownJob, UnknownUser);
+
+static bool ensureThorIsDown(const char *cluster, bool nofail, bool wait)
+{
+    bool retry = false;
+    do {
+        Owned<IRemoteConnection> pStatus = querySDS().connect("/Status/Servers", myProcessSession(), RTM_NONE, SDS_LOCK_TIMEOUT);
+        Owned<IPropertyTreeIterator> it = pStatus->queryRoot()->getElements("Server[@name='ThorMaster']");
+        retry = false;
+        ForEach(*it) {
+            IPropertyTree* pServer = &it->query();
+            if (pServer->hasProp("@cluster") && !strcmp(pServer->queryProp("@cluster"), cluster)) {
+                if (nofail) {
+                    WARNLOG("A Thor on cluster %s is still active", cluster);
+                    if (!wait)
+                        return false;
+                    Sleep(1000*10);
+                    PROGLOG("Retrying...");
+                    retry = true;
+                    break;
+                }
+                throw MakeStringException(-1, "A Thor cluster node swap requires the cluster to be offline.  Please stop the Thor cluster '%s' and try again.", cluster);
+            }
+        }
+    } while (retry);
+    return true;
+}
+
+bool WuResubmit(const char *wuid)
+{
+    Owned<IWorkUnitFactory> factory = getWorkUnitFactory();
+    Owned<IWorkUnit> wu = factory->updateWorkUnit(wuid);
+    if (!wu) {
+        ERRLOG("WuResubmit(%s): could not find workunit",wuid);
+        return false;
+    }
+    if (wu->getState()!=WUStateFailed) {
+        SCMStringBuffer state;
+        wu->getStateDesc(state);
+        ERRLOG("WuResubmit(%s): could not resubmit as workunit state is '%s'",wuid,state.str());
+        return false;
+    }
+    SCMStringBuffer token;
+    wu->getSecurityToken(token);
+    SCMStringBuffer user;
+    SCMStringBuffer password;
+    extractToken(token.str(), wuid, user, password);
+    wu->resetWorkflow();
+    wu->setState(WUStateSubmitted);
+    wu->commit();
+    wu.clear();
+    submitWorkUnit(wuid,user.str(),password.str());
+
+    PROGLOG("WuResubmit(%s): resubmitted",wuid);
+    return true;
+}
+
+static bool resolveComputerName(IPropertyTree *rootEnv,const char *name,IpAddress &ip)
+{
+    StringBuffer query;
+    query.appendf("Hardware/Computer[@name=\"%s\"]",name);
+    Owned<IPropertyTree> machine = rootEnv->getPropTree(query.str());
+    const char *node = machine?machine->queryProp("@netAddress"):NULL;
+    if (!node||!*node)
+        false;
+    ip.ipset(node);
+    return true;
+}
+
+
+// SwapNode info
+//
+//  SwapNode/
+//    Thor [ @group, @timeChecked ]
+//      BadNode [ @netAddress, @timeChecked, @time, @numTimes, @code, @rank, @ (msg)
+//      Swap [ @inNetAddress, @outNetAddress, @time, @rank]
+//      WorkUnit [ @id @time @resubmitted ]
+
+//time,nodenum,ip,code,errmsg
+//time,nodenum,swapout,swapin
+
+
+class CSwapNode
+{
+protected:
+    Linked<IPropertyTree> environment;
+    StringAttr clusterName;
+    StringAttr groupName, spareGroupName;
+    IPropertyTree *options;
+    Owned<IGroup> group, spareGroup;
+
+    bool checkIfNodeInUse(IpAddress &ip, bool includespares, StringBuffer &clustname)
+    {
+        SocketEndpoint ep(0,ip);
+        if (RANK_NULL != group->rank(ep)) {
+            clustname.append(groupName);
+            return true;
+        }
+        else if (includespares) {
+            if (RANK_NULL != spareGroup->rank(ep)) {
+                clustname.append(groupName).append(" spares");
+                return true;
+            }
+        }
+        return false;
+    }
+    IPropertyTree *getSwapNodeInfo(bool create)
+    {
+        Owned<IRemoteConnection> conn = querySDS().connect("/SwapNode", myProcessSession(), RTM_LOCK_WRITE|(create?RTM_CREATE_QUERY:0), 1000*60*5);
+        if (!conn) {
+            ERRLOG("SWAPNODE: could not connect to /SwapNode branch");
+            return NULL;
+        }
+        StringBuffer xpath;
+        xpath.appendf("Thor[@group=\"%s\"]",groupName.get());
+        Owned<IPropertyTree> info = conn->queryRoot()->getPropTree(xpath.str());
+        if (!info) {
+            if (!create) {
+                PROGLOG("SWAPNODE: no information for group %s",groupName.get());
+                return NULL;
+            }
+            info.set(conn->queryRoot()->addPropTree("Thor",createPTree("Thor")));
+            info->setProp("@group",groupName.get());
+        }
+        return info.getClear();
+    }
+    bool doSwap(const char *oldip, const char *newip)
+    {
+        Owned<INode> newNode = createINode(newip);
+        Owned<INode> oldNode = createINode(oldip);
+        if (!group->isMember(oldNode)) {
+            ERRLOG("Node %s is not part of group %s", oldip, groupName.get());
+            return false;
+        }
+        if (group->isMember(newNode)) {
+            ERRLOG("Node %s is already part of group %s", newip, groupName.get());
+            return false;
+        }
+        queryNamedGroupStore().swapNode(oldNode->endpoint(),newNode->endpoint());
+        return true;
+    }
+    bool doSingleSwapNode(const char *oldip,const char *newip,unsigned nodenum,IPropertyTree *info,const char *timechecked)
+    {
+        if (doSwap(oldip,newip)) {
+            if (info) {
+                StringBuffer times(timechecked);
+                if (times.length()==0) {
+                    CDateTime dt;
+                    dt.setNow();
+                    dt.getString(times);
+                }
+                // TBD tie up with bad node in auto?
+
+                IPropertyTree *swap = info->addPropTree("Swap",createPTree("Swap"));
+                swap->setProp("@inNetAddress",newip);
+                swap->setProp("@outNetAddress",oldip);
+                swap->setProp("@time",times.str());
+                if (UINT_MAX != nodenum)
+                    swap->setPropInt("@rank",nodenum-1);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    void init()
+    {
+        StringBuffer xpath("Software/ThorCluster[@name=\"");
+        xpath.append(clusterName).append("\"]");
+        Owned<IRemoteConnection> conn = querySDS().connect("/Environment", myProcessSession(), RTM_LOCK_READ, SDS_LOCK_TIMEOUT);
+        environment.setown(createPTreeFromIPT(conn->queryRoot()));
+        options = environment->queryPropTree(xpath.str());
+        if (!options)
+            throwUnexpected();
+        groupName.set(options->queryProp("@nodeGroup"));
+        if (groupName.isEmpty())
+            groupName.set(options->queryProp("@name"));
+        VStringBuffer spareS("%s_spares", groupName.get());
+        spareGroupName.set(spareS);
+        group.setown(queryNamedGroupStore().lookup(groupName));
+        spareGroup.setown(queryNamedGroupStore().lookup(spareGroupName));
+    }
+public:
+    CSwapNode(const char *_clusterName) :clusterName(_clusterName)
+    {
+        init();
+    }
+    void swappedList(unsigned days, StringBuffer *out)
+    {
+        Owned<IPropertyTree> info = getSwapNodeInfo(true); // should put out error if returns false
+        if (!info.get())
+            return;
+        CDateTime tt;
+        CDateTime cutoff;
+        if (days) {
+            cutoff.setNow();
+            cutoff.adjustTime(-60*24*(int)days);
+        }
+        Owned<IPropertyTreeIterator> it2 = info->getElements("Swap");
+        ForEach(*it2) {
+            IPropertyTree &swappednode = it2->query();
+            const char *ts = swappednode.queryProp("@time");
+            if (!ts)
+                continue;
+            if (days) {
+                tt.setString(ts);
+                if (cutoff.compare(tt)>0)
+                    continue;
+            }
+            const char *ips = swappednode.queryProp("@outNetAddress");
+            if (!ips||!*ips)
+                continue;
+            IpAddress ip(ips);
+            StringBuffer clustname;
+            if (checkIfNodeInUse(ip,true,clustname))
+                continue; // ignore
+            if (out)
+                out->append(ips).append('\n');
+            else
+                PROGLOG("%s",ips);
+        }
+    }
+    void emailSwap(const char *msg, bool warn=false, bool sendswapped=false, bool sendhistory=false)
+    {
+        StringBuffer emailtarget;
+        StringBuffer smtpserver;
+        if (options->getProp("SwapNode/@EmailAddress",emailtarget)&&emailtarget.length()&&options->getProp("SwapNode/@EmailSMTPServer",smtpserver)&&smtpserver.length()) {
+            const char * subject = options->queryProp("SwapNode/@EmailSubject");
+            if (!subject)
+                subject = "SWAPNODE automated email";
+            StringBuffer msgs;
+            if (!msg) {
+                msgs.append("Swapnode command line, Cluster: ");
+                msg = msgs.append(groupName).append('\n').str();
+            }
+            CDateTime dt;
+            dt.setNow();
+            StringBuffer out;
+            dt.getString(out,true).append(": ").append(msg).append("\n\n");
+            if (options->getPropBool("SwapNode/@EmailSwappedList")||sendswapped) {
+                out.append("Currently swapped out nodes:\n");
+                swappedList(0,&out);
+                out.append('\n');
+            }
+            if (options->getPropBool("SwapNode/@EmailHistory")||sendhistory) {
+                out.append("Swap history:\n");
+                swapNodeHistory(0,&out);
+                out.append('\n');
+            }
+            SocketEndpoint ep(smtpserver.str(),25);
+            StringBuffer sender("swapnode@");
+            queryHostIP().getIpText(sender);
+            // add tbd
+            StringBuffer ips;
+            StringArray warnings;
+            sendEmail(emailtarget.str(),subject,out.str(),ep.getIpText(ips).str(),ep.port,sender.str(),&warnings);
+            ForEachItemIn(i,warnings)
+                WARNLOG("SWAPNODE: %s",warnings.item(i));
+        }
+        else if (warn)
+            WARNLOG("Either SwapNode/@EmailAddress or SwapNode/@EmailSMTPServer not set in thor.xml");
+    }
+    void swapNodeHistory(unsigned days,StringBuffer *out)
+    {
+        Owned<IPropertyTree> info = getSwapNodeInfo(true);
+        if (!info.get()) {
+            if (out)
+                out->append("No swapnode info\n");
+            else
+                ERRLOG("No swapnode info");
+            return;
+        }
+        StringBuffer line;
+        CDateTime tt;
+        CDateTime cutoff;
+        if (days) {
+            cutoff.setNow();
+            cutoff.adjustTime(-60*24*(int)days);
+        }
+        unsigned i=0;
+        if (out)
+            out->append("Failure, Time, NodeNum, NodeIp, ErrCode, Error Message\n------------------------------------------------------\n");
+        else {
+            PROGLOG("Failure, Time, NodeNum, NodeIp, ErrCode, Error Message");
+            PROGLOG("------------------------------------------------------");
+        }
+        Owned<IPropertyTreeIterator> it1 = info->getElements("BadNode");
+        ForEach(*it1) {
+            IPropertyTree &badnode = it1->query();
+            const char *ts = badnode.queryProp("@time");
+            if (!ts)
+                continue;
+            if (days) {
+                tt.setString(ts);
+                if (cutoff.compare(tt)>0)
+                    continue;
+            }
+            line.clear().append(++i).append(", ");
+            line.append(ts).append(", ").append(badnode.getPropInt("@rank",-1)+1).append(", ");
+            badnode.getProp("@netAddress",line);
+            line.append(", ").append(badnode.getPropInt("@code")).append(", \"");
+            badnode.getProp(NULL,line);
+            line.append('\"');
+            if (out)
+                out->append(line).append('\n');
+            else
+                PROGLOG("%s",line.str());
+        }
+        if (out)
+            out->append("\nSwapped, Time, NodeNum, OutIp, InIp\n-----------------------------------\n");
+        else {
+            PROGLOG("%s", "");
+            PROGLOG("Swapped, Time, NodeNum, OutIp, InIp");
+            PROGLOG("-----------------------------------");
+        }
+        i = 0;
+        Owned<IPropertyTreeIterator> it2 = info->getElements("Swap");
+        ForEach(*it2) {
+            IPropertyTree &swappednode = it2->query();
+            const char *ts = swappednode.queryProp("@time");
+            if (!ts)
+                continue;
+            if (days) {
+                tt.setString(ts);
+                if (cutoff.compare(tt)>0)
+                    continue;
+            }
+            line.clear().append(++i).append(", ");
+            swappednode.getProp("@time",line);
+            line.append(", ").append(swappednode.getPropInt("@rank",-1)+1).append(", ");
+            swappednode.getProp("@outNetAddress",line);
+            line.append(", ");
+            swappednode.getProp("@inNetAddress",line);
+            if (out)
+                out->append(line.str()).append('\n');
+            else
+                PROGLOG("%s",line.str());
+        }
+    }
+    bool checkThorNodeSwap(const char *failedwuid, unsigned mininterval)
+    {
+        bool ret = false;
+        if (mininterval==(unsigned)-1) { // called by thor
+            mininterval = 0;
+            if (!options||!options->getPropBool("SwapNode/@autoSwapNode"))
+                return false;
+            if ((!failedwuid||!*failedwuid)&&!options->getPropBool("SwapNode/@checkAfterEveryJob"))
+                return false;
+        }
+
+        try {
+            Owned<IPropertyTree> info = getSwapNodeInfo(true);
+            if (info.get()) {
+                PROGLOG("checkNodeSwap started");
+                StringBuffer xpath;
+                CDateTime dt;
+                StringBuffer ts;
+                // see if done less than mininterval ago
+                if (mininterval) {
+                    dt.setNow();
+                    dt.adjustTime(-((int)mininterval));
+                    if (info->getProp("@timeChecked",ts)) {
+                        CDateTime dtc;
+                        dtc.setString(ts.str());
+                        if (dtc.compare(dt,false)>0) {
+                            PROGLOG("checkNodeSwap using cached validate from %s",ts.str());
+                            xpath.clear().appendf("BadNode[@timeChecked=\"%s\"]",ts.str());
+                            return info->hasProp(xpath.str());
+                        }
+                    }
+                }
+
+                Owned<IGroup> grp = queryNamedGroupStore().lookup(groupName);
+                if (!grp)
+                    PROGLOG("%s group doesn't exist", groupName.get());
+                else
+                {
+                    SocketEndpointArray epa;
+                    grp->getSocketEndpoints(epa);
+                    ForEachItemIn(i1,epa) {
+                        epa.item(i1).port = getDaliServixPort();
+                    }
+                    SocketEndpointArray failures;
+                    UnsignedArray failedcodes;
+                    StringArray failedmessages;
+                    unsigned start = msTick();
+
+                    const char *thorname = options->queryProp("@name");
+                    StringBuffer dataDir, mirrorDir;
+                    getConfigurationDirectory(environment->queryPropTree("Software/Directories"),"data","thor",thorname,dataDir); // if not defined can't check
+                    getConfigurationDirectory(environment->queryPropTree("Software/Directories"),"mirror","thor",thorname,mirrorDir); // if not defined can't check
+
+                    validateNodes(epa,dataDir.str(),mirrorDir.str(),false,options->queryProp("SwapNode/@swapNodeCheckScript"),options->getPropInt("SwapNode/@swapNodeCheckScriptTimeout")*1000,failures,failedcodes,failedmessages);
+
+                    dt.setNow();
+                    dt.getString(ts.clear());
+                    ForEachItemIn(i,failures) {
+                        SocketEndpoint ep(failures.item(i));
+                        ep.port = 0;
+                        StringBuffer ips;
+                        ep.getIpText(ips);
+                        int r = (int)grp->rank(ep);
+                        if (r<0) {  // shouldn't occur
+                            ERRLOG("SWAPNODE node %s not found in group %s",ips.str(),groupName.get());
+                            continue;
+                        }
+                        PROGLOG("CheckSwapNode FAILED(%d) %s : %s",failedcodes.item(i),ips.str(),failedmessages.item(i));
+                        // SNMP TBD?
+
+                        ret = true;
+                        xpath.clear().appendf("BadNode[@netAddress=\"%s\"]",ips.str());
+                        IPropertyTree *bnt = info->queryPropTree(xpath.str());
+                        if (!bnt) {
+                            bnt = info->addPropTree("BadNode",createPTree("BadNode"));
+                            bnt->setProp("@netAddress",ips.str());
+                        }
+                        bnt->setPropInt("@numTimes",bnt->getPropInt("@numTimes",0)+1);
+                        bnt->setProp("@timeChecked",ts.str());
+                        bnt->setProp("@time",ts.str());
+                        bnt->setPropInt("@code",failedcodes.item(i));
+                        bnt->setPropInt("@rank",r);
+                        bnt->setProp(NULL,failedmessages.item(i));
+                    }
+                    if (failedwuid&&*failedwuid) {
+                        xpath.clear().appendf("WorkUnit[@id=\"%s\"]",failedwuid);
+                        IPropertyTree *wut = info->queryPropTree(xpath.str());
+                        if (!wut) {
+                            wut = info->addPropTree("WorkUnit",createPTree("WorkUnit"));
+                            wut->setProp("@id",failedwuid);
+                        }
+                        wut->setProp("@time",ts.str());
+                    }
+                    PROGLOG("checkNodeSwap: Time taken = %dms",msTick()-start);
+                    info->setProp("@timeChecked",ts.str());
+                }
+            }
+        }
+        catch (IException *e) {
+            EXCLOG(e,"checkNodeSwap");
+        }
+        return ret;
+    }
+};
+
+void swappedList(const char *clusterName, unsigned days, StringBuffer *out)
+{
+    CSwapNode swapNode(clusterName);
+    swapNode.swappedList(days, out);
+}
+
+void emailSwap(const char *clusterName, const char *msg, bool warn, bool sendswapped, bool sendhistory)
+{
+    CSwapNode swapNode(clusterName);
+    swapNode.emailSwap(msg, warn, sendswapped, sendhistory);
+}
+
+void swapNodeHistory(const char *clusterName, unsigned days, StringBuffer *out)
+{
+    CSwapNode swapNode(clusterName);
+    swapNode.swapNodeHistory(days, out);
+}
+
+bool checkThorNodeSwap(const char *clusterName, const char *failedwuid, unsigned mininterval)
+{
+    CSwapNode swapNode(clusterName);
+    return swapNode.checkThorNodeSwap(failedwuid, mininterval);
+}
+
+
+class CSingleSwapNode : public CSwapNode
+{
+public:
+    CSingleSwapNode(const char *clusterName) : CSwapNode(clusterName)
+    {
+    }
+    bool swap(const char *oldip, const char *newip)
+    {
+        ensureThorIsDown(clusterName,false,false);
+
+        Owned<IPropertyTree> info = getSwapNodeInfo(true);
+        if (!doSingleSwapNode(oldip,newip,UINT_MAX,info,NULL))
+            return false;
+        // check to see if it was a spare and remove
+        SocketEndpoint spareEp(newip);
+        rank_t r = spareGroup->rank(spareEp);
+        if (RANK_NULL != r)
+        {
+            PROGLOG("Removing spare : %s", newip);
+            spareGroup.setown(spareGroup->remove(r));
+            queryNamedGroupStore().add(spareGroupName, spareGroup); // NB: replace
+        }
+
+        info.clear();
+
+        PROGLOG("SwapNode finished");
+
+        return true;
+    }
+};
+
+bool swapNode(const char *cluster, const char *oldip, const char *newip)
+{
+    PROGLOG("SWAPNODE(%s,%s,%s) starting",cluster,oldip,newip);
+    CSingleSwapNode swapNode(cluster);
+    return swapNode.swap(oldip, newip);
+}
+
+
+class CAutoSwapNode : public CSwapNode
+{
+    bool doAutoSwapNode(bool dryRun=false)
+    {
+        if (!checkThorNodeSwap(NULL,dryRun?0:5)) {
+            PROGLOG("No bad nodes detected");
+            PROGLOG("SWAPNODE(auto) exiting");
+            return false;
+        }
+        Owned<IPropertyTree> info = getSwapNodeInfo(false);
+        if (!info.get()) {    // should put out error if returns false
+            PROGLOG("SWAPNODE(auto) exiting");
+            return false;
+        }
+        StringBuffer ts;
+        if (!info->getProp("@timeChecked",ts)) {
+            PROGLOG("SWAPNODE(auto): no check information generated");
+            return false;
+        }
+
+        // enumerate bad nodes
+        StringBuffer xpath;
+        xpath.appendf("BadNode[@time=\"%s\"]",ts.str());
+        Owned<IPropertyTreeIterator> it = info->getElements(xpath.str());
+        SocketEndpointArray epa1;
+        ForEach(*it) {
+            IPropertyTree &badnode = it->query();
+            const char *ip = badnode.queryProp("@netAddress");
+            if (!ip)
+                continue;
+            SocketEndpoint ep(ip);
+            ep.port = getDaliServixPort();
+            epa1.append(ep);
+        }
+        // recheck
+        SocketEndpointArray badepa;
+        UnsignedArray failedcodes;
+        StringArray failedmessages;
+        unsigned start = msTick();
+
+        const char *thorname = options->queryProp("@name");
+        StringBuffer dataDir, mirrorDir;
+        if (options->getPropBool("SwapNode/@swapNodeCheckPrimaryDrive",true))
+            getConfigurationDirectory(environment->queryPropTree("Software/Directories"),"data","thor",thorname,dataDir); // if not defined can't check
+        if (options->getPropBool("SwapNode/@swapNodeCheckMirrorDrive",true))
+            getConfigurationDirectory(environment->queryPropTree("Software/Directories"),"mirror","thor",thorname,mirrorDir); // if not defined can't check
+
+        validateNodes(epa1, dataDir.str(), mirrorDir.str(), false, options->queryProp("SwapNode/@swapNodeCheckScript"), options->getPropInt("SwapNode/@swapNodeCheckScriptTimeout")*1000, badepa, failedcodes, failedmessages);
+        if (!badepa.ordinality()) {
+            PROGLOG("SWAPNODE: on recheck all bad nodes passed (%s,%s)",groupName.get(),ts.str());
+            return false;
+        }
+        Owned<IGroup> grp = queryNamedGroupStore().lookup(groupName);
+        CDateTime dt;
+        dt.setNow();
+        dt.getString(ts.clear());
+        bool abort=false;
+        UnsignedArray badrank;
+        ForEachItemIn(i1,badepa) {
+            SocketEndpoint ep(badepa.item(i1));
+            ep.port = 0;    // should be no ports in group
+            StringBuffer ips;
+            ep.getIpText(ips);
+            xpath.clear().appendf("BadNode[@netAddress=\"%s\"]",ips.str());
+            IPropertyTree *bnt = info->queryPropTree(xpath.str());
+            if (!bnt) {
+                ERRLOG("SWAPNODE node %s not found in swapnode info!",ips.str());
+                return false;
+            }
+            bnt->setProp("@time",ts.str());
+            int r = bnt->getPropInt("@rank",-1);
+            if ((int)r<0) { // shouldn't occur
+                ERRLOG("SWAPNODE node %s rank not found in group %s",ips.str(),groupName.get());
+                return false;
+            }
+            badrank.append((unsigned)r);
+            for (unsigned j1=0;j1<i1;j1++) {
+                SocketEndpoint ep1(badepa.item(j1));
+                ep1.port = 0;   // should be no ports in group
+                int r1 = (int)badrank.item(j1);
+                if ((r==(r1+1)%grp->ordinality())||
+                    (r1==(r+1)%grp->ordinality())) {
+                    StringBuffer ips1;
+                    ep1.getIpText(ips1);
+                    ERRLOG("SWAPNODE adjacent nodes %d (%s) and %d (%s) are bad!",r+1,ips.str(),r1+1,ips1.str());
+                    abort = true;
+                }
+            }
+        }
+        // now see if any of bad nodes have been swapped out recently
+        CDateTime recent = dt;
+        int snint = options->getPropInt("SwapNode/@swapNodeInterval",24);
+        recent.adjustTime(-60*snint);
+        it.setown(info->getElements("Swap"));
+        ForEach(*it) {
+            IPropertyTree &swappednode = it->query();
+            CDateTime dt1;
+            const char *dt1s = swappednode.queryProp("@time");
+            if (!dt1s||!*dt1s)
+                continue;
+            dt1.setString(dt1s);
+            if (dt1.compare(recent)<0)
+                continue;
+            const char *ips = swappednode.queryProp("@outNetAddress");
+            if (!ips||!*ips)
+                continue;
+            int r1 = swappednode.getPropInt("@rank",-1);
+            SocketEndpoint swappedep(ips);
+            swappedep.port = 0;
+            ForEachItemIn(i2,badepa) {
+                SocketEndpoint badep(badepa.item(i2));
+                int badr = (int)badrank.item(i2);
+                badep.port = 0;
+                if (swappedep.equals(badep)) {
+                    // not sure if *really* want this
+                    ERRLOG("Node %d (%s) was swapped out on %s (too recent)",badr+1,ips,dt1s);
+                    abort = true;
+                }
+                else if ((badr==(r1+1)%grp->ordinality())||
+                    (r1==(badr+1)%grp->ordinality())) {
+                    StringBuffer bs;
+                    ERRLOG("SWAPNODE adjacent node to bad node %d (%s), %d (%s) was swapped on %s (too recent) !",badr+1,badep.getIpText(bs).str(),r1+1,ips,dt1s);
+                    abort = true;
+                }
+            }
+        }
+        const char *intent = dryRun?"would":"will";
+        // find spares
+        SocketEndpointArray spareepa;
+        StringArray swapfrom;
+        StringArray swapto;
+        Owned<IGroup> spareGroup;
+        if (!abort) {
+            spareGroup.setown(queryNamedGroupStore().lookup(spareGroupName));
+            if (!spareGroup) {
+                ERRLOG("SWAPNODE could not find spare group %s", spareGroupName.get());
+                abort = true;
+            }
+            else
+            {
+                spareGroup->getSocketEndpoints(spareepa);
+                ForEachItemIn(i3,badepa) {
+                    StringBuffer from;
+                    badepa.item(i3).getIpText(from);
+                    if (i3<spareepa.ordinality()) {
+                        StringBuffer to;
+                        spareepa.item(i3).getIpText(to);
+                        PROGLOG("SWAPNODE %s swap node %d from %s to %s",intent,badrank.item(i3)+1,from.str(),to.str());
+                    }
+                    else {
+                        abort = true;
+                        ERRLOG("SWAPNODE no spare available to swap for node %d (%s)",badrank.item(i3)+1,from.str());
+                    }
+                }
+            }
+        }
+        // now list what can do
+        if (abort) {
+            ERRLOG("SWAPNODE: problems found (listed above), no swap %s be attempted",intent);
+            return false;
+        }
+        if (dryRun)
+            return false;
+        // need to release swapnode lock for multi thor not to get deadlocked
+        info.clear(); // NB: This clears the connection to SwapNode
+        ensureThorIsDown(clusterName,true,true);
+        ForEachItemIn(i4,badepa) {
+            StringBuffer from;
+            badepa.item(i4).getIpText(from);
+            SocketEndpoint &spareEp = spareepa.item(i4);
+            StringBuffer to;
+            spareEp.getIpText(to);
+            rank_t r = spareGroup->rank(spareEp);
+            spareGroup.setown(spareGroup->remove(r));
+            queryNamedGroupStore().add(spareGroupName, spareGroup); // NB: replace
+            Owned<IPropertyTree> info = getSwapNodeInfo(false);
+            if (doSingleSwapNode(from.str(),to.str(),badrank.item(i4)+1,info,ts.str())) {
+                StringBuffer msg;
+                msg.appendf("AUTOSWAPNODE: cluster %s node %d: swapped out %s, swapped in %s",groupName.get(),badrank.item(i4)+1,from.str(),to.str());
+                emailSwap(msg.str());
+                FLLOG(MCoperatorError, swapnodeJob, "%s", msg.str());
+            }
+        }
+        return true;
+    }
+    void autoRestart()
+    {
+        // restarts any workunits that failed near to swap
+        // let see if need resubmit any nodes
+        StringArray toresubmit;
+        if (options->getPropBool("SwapNode/@swapNodeRestartJob")) {
+            Owned<IPropertyTree> info = getSwapNodeInfo(false); // should put out error if returns false
+            if (!info.get())
+            {
+                PROGLOG("SWAPNODE(autoRestart) exiting");
+                return;
+            }
+            CDateTime recent;
+            recent.setNow();
+            recent.adjustTime(-SWAPNODE_RETRY_TIME/(1000*60));
+            Owned<IPropertyTreeIterator> it = info->getElements("WorkUnit");
+            ForEach(*it) {
+                IPropertyTree &wu = it->query();
+                const char *wuid = wu.queryProp("@id");
+                if (!wuid)
+                    continue;
+                if (!wu.getPropBool("@resubmitted")) {
+                    // see if any swaps recently done
+                    const char *dt1s = wu.queryProp("@time");
+                    if (!dt1s||!*dt1s)
+                        continue;
+                    CDateTime dt1;
+                    dt1.setString(dt1s);
+                    dt1.adjustTime(SWAPNODE_RETRY_TIME/(1000*60));
+                    Owned<IPropertyTreeIterator> swit = info->getElements("Swap");
+                    ForEach(*swit) {
+                        IPropertyTree &swap = swit->query();
+                        const char *dt2s = swap.queryProp("@time");
+                        if (!dt2s||!*dt2s)
+                            continue;
+                        CDateTime dt2;
+                        dt2.setString(dt2s);
+                        if ((dt2.compare(recent)>0)&&(dt1.compare(dt2)>0)) {
+                            wu.setPropBool("@resubmitted",true); // only one attempt
+                            toresubmit.append(wuid);
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+        ForEachItemIn(ir,toresubmit) {
+            WuResubmit(toresubmit.item(ir));
+        }
+    }
+public:
+    CAutoSwapNode(const char *clusterpName) : CSwapNode(clusterpName)
+    {
+    }
+public:
+    bool swap(bool dryRun)
+    {
+        PROGLOG("SWAPNODE(auto%s) starting",dryRun?",dryRun":"");
+
+        if (!doAutoSwapNode(dryRun)) // using info in Dali (spares etc.)
+            return false;
+
+        autoRestart();
+
+        PROGLOG("AutoSwapNode finished");
+        return true;
+    }
+};
+
+bool autoSwapNode(const char *groupName, bool dryRun)
+{
+    CAutoSwapNode swapNode(groupName);
+    return swapNode.swap(dryRun);
+}

+ 49 - 0
tools/swapnode/swapnodelib.hpp

@@ -0,0 +1,49 @@
+/*##############################################################################
+
+    Copyright (C) 2011 HPCC Systems.
+
+    All rights reserved. This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as
+    published by the Free Software Foundation, either version 3 of the
+    License, or (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+############################################################################## */
+
+#ifndef _SWAPNODE_LIB_HPP
+#define _SWAPNODE_LIB_HPP
+
+#ifdef _WIN32
+ #ifdef SWAPNODELIB_EXPORTS
+  #define swapnodelib_decl __declspec(dllexport)
+ #else
+  #define swapnodelib_decl __declspec(dllimport)
+ #endif
+#else
+ #define swapnodelib_decl
+#endif
+
+interface IPropertyTree;
+extern swapnodelib_decl bool swapNode(const char *cluster, const char *oldIP, const char *newIP);
+extern swapnodelib_decl void emailSwap(const char *cluster, const char *msg, bool warn=false, bool sendswapped=false, bool sendhistory=false);
+
+// Called from swapnode and thor
+extern swapnodelib_decl bool checkThorNodeSwap(
+                                      const char *cluster,      // which cluster
+                                      const char *failedwuid,   // failed WUID or null if none
+                                      unsigned mininterval=0    // minimal interval before redoing check (mins)
+                                      ); // if returns true swap needed
+
+#ifdef ENABLE_AUTOSWAP
+extern swapnodelib_decl bool autoSwapNode(const char *cluster, bool dryrun);
+extern swapnodelib_decl void swapNodeHistory(const char *cluster, unsigned days, StringBuffer *out);
+extern swapnodelib_decl void swappedList(const char *cluster, unsigned days, StringBuffer *out);
+#endif
+
+#endif

+ 0 - 21
tools/swapnode/swapnodemain.cpp

@@ -1,21 +0,0 @@
-/*##############################################################################
-
-    Copyright (C) 2011 HPCC Systems.
-
-    All rights reserved. This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-############################################################################## */
-
-// Kludge as ESP seems to want some functions from swapnode
-#define _ESP
-#include "swapnode.cpp"

+ 0 - 34
tools/swapnode/swapnodemain.hpp

@@ -1,34 +0,0 @@
-/*##############################################################################
-
-    Copyright (C) 2011 HPCC Systems.
-
-    All rights reserved. This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Affero General Public License as
-    published by the Free Software Foundation, either version 3 of the
-    License, or (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Affero General Public License for more details.
-
-    You should have received a copy of the GNU Affero General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-############################################################################## */
-
-// main.h: interface for the SwapNodeLib class.
-//
-//////////////////////////////////////////////////////////////////////
-
-interface IProperties;
-
-extern void SwapNode(const char* Cluster, const char* OldIP, const char* NewIP,unsigned nodenu);
-
-
-
-#ifdef ENABLE_AUTOSWAP
-extern void autoSwapNode(IProperties *options,bool doswap);
-extern void swapNodeHistory(IProperties *options,unsigned days);
-extern void swappedList(IProperties *options,unsigned days);
-#endif
-