######################################################################## # $Header: /var/local/cvsroot/4Suite/Ft/Server/Common/Install/InstallUtil.py,v 1.72 2005/04/07 00:18:47 jkloth Exp $ """ Definitions and functions used in the installation process Copyright 2004 Fourthought, Inc. (USA). Detailed license and copyright information: http://4suite.org/COPYRIGHT Project home, documentation, distributions: http://4suite.org/ """ import sys, os, sha, urllib, stat from Ft.Server.Common import Schema, AclConstants from Ft.Server.Common.ResourceTypes import ResourceType from Ft.Lib import Time, ProgressIndicator from distutils import file_util, dep_util from Ft.Xml import XPath, Domlette, ReaderException, EMPTY_NAMESPACE from Ft.Xml.Xslt import XSL_NAMESPACE from Ft.Lib import Uri from Ft.Lib import Resolvers from Ft.Xml import InputSource class StatusFlags: UNKNOWN = -1 NO_CHANGE = 0 NOT_PRESENT = 1 CONTENT_CHANGE = 2 IMT_CHANGE = 4 ACL_CHANGE = 8 OWNER_CHANGE = 16 OTHER_CHANGE = 32 ACTION_REQUIRED = NOT_PRESENT | CONTENT_CHANGE | IMT_CHANGE | ACL_CHANGE | OWNER_CHANGE | OTHER_CHANGE g_safeAclIdents = [AclConstants.SUPER_USER_GROUP_NAME, AclConstants.WORLD_GROUP_NAME, AclConstants.USERS_GROUP_NAME, AclConstants.ANONYMOUS_USER_NAME, AclConstants.OWNER] class IndicatorBase: indicator = None def _message(self, msg, quiet): if not quiet: if self.indicator: self.indicator.message(msg) else: print msg def _setIndicator(self, indicator): """ Store information about where we are in the indicator list """ self.indicator = indicator class RawFile(IndicatorBase): resourceType = ResourceType.RAW_FILE contentUri = None stringContent = None stringLastModified = None #specified in iso 8601 lmds = None owners = {} imts = {} def __init__(self, path, acl, owner, imt): self.path = path self.absPath = path + ';no-traverse' #Cache any changes to ACL self._aclToUpdate = {} self._aclToRemove = [] self.owner = owner self.imt = imt or 'text/plain' self.status = StatusFlags.UNKNOWN self.dependencies = [] if acl is None: acl = [] self._setAcl(acl) return def _setAcl(self, acl): """ Helper function for test harnesses only """ self.acl = acl self.aclDict = {} for access, ident, allowed in acl: if access not in self.aclDict: self.aclDict[access] = {} self.aclDict[access][ident] = allowed return def setPath(self, contentUri): self.contentUri = contentUri return def setSource(self, stringContent, stringLastModified): self.stringContent = stringContent self.stringLastModified = stringLastModified return def checkStatus(self, repo, force=0, checkAcl=1): """ Get the status of this resource from against the supplied repo, Note, force here specifies that we force update to the status. Not force install """ if not force and self.status != StatusFlags.UNKNOWN: return self.status self._initRdfCaches(repo) if self.path not in self.lmds: self.status = StatusFlags.ACTION_REQUIRED #Force the ACL changes self._aclToUpdate = self.aclDict #Exit here return self.status self.status = StatusFlags.NO_CHANGE # Compare the file times. localTime = None if self.contentUri is not None: urlfile = Uri.UrlOpen(self.contentUri) try: mtime = os.fstat(urlfile.fileno())[stat.ST_MTIME] except OSError: # Window's socket streams cannot be stat'ed # Get the last modified date from the headers, if possible modified = urlfile.info().getheader('Last-Modified') if modified: datetime = Time.FromRFC822(modified) else: # Unable to determine last modified date, use current # time to force an update datetime = Time.FromPythonTime() else: datetime = Time.FromPythonTime(mtime) urlfile.close() localTime = datetime.asISO8601DateTime() elif self.stringLastModified is not None: localTime = self.stringLastModified if localTime is not None: # Compare it with the repo. Some things (i.e., containers) don't # have a modified date check. if localTime > self.lmds[self.path]: self.status = self.status | StatusFlags.CONTENT_CHANGE # Check for other kinds of changes if checkAcl: self._checkAclChange(repo) self._checkImtChange(repo) self._checkOwnerChange(repo) self._checkOtherChange(repo) return self.status def createContent(self, repo, quiet): """ Called when we need to add our content to the repo """ self._message("Create RawFile: %s IMT=%s"%(self.path, self.imt), quiet) repo.createRawFile(self.path, self.imt, self._getContent()) #Don't need to reset this on a create self.status &= ~StatusFlags.IMT_CHANGE return def updateContent(self, repo, quiet): """ Called when we need to update our content in the repo """ self._message("Modify %s content"%self.path, quiet) repo.fetchResource(self.path).setContent(self._getContent()) return def updateImt(self, repo, quiet): """ Called when we need to update our imt in the repo """ self._message("Setting IMT of %s --> %s" % (self.path, self.imt), quiet) repo.fetchResource(self.absPath).setImt(self.imt) return def updateOwner(self, repo, quiet): """ Called when we need to update our owner in the repo """ self._message("Setting Owner of %s --> %s" % (self.path, self.owner), quiet) owner = repo.fetchUserOrGroupByName(self.owner) #Owner will be None if 'world' if owner: repo.fetchResource(self.absPath).setOwner(owner) return def updateAcl(self, repo, quiet): """ Called when we need to update our acl in the repo """ res = repo.fetchResource(self.absPath) for access in self._aclToRemove: self._message("Removing ACL: %s from %s" % (access, self.path), quiet) res.inheritAcl(access) for access, info in self._aclToUpdate.items(): self._message("Setting ACL: %s for %s" % (access, self.path), quiet) l = info.items() for ident, allowed in l[:1]: res.setAcl(access, ident, allowed) for ident, allowed in l[1:]: res.addAcl(access, ident, allowed) return def updateOther(self, repo, quiet): """ Called when we need to update our other stuff in the repo """ #The resource type might be changing, in which case we need to abort #the update, delete the existing resource, and install the new. #Aborting at this point could get ugly, so for now, just raise an #exception. FIXME! doc = repo.fetchResource(self.absPath) if doc.resourceType != self.resourceType: raise RuntimeError("Resource type change for %s not supported; please delete it first." % self.path) return def _getContent(self): if self.stringContent: return self.stringContent elif self.contentUri: urlfile = Uri.UrlOpen(self.contentUri) content = urlfile.read() urlfile.close() return content else: raise RuntimeError("Attempting to update %s with no content" % self.path) return def _checkAclChange(self, repo): """ Look for changes in the ACL """ res = repo.fetchResource(self.absPath) cur = res.getAcl() for access, info in self.aclDict.items(): if access not in cur: #Add the entire access self._aclToUpdate[access] = info else: #Verify all of the keys are the same for ident, allowed in info.items(): if cur[access].get(ident, -1) != allowed: #The access is the same, but the ident/allowed is different #Add the whole thing self._aclToUpdate[access] = info break else: #Make sure that cur doesn't define any extra idents for ident, allowed in cur[access].items(): if ident not in self.aclDict[access]: #It does so rewrite the entire access self._aclToUpdate[access] = info break #Lastly, make sure that cur does not define any other access for access in cur: if access not in self.aclDict: self._aclToRemove.append(access) if len(self._aclToUpdate) or len(self._aclToRemove): self.status |= StatusFlags.ACL_CHANGE return def _checkImtChange(self, repo): """ Look for changes in the IMT """ if self.imt and self.imt != self.imts[self.path]: self.status |= StatusFlags.IMT_CHANGE return def _checkOwnerChange(self, repo): """ Look for changes in the Owner """ if self.owner and self.owner != self.owners[self.path]: self.status |= StatusFlags.OWNER_CHANGE return def _checkOtherChange(self, repo): """ Look for other changes """ pass def _setBase(self, basePath): """ Adjust for a relative installation """ self.path = Uri.BaseJoin(basePath, self.path) self.absPath = self.path + ';no-traverse' return def _gatherDependencies(self, basePath): """ Get a list of dependencies for this resource. At the RawFile level, it is dependencies based on path """ self.dependencies = [] base = os.path.dirname(self.path) if not (base == basePath or base + '/' == basePath): self.dependencies.append(base) return def _initRdfCaches(self, repo): """ We use RDF completes to get a lot of the information about the resources. This speeds up installation a lot. """ if self.lmds is not None: return RawFile.lmds = {} for lmd in repo.getModel().complete(None, Schema.MODIFIED_DATE, None): self.lmds[lmd.subject] = lmd.object for imt in repo.getModel().complete(None, Schema.IMT, None): self.imts[imt.subject] = imt.object for owner in repo.getModel().complete(None, Schema.OWNER, None): self.owners[owner.subject] = owner.object return def __repr__(self): return "<%s to be installed at %s id=%s>" % (self.__class__.__name__, self.path, str(hex(id(self)))) class UriReferenceFile(RawFile): resourceType = ResourceType.URI_REFERENCE_FILE _refs = None def __init__(self, path, acl, owner, imt, href): self.href = href RawFile.__init__(self, path, acl, owner, imt) return def _checkOtherChange(self, repo): """ See if the doc def has changed """ RawFile._checkOtherChange(self, repo) if self.path not in self._refs or self.href != self._refs[self.path]: self.status |= StatusFlags.OTHER_CHANGE return def createContent(self, repo, quiet): """ Called when we need to add our content to the repo """ self._message("Create UriReferenceFile: %s IMT=%s reference=%s"%(self.path, self.imt, self.href), quiet) repo.createUriReferenceFile(self.path, self.imt, self.href) #Don't need to reset this on a create self.status &= ~StatusFlags.IMT_CHANGE return def updateContent(self, repo, quiet): """ Called when we need to update our content in the repo """ raise RuntimeError("Unable to update content for %s" % self.path) def _initRdfCaches(self, repo): RawFile._initRdfCaches(self, repo) if self._refs is not None: return UriReferenceFile._refs = {} for ref in repo.getModel().complete(None, Schema.URI_REFERENCE_LOCATION, None): self._refs[ref.subject] = ref.object return class XmlDocument(RawFile): resourceType = ResourceType.XML_DOCUMENT _dds = None docName='XmlDocument' def __init__(self, path, acl, owner, imt, docDef): RawFile.__init__(self, path, acl, owner, imt) self.docDef = docDef if imt is None: self.imt = 'text/xml' return def createContent(self, repo, quiet): """ Called when we need to add our content to the repo """ self._message("Create %s: %s IMT=%s%s" % (self.docName, self.path, self.imt, " docDef=" * (not not self.docDef) + (self.docDef or "")), quiet) cnt = self._getContent() repo.createDocument(self.path, cnt, docDef = self.docDef, imt=self.imt, forcedType = self.resourceType) #Don't need to reset this on a create self.status &= ~StatusFlags.IMT_CHANGE #Note, we need to be smart in updateOther cause we already set the doc def return def updateOther(self, repo, quiet): RawFile.updateOther(self, repo, quiet) doc = repo.fetchResource(self.absPath) curDD = doc.getDocumentDefinition() if (curDD is None and self.docDef is not None) or (curDD is not None and curDD.getAbsolutePath() != self.docDef): self._message("Setting %s document definition to %s" % (self.path, self.docDef), quiet) dd = None if self.docDef is not None: dd = doc.fetchResource(self.docDef) doc.setDocumentDefinition(dd) return def _setBase(self, basePath): """ Adjust for a relative installation """ RawFile._setBase(self, basePath) if self.docDef is not None: self.docDef = Uri.BaseJoin(basePath, self.docDef) return def _checkOtherChange(self, repo): """ See if the doc def has changed """ RawFile._checkOtherChange(self, repo) if self.path not in self._dds or self.docDef != self._dds[self.path]: self.status = self.status | StatusFlags.OTHER_CHANGE return def _getContent(self): # note this might be overridden by a descendant class if self.stringContent: return self.stringContent elif self.contentUri: urlfile = Uri.UrlOpen(self.contentUri) content = urlfile.read() urlfile.close() return content return def _gatherDependencies(self, basePath): RawFile._gatherDependencies(self, basePath) #Find dependencies from resolution needed during parsing #FIXME: Only works for one resolution dependency at a time # # the general idea here is to attempt to parse the XML. # if one of the document's external entities or xincludes # aren't readable, then the XML reader will report this as # a UriException from which we can usually get the URI of # whatever couldn't be read. self.cachedDoc = None resolve_requests = [] #Use a closure to log all requests to the resolver def resolve_hook(uri, base): resolve_requests.append(uri) resolver = Resolvers.FacadeResolver(observer=resolve_hook) factory = InputSource.InputSourceFactory(resolver=resolver) #Using NoExtDtdReaderBase is a cop out, basically, for the difficulty #Of determining how to dummy out reader = Domlette.NonvalidatingReaderBase(inputSourceFactory=factory) doc = self._getContent() if not doc: # nothing to parse, which is probably a problem, # but as far as dependency checking goes, is OK return if self.contentUri: uri = self.contentUri else: # if we don't have a real base URI for the content # then we won't be able to properly resolve any # relative URI refs of external entities / xincludes. # but we should try to parse anyway in order to detect # unreadable ext. entities / xincludes that are # referenced by absolute URIs. uri = 'urn:dummy:xml-string-content-for-%s' % self.path #Until parse is successful... while not self.cachedDoc: try: doc = reader.parseString(doc, uri) self.cachedDoc = doc except Uri.UriException, e: #print >> sys.stderr, 'dependency found:', e.params[0]['uri'], '\n', e.params code = e.errorCode if code == Uri.UriException.RESOURCE_ERROR: # 1st arg to the err msg is assumed to be a dict # in which the 'loc' item is the URI # of the unreadable entity or xinclude self.dependencies.append(e.params[0]['loc']) #Can't be '' in case it's to be used as stand-in for an #XInclude resolver.cache[e.params[0]['uri']] = '' elif code in (Uri.UriException.INVALID_BASE_URI, Uri.UriException.RELATIVE_BASE_URI, Uri.UriException.SCHEME_REQUIRED): # 1st arg to the err msg is assumed to be a dict # in which the 'ref' item is the URI ref of the # entity or xinclude that we couldn't attempt to # read, possibly due to a bad base URI self.dependencies.append( '%s - possibly bad a base URI %s' % (e.params[0]['ref'], e.params[0]['base'])) else: self.dependencies.append('an external entity or XInclude (URI unknown)') except ReaderException, e: if not resolver.cache.get(e.params[0]): #Either this didn't come from cached resolution (get() -> None) or #We've already fallen back to trying the empty/dummy resource (value == '') #So just abort raise else: resolver.cache[e.params[0]] = '' except: raise if self.docDef is not None: self.dependencies.append(self.docDef) #Everything encountered in the facade resolver is also a dependency #Comment out for now because the blasted dependency sorting logic is broken #self.dependencies.extend(resolve_requests) return def _initRdfCaches(self, repo): RawFile._initRdfCaches(self, repo) if self._dds is not None: return XmlDocument._dds = {} for dd in repo.getModel().complete(None, Schema.DOCDEF, None): if dd.object == Schema.NULL_DOCDEF: self._dds[dd.subject] = None else: self._dds[dd.subject] = dd.object return class Container(XmlDocument): resourceType = ResourceType.CONTAINER def __init__(self, path, acl, owner, imt, docDef): if path[-1] == '/' and len(path) > 1: path = path[:-1] XmlDocument.__init__(self, path, acl, owner, imt, docDef) return def createContent(self, repo, quiet): self._message("Create Container: %s" % self.path, quiet) repo.createContainer(self.path, 1) if self.imt == 'text/xml': self.status &= ~StatusFlags.IMT_CHANGE return def updateContent(self, repo, quiet): """ Called when we need to update our content in the repo """ raise RuntimeError("Unable to update content for %s" % self.path) from Ft.Share import ExtensionModules _EXT_NAME = ExtensionModules.__name__ _EXT_DIR = os.path.dirname(ExtensionModules.__file__) del ExtensionModules class ExtensionModule(IndicatorBase): resourceType = None owner=-1 def __init__(self, file): if not file: raise Exception('File attribute not given') self._file = Uri.UriToOsPath(file, attemptAbsolute=False) self.path = '' basename = os.path.basename(self._file) self._dest = os.path.join(_EXT_DIR, basename) self.moduleName = _EXT_NAME + '.' + os.path.splitext(basename)[0] self.status = StatusFlags.UNKNOWN return def createContent(self, repo, quiet): #repo.copyFile(open(self._file, 'r').read(), ) res = file_util.copy_file(self._file, self._dest, update=1, ) if res[1]: self._message("Copy extension module: %s" % self._file, quiet) return def updateContent(self, repo, quiet): self.createContent(repo, quiet) repo.reloadModule(self.moduleName) def checkStatus(self, repo, force=0, checkAcl = 1): self.status = StatusFlags.NO_CHANGE if os.path.exists(self._dest): if force or dep_util.newer(self._file, self._dest): self.status = StatusFlags.CONTENT_CHANGE else: self.status = StatusFlags.NOT_PRESENT return self.status def _setBase(self, basePath): return def _gatherDependencies(self, basePath): self.dependencies = [] def setPath(self, fName): pass class XsltDocument(XmlDocument): resourceType = ResourceType.XSLT_DOCUMENT dependencyXPath = XPath.Compile('/xsl:*/xsl:import | /xsl:*/xsl:include') docName = 'XsltDocument' def _gatherDependencies(self, basePath): """ The dependencies of a Xslt document are all xslt:include and xslt:import elements """ XmlDocument._gatherDependencies(self, basePath) if not self.cachedDoc: return con = XPath.Context.Context(self.cachedDoc, processorNss={'xsl': XSL_NAMESPACE}) for dep in self.dependencyXPath.evaluate(con): d = dep.getAttributeNS(EMPTY_NAMESPACE, 'href') if d: if ':' in d: #External to the system (not a safe assumption, really) continue if d[0] != '/': d = Uri.BaseJoin(self.path, d) self.dependencies.append(d) return class SchematronDocument(XmlDocument): resourceType = ResourceType.SCHEMATRON_DOCUMENT docName = 'SchematronDocument' class RdfDocument(XmlDocument): resourceType = ResourceType.RDF_DOCUMENT docName = 'RdfDocument' class XsltDocumentDefinition(XmlDocument): resourceType = ResourceType.XSLT_DOCUMENT_DEFINITION docName = 'XsltDocumentDefinition' class XPathDocumentDefinition(XmlDocument): resourceType = ResourceType.XPATH_DOCUMENT_DEFINITION docName = 'XPathDocumentDefinition' class Server(XmlDocument): resourceType = ResourceType.SERVER docName = 'Server' class Command(XmlDocument): resourceType = ResourceType.COMMAND docName = 'Command' class Alias(XmlDocument): resourceType = ResourceType.ALIAS _refs = None def __init__(self, path, acl, owner, imt, docDef, reference): self.reference = reference XmlDocument.__init__(self, path, acl, owner, imt, docDef) if self.reference[0] != '/': #Make it absolute self.reference = Uri.BaseJoin(self.path, self.reference) return def createContent(self, repo, quiet): """ Called when we need to add our content to the repo """ self._message("Create Alias: %s" % self.path, quiet) doc = repo.fetchResource(self.reference) doc.addAlias(self.path) if self.imt == 'text/xml': self.status &= ~StatusFlags.IMT_CHANGE #Note, we need to be smart in updateOther cause we already set the reference return def updateContent(self, repo, quiet): """ Called when we need to update our content in the repo """ raise RuntimeError("Unable to update content for %s" % self.path) def updateOther(self, repo, quiet): XmlDocument.updateOther(self, repo, quiet) #You cannot change an aliases reference, so raise exception for now, we could lose ACL, owner, etc if repo.fetchResource(self.absPath).getReference().getAbsolutePath() != self.reference: raise RuntimeError("Unable to change the alias reference for %s, please remove alias by hand. You can do so on the command line using '4ss delete --no-traverse %s'" % (self.path, self.path)) return def _checkOtherChange(self, repo): """ See if the doc def has changed """ XmlDocument._checkOtherChange(self, repo) if self.path not in self._refs or self.reference != self._refs[self.path]: self.status |= StatusFlags.OTHER_CHANGE return def _setBase(self, basePath): """ Adjust for a relative installation """ XmlDocument._setBase(self, basePath) self.reference = Uri.BaseJoin(self.path, self.reference) return def _gatherDependencies(self, basePath): XmlDocument._gatherDependencies(self, basePath) self.dependencies.append(self.reference) return def _initRdfCaches(self, repo): XmlDocument._initRdfCaches(self, repo) if self._refs is not None: return Alias._refs = {} for ref in repo.getModel().complete(None, Schema.ALIAS_REFERENCE, None): self._refs[ref.subject] = ref.object return class User(XmlDocument): resourceType = ResourceType.USER def __init__(self, userName, acl, owner, imt, docDef, basePath, password=None, data={}, passwdHash=None): XmlDocument.__init__(self, basePath + '/' + userName, acl, owner, imt, docDef) self.userName = userName self.basePath = basePath if passwdHash: self.passwordHash = passwdHash self.password = password else: self.passwordHash = sha.new(password).hexdigest() self.password = password self.data = data return def createContent(self, repo, quiet): """ Called when we need to add our content to the repo """ self._message("Create User: %s%s" % (self.path, " docDef=" * (not not self.docDef) + (self.docDef or "")), quiet) newUser=repo.fetchResource(self.basePath).createUser(self.userName, self.passwordHash, self.docDef) newUser.setUserData(self.data) #Note, we need to be smart in updateOther cause we already set the password if self.imt == 'text/xml': self.status &= ~StatusFlags.IMT_CHANGE return def updateContent(self, repo, quiet): """ Called when we need to update our content in the repo """ raise RuntimeError("Unable to update content for %s" % self.path) def updateOther(self, repo, quiet): XmlDocument.updateOther(self, repo, quiet) if repo.fetchResource(self.path).getPassword() != self.passwordHash: self._message("Updating User %s password" % self.userName, quiet) repo.fetchResource(self.path).setPassword(self.passwordHash) if repo.fetchResource(self.path).getUserData() != self.data: self._message("Updating User %s's data" % self.userName, quiet) repo.fetchResource(self.path).setUserData(self.data) return def _checkAclChange(self, repo): #Users get thier ACL set automatically by the system. We need to account for that here. #So, we need to update the ACL list before we do the check, unless an ACL list was specified if not self.acl: for key in [AclConstants.DELETE_ACCESS, AclConstants.CHANGE_PERMISSIONS_ACCESS, AclConstants.WRITE_USER_MODEL_ACCESS, AclConstants.CHANGE_OWNER_ACCESS, AclConstants.READ_ACCESS, AclConstants.EXECUTE_ACCESS, AclConstants.WRITE_ACCESS]: for ident in [self.userName, AclConstants.OWNER, repo.getUserName()]: self.acl.append((key, ident, AclConstants.ALLOWED)) return XmlDocument._checkAclChange(self, repo) def _checkOtherChange(self, repo): """ See if the doc def has changed """ XmlDocument._checkOtherChange(self, repo) if self.passwordHash != repo.fetchResource(self.path).getPassword(): self.status |= StatusFlags.OTHER_CHANGE return def _setBase(self, basePath): """ Adjust for a relative installation """ XmlDocument._setBase(self, basePath) self.basePath = Uri.BaseJoin(basePath, self.basePath) return class Group(XmlDocument): resourceType = ResourceType.GROUP def __init__(self, groupName, acl, owner, imt, docDef, basePath, members): XmlDocument.__init__(self, basePath + '/' + groupName, acl, owner, imt, docDef) if not groupName: raise SyntaxError("Must specify a group name in a member definition") self.groupName = groupName self.members = members self.basePath = basePath self._membersToAdd = [] self._membersToRemove = [] return def checkStatus(self, repo, force=0, checkAcl=True): rt = XmlDocument.checkStatus(self, repo, force, checkAcl) ## Should this be 'self.status & StatusFlags.ACTION_REQUIRED'?? if self.status & StatusFlags.NOT_PRESENT: self._membersToAdd = self.members return rt def createContent(self, repo, quiet): """ Called when we need to add our content to the repo """ self._message("Create Group: %s"%self.path, quiet) self._message("creating members: %s" % self.members, quiet) g = repo.fetchResource(self.basePath).createGroup(self.groupName) if self.imt == 'text/xml': self.status &= ~StatusFlags.IMT_CHANGE return def updateContent(self, repo, quiet): """ Called when we need to update our content in the repo """ raise RuntimeError("Unable to update content for %s" % self.path) def updateOther(self, repo, quiet): XmlDocument.updateOther(self, repo, quiet) g = repo.fetchUserOrGroupByName(self.groupName) for m in self._membersToAdd: self._message("Adding member %s to group %s" % (m, self.groupName), quiet) mem = repo.fetchUserOrGroupByName(m) g.addMember(mem) for m in self._membersToRemove: self._message("Removing member %s from group %s" % (m, self.groupName), quiet) mem = repo.fetchUserOrGroupByName(m) g.removeMember(mem) return def _checkAclChange(self, repo): #Groups get thier ACL set automatically by the system. We need to account for that here. #So, we need to update the ACL list before we do the check, unless an ACL list was specified if not self.acl: for key in [AclConstants.DELETE_ACCESS, AclConstants.CHANGE_PERMISSIONS_ACCESS, AclConstants.WRITE_USER_MODEL_ACCESS, AclConstants.CHANGE_OWNER_ACCESS, AclConstants.READ_ACCESS, AclConstants.EXECUTE_ACCESS, AclConstants.WRITE_ACCESS]: for ident in [self.groupName, AclConstants.OWNER, repo.getUserName()]: self.acl.append((key, ident, AclConstants.ALLOWED)) return XmlDocument._checkAclChange(self, repo) def _checkOtherChange(self, repo): """ See if the doc def has changed """ XmlDocument._checkOtherChange(self, repo) curMembers = [r.getUsername() for r in repo.fetchUserOrGroupByName(self.groupName).getMembers()] for m in curMembers: if m not in self.members: self._membersToRemove.append(m) for m in self.members: if m not in curMembers: self._membersToAdd.append(m) if len(self._membersToRemove) or len(self._membersToAdd): self.status |= StatusFlags.OTHER_CHANGE return def _setBase(self, basePath): """ Adjust for a relative installation """ XmlDocument._setBase(self, basePath) self.basePath = Uri.BaseJoin(basePath, self.basePath) return def _gatherDependencies(self, basePath): XmlDocument._gatherDependencies(self, basePath) for m in self.members: if m not in g_safeAclIdents: self.dependencies.append(m) return class Product: name = "" version = 0 description = "" def __init__(self, resourceList, name='', version=0, description='', useIndicator=None): self.resourceList = resourceList self.filteredList = None self.sortedList = None self.notDirectlyChangedList = None self.basePath = '/' self.name = name self.version = version self.description = description if useIndicator is None: useIndicator = sys.stdout.isatty() self.useIndicator = useIndicator return def setBase(self, basePath): """ Processes self.resourceList, setting the destination container on each resource to be the given basePath. """ # append leading and trailing slashes if absent if basePath[-1] != '/': basePath += '/' if basePath[0] != '/': basePath = '/' + basePath for r in self.resourceList: r._setBase(basePath) self.basePath = basePath return def setIndicator(self): """ Processes self.resourceList, hooking each resource into the progress indicator. """ for r in self.resourceList: r._setIndicator(self.progressIndicator) return def filter(self, repo, checkAcl=True): """ Processes self.resourceList, creating (if not already created) 3 more lists: self.sortedList, which is the resources that have content or docdef changes and thus need to have their dependencies expanded; self.filteredList, which is the resources that have other changes; and self.notDirectlyChangedList, which is resources with no changes. """ if self.filteredList is not None: return # resources w/content or docdef changes self.sortedList = [] # resources w/minor changes self.filteredList = [] # resources with no direct changes self.notDirectlyChangedList = [] for r in self.resourceList: status = r.checkStatus(repo, checkAcl=checkAcl) if status == StatusFlags.UNKNOWN: raise RuntimeError("Status of %s not determined" % r.path) if status & (StatusFlags.CONTENT_CHANGE | StatusFlags.NOT_PRESENT): self.sortedList.append(r) elif status == StatusFlags.NO_CHANGE: self.notDirectlyChangedList.append(r) else: self.filteredList.append(r) return def sort(self, repo, quiet=True): # Sort our resources by dependency # We assume that the resources have been filtered, so first gather # dependencies on the sortedList for r in self.sortedList: r._gatherDependencies(self.basePath) sorted = [] provides = [] # list of resource paths unsorted = self.sortedList # Transfer from the unsorted list to the res list. If we don't # transfer any, then blindly move one over and print an error satisfied = True while unsorted and satisfied: # Start off by assuming nothing will be satified this round satisfied = [] # Find all resources whose dependencies are satisfied for resource in unsorted: for path in resource.dependencies: if path not in provides: if repo.hasResource(path): provides.append(path) else: # Unknown break else: # All dependencies satisfied satisfied.append(resource) provides.append(resource.path) if resource.resourceType == ResourceType.USER: provides.append(resource.userName) elif resource.resourceType == ResourceType.GROUP: provides.append(resource.groupName) for resource in satisfied: sorted.append(resource) unsorted.remove(resource) # Gather the paths which have failed for the remaining resources if unsorted: failed = [] for resource in unsorted: for path in resource.dependencies: if path not in provides: failed.append((path, resource.path)) failed.sort() sys.stderr.write("Failed dependencies:\n") for item in failed: sys.stderr.write("\t%s needed by %s\n" % item) sys.stderr.flush() #Add to sorted list any whose deps have changed sorted_paths = dict([ (r.path, None) for r in sorted ]) lookupPath = dict([ (r.path, r) for r in self.notDirectlyChangedList ]) deps_cache = {} def needsUpdate(path): if unicode(path) in sorted_paths: return True resource = lookupPath.get(path) if not resource: return False deps = deps_cache.get(resource) if deps is None: resource._gatherDependencies(self.basePath) deps = resource.dependencies deps_cache[resource] = deps #print "*"*10, "Dependencies:", deps for dep in deps: if needsUpdate(dep): sorted.append(resource) sorted_paths[resource.path] = None #print "*"*10, "Appending to sorted", path return True return False #import pprint; pprint.pprint(lookupPath.keys()) for resource in self.notDirectlyChangedList: #Mutates sorted list itself, so we don't care about return val #Blah blah ugly use of side effects blah blah. :-) needsUpdate(resource.path) self.sortedList = sorted return not unsorted def serialize(self, stream=sys.stdout, refUri=''): import Serialize stream.write(Serialize.Serialize(self, refUri)) return def install(self, repo, basePath, quiet, do_update=False, checkAcl=True): """ Install the product into the repo """ action = ('Installing/Updating', 'Updating')[do_update and 1 or 0] header = "" if self.version: header = "\n%s %s version %s" % (action, self.name, self.version) else: header = "\n%s %s" % (action, self.name) print header self.setBase(basePath) self.filter(repo, checkAcl=checkAcl) if not self.sort(repo): raise RuntimeError("Unresolved dependencies exist") if self.useIndicator: #Calculate the total size #We go over the resource list twice, filter, sort, header total = len(self.sortedList) + len(self.sortedList) + len(self.filteredList) if total: self.progressIndicator = ProgressIndicator.ProgressIndicator(self.name) self.progressIndicator.newIndicator(total) self.setIndicator() ctr = 0 #First install the sorted list for r in self.sortedList: if r.status & StatusFlags.NOT_PRESENT: r.createContent(repo, quiet) #Might need to remove owner update if not specified if r.owner is None: r.status &= ~StatusFlags.OWNER_CHANGE else: r.updateContent(repo, quiet) r.status = r.status & ~StatusFlags.CONTENT_CHANGE # save some memory, assuming we're done with cached docs if hasattr(r, 'cachedDoc'): r.cachedDoc = None if hasattr(r, 'stringContent'): r.stringContent = None if self.useIndicator: self.progressIndicator.updateProgress(ctr) ctr += 1 #Then the filtered list for r in self.filteredList + self.sortedList: if r.status & StatusFlags.IMT_CHANGE: r.updateImt(repo, quiet) r.status &= ~StatusFlags.IMT_CHANGE if r.status & StatusFlags.ACL_CHANGE: r.updateAcl(repo, quiet) r.status &= ~StatusFlags.ACL_CHANGE if r.status & StatusFlags.OWNER_CHANGE: r.updateOwner(repo, quiet) r.status &= ~StatusFlags.OWNER_CHANGE if r.status & StatusFlags.OTHER_CHANGE: r.updateOther(repo, quiet) r.status &= ~StatusFlags.OTHER_CHANGE # save some memory, assuming we're done with cached docs if hasattr(r, 'cachedDoc'): r.cachedDoc = None if hasattr(r, 'stringContent'): r.stringContent = None if self.useIndicator: self.progressIndicator.updateProgress(ctr) ctr += 1 # more potential memory savings self.sortedList = None self.filteredList = None return def Deserialize(stream, refUri=''): import Serialize return Serialize.Deserialize(stream, refUri)