diff --git a/crm/cib/io.c b/crm/cib/io.c index 27c37087f7..a3a319a9b7 100644 --- a/crm/cib/io.c +++ b/crm/cib/io.c @@ -1,400 +1,400 @@ -/* $Id: io.c,v 1.13 2005/02/21 13:21:08 andrew Exp $ */ +/* $Id: io.c,v 1.14 2005/02/21 14:23:51 andrew Exp $ */ /* * Copyright (C) 2004 Andrew Beekhof * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This software 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 * General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include gboolean cib_writes_enabled = TRUE; const char * local_resource_path[] = { XML_CIB_TAG_STATUS, }; const char * resource_path[] = { XML_CIB_TAG_RESOURCES, }; const char * node_path[] = { XML_CIB_TAG_NODES, }; const char * constraint_path[] = { XML_CIB_TAG_CONSTRAINTS, }; gboolean initialized = FALSE; crm_data_t *the_cib = NULL; crm_data_t *node_search = NULL; crm_data_t *resource_search = NULL; crm_data_t *constraint_search = NULL; crm_data_t *status_search = NULL; /* * It is the callers responsibility to free the output of this function */ crm_data_t* readCibXml(char *buffer) { crm_data_t *root = string2xml(buffer); if (verifyCibXml(root) == FALSE) { free_xml(root); return createEmptyCib(); } return root; } /* * It is the callers responsibility to free the output of this function */ crm_data_t* readCibXmlFile(const char *filename) { int s_res = -1; struct stat buf; crm_data_t *root = NULL; if(filename != NULL) { s_res = stat(filename, &buf); } if (s_res == 0) { FILE *cib_file = fopen(filename, "r"); root = file2xml(cib_file); set_xml_property_copy(root, "generated", XML_BOOLEAN_FALSE); fclose(cib_file); } else { crm_warn("Stat of (%s) failed, file does not exist.", CIB_FILENAME); } if(root != NULL) { int lpc = 0; crm_data_t *status = get_object_root(XML_CIB_TAG_STATE, root); - for (; lpc < status->nfields; ) { + for (; status != NULL && lpc < status->nfields; ) { if(status->types[lpc] != FT_STRUCT) { lpc++; continue; } zap_xml_from_parent(status, status->values[lpc]); } } if (verifyCibXml(root) == FALSE) { free_xml(root); root = NULL; } return root; } /* * The caller should never free the return value */ crm_data_t* get_the_CIB(void) { return the_cib; } gboolean uninitializeCib(void) { crm_data_t *tmp_cib = the_cib; if(tmp_cib == NULL) { crm_err("The CIB has already been deallocated."); return FALSE; } initialized = FALSE; the_cib = NULL; node_search = NULL; resource_search = NULL; constraint_search = NULL; status_search = NULL; crm_err("Deallocating the CIB."); free_xml(tmp_cib); crm_err("The CIB has been deallocated."); return TRUE; } /* * This method will not free the old CIB pointer or the new one. * We rely on the caller to have saved a pointer to the old CIB * and to free the old/bad one depending on what is appropriate. */ gboolean initializeCib(crm_data_t *new_cib) { #if 0 if(new_cib != NULL) { crm_set_element_parent(new_cib, NULL); } #endif if (verifyCibXml(new_cib)) { initialized = FALSE; the_cib = new_cib; /* update search paths */ /* not used yet... node_search = get_object_root(XML_CIB_TAG_NODES, new_cib); resource_search = get_object_root(XML_CIB_TAG_RESOURCES, new_cib); constraint_search = get_object_root(XML_CIB_TAG_CONSTRAINTS, new_cib); status_search = get_object_root(XML_CIB_TAG_STATUS, new_cib); */ initialized = TRUE; crm_trace("CIB initialized"); return TRUE; } if(initialized == FALSE) { crm_warn("CIB Verification failed"); } else { const char *option = "suppress_cib_writes"; const char *value = NULL; crm_data_t *config = get_object_root( XML_CIB_TAG_CRMCONFIG, new_cib); crm_data_t * a_default = find_entity( config, XML_CIB_TAG_NVPAIR, option, FALSE); if(a_default != NULL) { value = crm_element_value( a_default, XML_NVPAIR_ATTR_VALUE); } cib_writes_enabled = TRUE; if(value == NULL) { crm_warn("Option %s not set", option); } else if(safe_str_eq(value, XML_BOOLEAN_TRUE)) { cib_writes_enabled = FALSE; } if(cib_writes_enabled) { crm_info("CIB disk writes to %s enabled", CIB_FILENAME); } else { crm_notice("Disabling CIB disk writes"); } } return initialized; } int moveFile(const char *oldname, const char *newname, gboolean backup, char *ext) { /* move 'oldname' to 'newname' by creating a hard link to it * and then removing the original hard link */ int res = 0; struct stat tmp; int s_res = stat(newname, &tmp); if (s_res >= 0) { if (backup == TRUE) { char backname[1024]; static const char *back_ext = "bak"; if (ext != NULL) { back_ext = (char*)ext; } snprintf(backname, sizeof(backname)-1, "%s.%s", newname, back_ext); moveFile(newname, backname, FALSE, NULL); } else { res = unlink(newname); if (res < 0) { perror("Could not remove the current backup of Cib"); return -1; } } } s_res = stat(oldname, &tmp); if (s_res >= 0) { res = link(oldname, newname); if (res < 0) { perror("Could not create backup of current Cib"); return -2; } res = unlink(oldname); if (res < 0) { perror("Could not unlink the current Cib"); return -3; } } return 0; } int activateCibBuffer(char *buffer, const char *filename) { int result = -1; crm_data_t *local_cib = NULL; local_cib = readCibXml(buffer); result = activateCibXml(local_cib, filename); return result; } /* * This method will free the old CIB pointer on success and the new one * on failure. */ int activateCibXml(crm_data_t *new_cib, const char *filename) { int error_code = cib_ok; crm_data_t *saved_cib = get_the_CIB(); const char *filename_bak = CIB_BACKUP; /* calculate */ crm_xml_devel(new_cib, "Attempting to activate CIB"); CRM_ASSERT(new_cib != saved_cib); crm_validate_data(new_cib); if(saved_cib != NULL) { crm_validate_data(saved_cib); } if (initializeCib(new_cib) == FALSE) { crm_warn("Ignoring invalid or NULL Cib"); error_code = -5; } else if(cib_writes_enabled) { if(saved_cib != NULL) { CRM_DEV_ASSERT(0 >= moveFile(filename, filename_bak, FALSE, NULL)); if (crm_assert_failed) { crm_warn("Could not make backup of the current" " Cib... aborting update."); error_code = -1; } } if(error_code == cib_ok) { crm_devel("Writing CIB out to %s", CIB_FILENAME); CRM_DEV_ASSERT(write_xml_file( new_cib, CIB_FILENAME) >= 0); if (crm_assert_failed) { error_code = -4; } } if(error_code == -4 && saved_cib != NULL) { CRM_DEV_ASSERT(moveFile(filename_bak, filename, FALSE, NULL) >= 0); if (crm_assert_failed){ crm_crit("Could not restore the backup of the " " current Cib... panic!"); error_code = -2; /* should probably exit here */ } } CRM_DEV_ASSERT(saved_cib != NULL || error_code == cib_ok); if(crm_assert_failed) { /* oh we are so dead */ crm_crit("Could not write out new CIB and no saved" " version to revert to"); error_code = -3; } else if(error_code != cib_ok) { crm_crit("Update of Cib failed (%d)... reverting" " to last known valid version", error_code); CRM_DEV_ASSERT(initializeCib(saved_cib)); if (crm_assert_failed) { /* oh we are so dead */ crm_crit("Could not re-initialize with the old" " CIB. Can anyone say corruption?"); error_code = -3; } } } /* Make sure memory is cleaned up appropriately */ if (error_code != cib_ok) { crm_trace("Freeing new CIB %p", new_cib); free_xml(new_cib); } else if(saved_cib != NULL) { crm_trace("Freeing saved CIB %p", saved_cib); crm_validate_data(saved_cib); free_xml(saved_cib); } return error_code; } diff --git a/cts/CM_LinuxHAv2.py.in b/cts/CM_LinuxHAv2.py.in index 1c773ac008..3600a09200 100755 --- a/cts/CM_LinuxHAv2.py.in +++ b/cts/CM_LinuxHAv2.py.in @@ -1,555 +1,558 @@ #!@PYTHON@ '''CTS: Cluster Testing System: LinuxHA v2 dependent modules... ''' __copyright__=''' Author: Huang Zhen Copyright (C) 2004 International Business Machines Additional Audits: Andrew Beekhof ''' # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. import CTS from CTS import * from CM_hb import HeartbeatCM from xml.dom.minidom import * import CTSaudits from CTSaudits import ClusterAudit import CTStests from CTStests import * ####################################################################### # # LinuxHA v2 dependent modules # ####################################################################### class LinuxHAv2(HeartbeatCM): ''' The linux-ha version 2 cluster manager class. It implements the things we need to talk to and manipulate linux-ha version 2 clusters ''' def __init__(self, Environment, randseed=None): HeartbeatCM.__init__(self, Environment, randseed=randseed) self.update({ "Name" : "linux-ha-v2", "DeadTime" : 90, "StartCmd" : "@libdir@/heartbeat/heartbeat >/dev/null 2>&1", "StopCmd" : "@libdir@/heartbeat/heartbeat -k", "StatusCmd" : "@libdir@/heartbeat/crmadmin -S %s 2>/dev/null", "EpocheCmd" : "@libdir@/heartbeat/ccm_epoche", "IsRscRunning" : "@libdir@/heartbeat/lrmadmin -E %s status 0 0 EVERYTIME 2>/dev/null|grep return", "IsIPAddrRscRunning" : "", "ExecuteRscOp" : "@libdir@/heartbeat/lrmadmin -E %s %s 0 0 EVERYTIME 2>/dev/null", "CIBfile" : "%s:@HA_VARLIBDIR@/heartbeat/crm/cib.xml", # Patterns to look for in the log files for various occasions... "Pat:We_started" : " %s crmd: .* State transition .*-> (S_NOT_DC|S_IDLE)", "Pat:They_started" : " %s crmd: .* State transition .*-> (S_NOT_DC|S_IDLE)", # Bad news Regexes. Should never occur. "BadRegexes" : ( r"Shutting down\.", r"Forcing shutdown\.", r"Timer I_TERMINATE just popped", r"Both machines own .* resources!", r"No one owns .* resources!", r", exiting\.", r"ERROR:", r"CRIT:", ), }) self.default_cts_cib=''' ''' # KLUDGE! Expedient, but a Kludge (FIXME) # CTStests.AllTestClasses = [FlipTest,RestartTest,StartOnebyOne,SimulStart,SimulStop,Split_brainTest,BandwidthTest] CTStests.AllTestClasses = [FlipTest, RestartTest, StartOnebyOne, SimulStart, SimulStop] # CTSaudits.AllAuditClasses = [CrmdStateAudit, HAResourceAudit] CTSaudits.AllAuditClasses = [CrmdStateAudit, DcAudit, DcIPaddrAudit] def StataCM(self, node): '''Report the status of the cluster manager on a given node''' out=self.rsh.readaline(node, self["StatusCmd"]%node) ret= (string.find(out, 'ok') != -1) try: if ret: if self.ShouldBeStatus[node] != self["up"]: self.log( "Node status for %s is %s but we think it should be %s" % (node, self["up"], self.ShouldBeStatus[node])) else: if self.ShouldBeStatus[node] != self["down"]: self.log( "Node status for %s is %s but we think it should be %s" % (node, self["down"], self.ShouldBeStatus[node])) except KeyError: pass if ret: self.ShouldBeStatus[node]=self["up"] else: self.ShouldBeStatus[node]=self["down"] return ret def StartaCM(self, node): '''Start up the cluster manager on a given node''' watch = CTS.LogWatcher(self["LogFileName"] , [self["Pat:We_started"]%node] , 60) watch.setwatch() self.log ("CM_LinuxHAv2.py: Starting %s on node %s" %(self["Name"], node)) if self.Env["ClobberCIB"] != None: if self.Env["CIBfilename"] == None: os.system("rm -f /tmp/cts.default.cib") - self.log("Installing default CIB") os.system("echo \'" + self.default_cts_cib + "\' > /tmp/cts.default.cib") self.rsh.cp("/tmp/cts.default.cib", self["CIBfile"]%node) os.system("rm -f /tmp/cts.default.cib") else: - self.log("Installing CIB from %s" %(self.Env["CIBfilename"])) self.rsh.cp(self.Env["CIBfilename"], self["CIBfile"]%node) self.rsh(node, self["StartCmd"]) if watch.look(): self.ShouldBeStatus[node]=self["up"] return 1 - out = self.CM.rsh.readaline(node, self.CM["StatusCmd"]) + out = self.rsh.readaline(node, self["StatusCmd"]) if string.find(out, 'ok') == -1: self.ShouldBeStatus[node]=self["down"] self.log ("Could not start %s on node %s" % (self["Name"], node)) else: self.ShouldBeStatus[node]=self["up"] self.log ("%s only partially started on node %s" % (self["Name"], node)) return None def Configuration(self): if not self.rsh.cp(self["CIBfile"]%self.Env["nodes"][0],self.Env["HAdir"]): raise ValueError("Can not copy file to %s, maybe permission denied"%self.Env["HAdir"]) cib=parse("%s/cib.xml"%self.Env["HAdir"]) return cib.getElementsByTagName('configuration')[0] def Resources(self): ResourceList = [] #read resources in cib configuration=self.Configuration() resources=configuration.getElementsByTagName('resources')[0] rscs=configuration.getElementsByTagName('resource') for rsc in rscs: ResourceList.append(HAResource(self,rsc)) return ResourceList def Dependancies(self): DependancyList = [] #read dependancy in cib configuration=self.Configuration() constraints=configuration.getElementsByTagName('constraints')[0] rsc_to_rscs=configuration.getElementsByTagName('rsc_to_rsc') for node in rsc_to_rscs: dependancy = {} dependancy["id"]=node.getAttribute('id') dependancy["from"]=node.getAttribute('from') dependancy["to"]=node.getAttribute('to') dependancy["type"]=node.getAttribute('type') dependancy["strength"]=node.getAttribute('strength') DependancyList.append(dependancy) return DependancyList class HAResourceAudit(ClusterAudit): def __init__(self, cm): self.CM = cm def _RscRunningNodes(self, resource): ResourceNodes = [] for node in self.CM.Env["nodes"]: if self.CM.ShouldBeStatus[node] == self.CM["up"]: if resource.IsRunningOn(node): ResourceNodes.append(node) return ResourceNodes def __call__(self): self.CM.log ("Do Audit %s"%self.name()) passed = 1 NodeofRsc = {} #Make sure the resouces are running on one and only one node Resources = self.CM.Resources() for resource in Resources : RunningNodes = self._RscRunningNodes(resource) NodeofRsc[resource.rid]=RunningNodes if len(RunningNodes) == 0 : print resource.rid + " isn't running anywhere" passed = 0 if len(RunningNodes) > 1: print resource.rid + " is running more than once: " \ + str(RunningNodes) passed = 0 #Make sure the resouces with "must","placement" constraint are running on the same node Dependancies = self.CM.Dependancies() for dependancy in Dependancies: if dependancy["type"] == "placement" and dependancy["strength"] == "must": if NodeofRsc[dependancy["from"]] != NodeofRsc[dependancy["to"]]: print dependancy["from"] + " and " + dependancy["to"] + " should be run on same node" passed = 0 return passed def name(self): return "HAResourceAudit" class HAResource(Resource): def __init__(self, cm, node): ''' Get information from xml node ''' self.rid = node.getAttribute('id') self.rclass = node.getAttribute('class') self.rtype = node.getAttribute('type') self.rparameters = {} attributes = node.getElementsByTagName('instance_attributes')[0] parameters = node.getElementsByTagName('rsc_parameters')[0] nvpairs = node.getElementsByTagName('nvpair') for nvpair in nvpairs: name=nvpair.getAttribute('name') value=nvpair.getAttribute('value') self.rparameters[name]=value Resource.__init__(self, cm, self.rtype, self.rid) def IsRunningOn(self, nodename): ''' This member function returns true if our resource is running on the given node in the cluster. We call the status operation for the resource script. ''' out=self.CM.rsh.readaline(nodename, self.CM["IsRscRunning"]%self.rid) return re.search("0",out) def _ResourceOperation(self, operation, nodename): ''' Execute an operation on the resource ''' self.CM.rsh.readaline(nodename, self.CM["ExecuteRscOp"]%(self.rid,operation)) return self.CM.rsh.lastrc == 0 def Start(self, nodename): ''' This member function starts or activates the resource. ''' return self._ResourceOperation("start", nodename) def Stop(self, nodename): ''' This member function stops or deactivates the resource. ''' return self._ResourceOperation("stop", nodename) def IsWorkingCorrectly(self, nodename): return self._ResourceOperation("monitor", nodename) class CrmdStateAudit(ClusterAudit): def __init__(self, cm): self.CM = cm self.Stats = {"calls":0 , "success":0 , "failure":0 , "skipped":0 , "auditfail":0} def has_key(self, key): return self.Stats.has_key(key) def __setitem__(self, key, value): self.Stats[key] = value def __getitem__(self, key): return self.Stats[key] def incr(self, name): '''Increment (or initialize) the value associated with the given name''' if not self.Stats.has_key(name): self.Stats[name]=0 self.Stats[name] = self.Stats[name]+1 def __call__(self): self.CM.log ("Do Audit %s"%self.name()) passed = 1 dc_count = 0 up_count = 0 node_count = 0 up_are_down = 0 down_are_up = 0 slave_count = 0 unstable_count = 0 for node in self.CM.Env["nodes"]: out=self.CM.rsh.readaline(node, self.CM["StatusCmd"]%node) ret = (string.find(out, 'ok') != -1) node_count = node_count + 1 if ret: up_count = up_count + 1 if self.CM.ShouldBeStatus[node] == self.CM["down"]: self.CM.log( "Node %s %s when it should be %s" % (node, self.CM["up"], self.CM.ShouldBeStatus[node])) self.CM.ShouldBeStatus[node] = self.CM["up"] down_are_up = down_are_up + 1 ret= (string.find(out, 'S_NOT_DC') != -1) if ret: slave_count = slave_count + 1 else: ret= (string.find(out, 'S_IDLE') != -1) if ret: dc_count = dc_count + 1 else: unstable_count = unstable_count + 1 else: if self.CM.ShouldBeStatus[node] == self.CM["up"]: self.CM.log( "Node %s %s when it should be %s" % (node, self.CM["down"], self.CM.ShouldBeStatus[node])) self.CM.ShouldBeStatus[node] = self.CM["down"] up_are_down = up_are_down + 1 if up_count > 0 and dc_count != 1: passed = 0 self.CM.log("Exactly 1 node should be DC. We found %d (of %d)" %(dc_count, up_count)) if unstable_count > 0: passed = 0 self.CM.log("Cluster is not stable. We found %d (of %d) unstable nodes" %(unstable_count, up_count)) if up_are_down > 0: passed = 0 self.CM.log("%d (of %d) nodes expected to be up were down." %(up_are_down, node_count)) if down_are_up > 0: passed = 0 self.CM.log("%d (of %d) nodes expected to be down were up." %(down_are_up, node_count)) return passed def name(self): return "CrmdStateAudit" class DcIPaddrAudit(ClusterAudit): def __init__(self, cm): self.CM = cm self.Stats = {"calls":0 , "success":0 , "failure":0 , "skipped":0 , "auditfail":0} def has_key(self, key): return self.Stats.has_key(key) def __setitem__(self, key, value): self.Stats[key] = value def __getitem__(self, key): return self.Stats[key] def incr(self, name): '''Increment (or initialize) the value associated with the given name''' if not self.Stats.has_key(name): self.Stats[name]=0 self.Stats[name] = self.Stats[name]+1 def __call__(self): self.CM.log ("Do Audit %s"%self.name()) passed = 1 the_dc = self.find_dc() if the_dc == None: return passed #Make sure the resouces are running on one and only one node Resources = self.CM.Resources() for resource in Resources : if resource.rid == "DcIPaddr": if self.audit_ip_addr(resource, the_dc) == 0: passed = 0 return passed def is_node_dc(self, node): out=self.CM.rsh.readaline(node, self.CM["StatusCmd"]%node) return (string.find(out, 'S_IDLE') != -1) def audit_ip_addr(self, resource, node): self.CM.log ("Auditing %s"%(resource)) RunningNodes = self._RscRunningNodes(resource) if len(RunningNodes) == 0 : self.CM.log("%s is not running" %(resource)) + for node in self.CM.Env["nodes"]: + if self.CM.ShouldBeStatus[node] == self.CM["up"]: + out = self.CM.rsh.readaline(node, self.CM["StatusCmd"]%node) + self.CM.log("%s" %(out)) return 0 if len(RunningNodes) > 1: self.CM.log("%s is running more than once" %(resource)) for running_on in RunningNodes: if self.is_node_dc(running_on) == 0: self.CM.log("%s is running on a non-DC node %s" %(resource, running_on)) return 0 return 1 def name(self): return "DcIPaddrAudit" def find_dc(self): for node in self.CM.Env["nodes"]: if self.is_node_dc(node): return node return None def _RscRunningNodes(self, resource): ResourceNodes = [] for node in self.CM.Env["nodes"]: if resource.IsRunningOn(node): ResourceNodes.append(node) return ResourceNodes class DcAudit(ClusterAudit): def __init__(self, cm): self.CM = cm self.Stats = {"calls":0 , "success":0 , "failure":0 , "skipped":0 , "auditfail":0} self.NodeEpoche={} self.NodeState={} def has_key(self, key): return self.Stats.has_key(key) def __setitem__(self, key, value): self.Stats[key] = value def __getitem__(self, key): return self.Stats[key] def incr(self, name): '''Increment (or initialize) the value associated with the given name''' if not self.Stats.has_key(name): self.Stats[name]=0 self.Stats[name] = self.Stats[name]+1 def __call__(self): self.CM.log ("Do Audit %s"%self.name()) passed = 0 lowest_epoche = None nodes_up = 0 dc_allowed_list=[] for node in self.CM.Env["nodes"]: if self.CM.ShouldBeStatus[node] == self.CM["up"]: nodes_up = nodes_up + 1 self.NodeEpoche[node] = self.CM.rsh.readaline( node, self.CM["EpocheCmd"]) self.NodeState[node] = self.CM.rsh.readaline( node, self.CM["StatusCmd"]%node) if len(self.NodeState[node]) > 1: self.NodeState[node] = self.NodeState[node][:-1] if len(self.NodeEpoche[node]) > 1: self.NodeEpoche[node] = self.NodeEpoche[node][:-1] if lowest_epoche == None or self.NodeEpoche[node] < lowest_epoche: lowest_epoche = self.NodeEpoche[node] if nodes_up == 0: print ("No nodes running") return 1 for node in self.CM.Env["nodes"]: if self.CM.ShouldBeStatus[node] == self.CM["up"]: if self.NodeEpoche[node] == lowest_epoche: dc_allowed_list.append(node) for node in dc_allowed_list: if self.is_node_dc(self.NodeState[node]): passed = 1 if passed == 0: self.CM.log("DC not found on any of the %d allowed nodes: %s" %(len(dc_allowed_list), str(dc_allowed_list))) for node in self.CM.Env["nodes"]: if self.CM.ShouldBeStatus[node] == self.CM["up"]: - self.CM.log ("%s : %s : %s" %(node, self.NodeEpoche[node], self.NodeState[node])) + self.CM.log("epoche %s : %s" + %(self.NodeEpoche[node], self.NodeState[node])) return passed def is_node_dc(self, status_line): return (string.find(status_line, 'S_IDLE') != -1) def name(self): return "DcAudit" ####################################################################### # # A little test code... # # Which you are advised to completely ignore... # ####################################################################### if __name__ == '__main__': pass