import os.path import math import fileinput import dateutil.parser import distutils.version import wizard class Error(wizard.Error): """Base error class for deploy errors""" pass class NoSuchApplication(Error): pass class DeploymentParseError(Error): def __init__(self, malformed): self.malformed = malformed def __str__(self): return """ ERROR: Could not parse deployment string: %s """ % self.malformed class ScriptsVersionTooManyFieldsError(Error): def __str__(self): return """ ERROR: Could not parse .scripts-version file. It contained too many fields. """ class ScriptsVersionNotEnoughFieldsError(Error): def __str__(self): return """ ERROR: Could not parse .scripts-version file. It didn't contain enough fields. """ class ScriptsVersionNoSuchFile(Error): def __init__(self, file): self.file = file def __str__(self): return """ ERROR: File %s didn't exist. """ % self.file def getInstallLines(global_options): """Retrieves a list of lines from the version directory that can be passed to Deployment.parse()""" vs = global_options.versions 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 @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)) @staticmethod def fromDir(dir): """Lazily creates a deployment from a directory""" return Deployment(dir) def getVersionFile(self): return os.path.join(self.location, '.scripts-version') def getApplication(self): return self.getAppVersion().application def getLog(self): if not self._log: self._log = DeployLog.load(self.getVersionFile()) return self._log def getVersion(self): """Returns the distutils Version of the deployment""" return self.getAppVersion().version 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 def count(self): """Simple method which registers the deployment as a +1 on the appropriate version. No further inspection is done.""" self.getAppVersion().count(self) return True def count_exists(self, file): """Checks if the codebase has a certain file/directory in it.""" if os.path.exists(self.location + "/" + file): self.getAppVersion().count_exists(self, file) return True return False class Application(object): """Represents the generic notion of an application, i.e. mediawiki or phpbb.""" def __init__(self, name): self.name = name self.versions = {} # Some cache variables for fast access of calculated data self._total = 0 self._max = 0 self._c_exists = {} def getVersion(self, version): if version not in self.versions: self.versions[version] = ApplicationVersion(distutils.version.LooseVersion(version), self) return self.versions[version] # XXX: This code should go in summary.py; maybe as a mixin, maybe as # a visitor acceptor HISTOGRAM_WIDTH = 30 def _graph(self, v): return '+' * int(math.ceil(float(v)/self._max * self.HISTOGRAM_WIDTH)) def report(self): if not self.versions: return "%-11s no installs" % self.name ret = \ ["%-16s %3d installs" % (self.name, self._total)] + \ [v.report() for v in sorted(self.versions.values())] for f,c in self._c_exists.items(): ret.append("%d users have %s" % (c,f)) return "\n".join(ret) 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 @staticmethod def load(file): """Loads a scripts version file and parses it into DeployLog and DeployRevision objects""" i = 0 rev = DeployRevision() revs = [] def append(rev): if i: if i != 4: raise ScriptsVersionNotEnoughFieldsError() 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: 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) else: # ruh oh raise ScriptsVersionTooManyFieldsError() 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 @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 self.c = 0 self.c_exists = {} def __cmp__(x, y): return cmp(x.version, y.version) @staticmethod def parse(deploydir,applookup=None): # The version of the deployment, will be: # /afs/athena.mit.edu/contrib/scripts/deploy/APP-x.y.z for old style installs # /afs/athena.mit.edu/contrib/scripts/wizard/srv/APP.git vx.y.z-scripts for new 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.split("-") elif name == "deploy": # Assume that it's django, since those were botched app = "django" version = "0.1-scripts" else: raise DeploymentParseError(deploydir) except ValueError: # mostly from the a, b = foo.split(' ') raise DeploymentParseError(deploydir) if not applookup: applookup = applications try: # defer to the application for version creation return applookup[app].getVersion(version) except KeyError: raise NoSuchApplication # This is summary specific code def count(self, deployment): self.c += 1 self.application._total += 1 if self.c > self.application._max: self.application._max = self.c def count_exists(self, deployment, n): if n in self.c_exists: self.c_exists[n] += 1 else: self.c_exists[n] = 1 if n in self.application._c_exists: self.application._c_exists[n] += 1 else: self.application._c_exists[n] = 1 def report(self): return " %-12s %3d %s" \ % (self.version, self.c, self.application._graph(self.c)) # 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 ])