import os.path import math import fileinput import dateutil.parser from distutils.version import LooseVersion as Version def getInstallLines(global_options): """Retrieves a list of lines from the version directory that can be passed to Deployment.parse()""" vd = global_options.version_dir try: return fileinput.input([vd + "/" + f for f in os.listdir(vd)]) except OSError: print "No permissions; check if AFS is mounted" raise SystemExit(-1) class NoSuchApplication(Exception): pass class DeploymentParseError(Exception): pass 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""" # XXX: This constructor should change self.location = location self._version = version self._log = log # Maybe should be an accessor self.application = version.application @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()""" try: location, deploydir = line.rstrip().split(":") except ValueError: raise DeploymentParseError name = deploydir.split("/")[-1] if 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 try: return Deployment(location, version=applications[app].getVersion(version)) except KeyError: raise NoSuchApplication @staticmethod def fromDir(dir): """Creates a deployment from a directory""" version_file = os.path.join(dir, '.scripts-version') # needs deployment log def getLog(self): if not self._log: # XXX: Load the deployment log raise NotImplemented 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 # XXX: This is summary specific code 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.""" # XXX: See below XXX HISTOGRAM_WIDTH = 30 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(Version(version), self) return self.versions[version] # XXX: This code should go in summary.py; maybe as a mixin, maybe as # a visitor acceptor def _graph(self, v): return '+' * int(math.ceil(float(v)/self._max * self.HISTOGRAM_WIDTH)) def __str__(self): if not self.versions: return "%-11s no installs" % self.name ret = \ ["%-16s %3d installs" % (self.name, self._total)] + \ [str(v) 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""" # pass to list list.__init__(self, revs) @staticmethod def load(file): """Loads a scripts version file and parses it into DeployLog and DeployRevision objects""" i = 0 rev = DeployRevision() revs = [] for line in open(file): line = line.rstrip() if not line: i = 0 revs.append(rev) 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 pass i += 1 if i: revs.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 NotImplemented # abstract class @staticmethod def parse(line): raise NotImplemented 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): self.location = location class OldUpgrade(DeploySource): """Upgrade using old upgrade infrastructure, characterized by /afs/athena.mit.edu/contrib/scripts/deploydev/updates/update-scripts-version.pl """ def __init__(self): pass # prevent not implemented error class WizardUpgrade(DeploySource): """Upgrade using wizard infrastructure, characterized by /afs/athena.mit.edu/contrib/scripts/wizard/bin/wizard HASHGOBBLEDYGOOK """ def __init__(self, rev): self.rev = rev 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""" 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(line): # 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 raise NotImplemented # 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 __str__(self): return " %-12s %3d %s" \ % (self.version, self.c, self.application._graph(self.c)) 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 ])