import os.path import fileinput import dateutil.parser import distutils.version import wizard def getInstallLines(vs): """Retrieves a list of lines from the version directory that can be passed to Deployment.parse()""" if os.path.isfile(vs): return fileinput.input([vs]) return fileinput.input([vs + "/" + f for f in os.listdir(vs)]) class Deployment(object): """Represents a deployment of an autoinstall; i.e. a concrete directory in web_scripts that has .scripts-version in it.""" def __init__(self, location, log=None, version=None): """ `location` Location of the deployment `version` ApplicationVersion of the app (this is cached info) `log` DeployLog of the app""" self.location = location self._version = version self._log = log # XXX: factory @staticmethod def parse(line): """Parses a line from the results of parallel-find.pl. This will work out of the box with fileinput, see getInstallLines()""" line = line.rstrip() try: location, deploydir = line.split(":") except ValueError: return Deployment(line) # lazy loaded version return Deployment(location, version=ApplicationVersion.parse(deploydir, location)) @staticmethod def fromDir(dir): """Lazily creates a deployment from a directory""" return Deployment(dir) # XXX: unclear? def getVersionFile(self): return os.path.join(self.location, '.scripts-version') # XXX: magic accessor def getApplication(self): return self.getAppVersion().application # XXX: magic accessor def getLog(self): if not self._log: self._log = DeployLog.load(self.getVersionFile()) return self._log # XXX: magic accessor def getVersion(self): """Returns the distutils Version of the deployment""" return self.getAppVersion().version # XXX: magic accessor def getAppVersion(self, force = False): """Returns the ApplicationVersion of the deployment""" if self._version and not force: return self._version else: return self.getLog()[-1].version class Application(object): """Represents the generic notion of an application, i.e. mediawiki or phpbb.""" def __init__(self, name): self.name = name self.versions = {} # XXX: magic accessor def getRepository(self): """Returns the Git repository that would contain this application.""" repo = os.path.join("/afs/athena.mit.edu/contrib/scripts/git/autoinstalls", self.name + ".git") if not os.path.isdir(repo): raise NoRepositoryError(app) return repo def getVersion(self, version): if version not in self.versions: self.versions[version] = ApplicationVersion(distutils.version.LooseVersion(version), self) return self.versions[version] class DeployLog(list): # As per #python; if you decide to start overloading magic methods, # we should remove this subclass """Equivalent to .scripts-version: a series of DeployRevisions.""" def __init__(self, revs = []): """`revs` List of DeployRevision objects""" list.__init__(self, revs) # pass to list # XXX: factory @staticmethod def load(file): """Loads a scripts version file and parses it into DeployLog and DeployRevision objects""" # XXX: DIRTY DIRTY HACK # What we should actually do is parse the git logs scriptsdir = os.path.join(os.path.dirname(file), ".scripts") if os.path.isdir(scriptsdir): file = os.path.join(scriptsdir, "old-version") i = 0 rev = DeployRevision() revs = [] def append(rev): if i: if i != 4: raise ScriptsVersionNotEnoughFieldsError(file) revs.append(rev) try: fh = open(file) except IOError: raise ScriptsVersionNoSuchFile(file) for line in fh: line = line.rstrip() if not line: append(rev) i = 0 rev = DeployRevision() continue if i == 0: # we need the dateutil parser in order to # be able to parse time offsets rev.datetime = dateutil.parser.parse(line) elif i == 1: rev.user = line elif i == 2: rev.source = DeploySource.parse(line) elif i == 3: rev.version = ApplicationVersion.parse(line, rev.source) else: # ruh oh raise ScriptsVersionTooManyFieldsError(file) i += 1 append(rev) return DeployLog(revs) def __repr__(self): return '' % list.__repr__(self) class DeployRevision(object): """A single entry in the .scripts-version file. Contains who deployed this revision, what application version this is, etc.""" def __init__(self, datetime=None, user=None, source=None, version=None): """ `datetime` Time this revision was deployed `user` Person who deployed this revision, in user@host format. `source` Instance of DeploySource `version` Instance of ApplicationVersion Note: This object is typically built incrementally.""" self.datetime = datetime self.user = user self.source = source self.version = version class DeploySource(object): """Source of the deployment; see subclasses for examples""" def __init__(self): raise NotImplementedError # abstract class # XXX: factory @staticmethod def parse(line): # munge out common prefix rel = os.path.relpath(line, "/afs/athena.mit.edu/contrib/scripts/") parts = rel.split("/") if parts[0] == "wizard": return WizardUpdate() elif parts[0] == "deploy" or parts[0] == "deploydev": isDev = ( parts[0] == "deploydev" ) try: if parts[1] == "updates": return OldUpdate(isDev) else: return TarballInstall(line, isDev) except IndexError: pass return UnknownDeploySource(line) class TarballInstall(DeploySource): """Original installation from tarball, characterized by /afs/athena.mit.edu/contrib/scripts/deploy/APP-x.y.z.tar.gz """ def __init__(self, location, isDev): self.location = location self.isDev = isDev class OldUpdate(DeploySource): """Upgrade using old upgrade infrastructure, characterized by /afs/athena.mit.edu/contrib/scripts/deploydev/updates/update-scripts-version.pl """ def __init__(self, isDev): self.isDev = isDev class WizardUpdate(DeploySource): """Upgrade using wizard infrastructure, characterized by /afs/athena.mit.edu/contrib/scripts/wizard/bin/wizard HASHGOBBLEDYGOOK """ def __init__(self): pass class UnknownDeploySource(DeploySource): """Deployment that we don't know the meaning of. Wot!""" def __init__(self, line): self.line = line class ApplicationVersion(object): """Represents an abstract notion of a version for an application""" def __init__(self, version, application): """ `version` Instance of distutils.LooseVersion `application` Instance of Application WARNING: Please don't call this directly; instead, use getVersion() on the application you want, so that this version gets registered.""" self.version = version self.application = application # XXX: unclear? def getScriptsTag(self): """Returns the name of the Git tag for this version""" # XXX: This assumes that there's only a -scripts version # which will not be true in the future. Unfortunately, finding # the "true" latest version is computationally expensive return "v%s-scripts" % self.version def __cmp__(x, y): return cmp(x.version, y.version) # XXX: move to factory @staticmethod def parse(deploydir,location,applookup=None): # The version of the deployment, will be: # /afs/athena.mit.edu/contrib/scripts/deploy/APP-x.y.z for old style installs name = deploydir.split("/")[-1] try: if name.find(" ") != -1: raw_app, raw_version = name.split(" ") version = raw_version[1:] # remove leading v app, _ = raw_app.split(".") # remove trailing .git elif name.find("-") != -1: app, _, version = name.partition("-") else: app = name version = "trunk" except ValueError: # mostly from the a, b = foo.split(' ') raise DeploymentParseError(deploydir, location) if not applookup: applookup = applications try: # defer to the application for version creation return applookup[app].getVersion(version) except KeyError: raise NoSuchApplication(app, location) class Error(wizard.Error): """Base error class for deploy errors""" def __init__(self, location): self.location = location def __str__(self): return "ERROR: Generic error at %s" % self.location class NoRepositoryError(Error): def __init__(self, app): self.app = app self.location = "unknown" def __str__(self): return """ ERROR: Could not find repository for this application. Have you converted the repository over? Is the name %s the same as the name of the .git folder? """ % self.app class NoSuchApplication(Error): def __init__(self, name, location): self.name = name self.location = location def __str__(self): return "ERROR: Unrecognized app '%s' at %s" % (self.name, self.location) class DeploymentParseError(Error): def __init__(self, malformed, location): self.malformed = malformed self.location = location def __str__(self): return """ERROR: Unparseable '%s' at %s""" % (self.malformed, self.location) class ScriptsVersionError(Error): """Errors specific to the parsing of a full .scripts-version file (errors that could also be triggered while parsing a parallel-find output should not be this subclass.)""" pass class ScriptsVersionTooManyFieldsError(ScriptsVersionError): def __str__(self): return """ ERROR: Could not parse .scripts-version file. It contained too many fields. """ class ScriptsVersionNotEnoughFieldsError(ScriptsVersionError): def __str__(self): return """ ERROR: Could not parse .scripts-version file. It didn't contain enough fields. """ class ScriptsVersionNoSuchFile(ScriptsVersionError): def __init__(self, file): self.file = file def __str__(self): return """ ERROR: File %s didn't exist. """ % self.file # If you want, you can wrap this up into a registry and access things # through that, but it's not really necessary application_list = [ "mediawiki", "wordpress", "joomla", "e107", "gallery2", "phpBB", "advancedbook", "phpical", "trac", "turbogears", "django", # these are technically deprecated "advancedpoll", "gallery", ] """Hash table for looking up string application name to instance""" applications = dict([(n,Application(n)) for n in application_list ])