X-Git-Url: https://scripts.mit.edu/gitweb/wizard.git/blobdiff_plain/20f9a8a397d61e9d2c73169153715b339435ad52..6eae78b3d8ed73a0266ef09a30e4b09e791dd9fb:/wizard/deploy.py?ds=inline diff --git a/wizard/deploy.py b/wizard/deploy.py index fce0c3a..b6d6f66 100644 --- a/wizard/deploy.py +++ b/wizard/deploy.py @@ -4,6 +4,9 @@ import dateutil.parser import distutils.version import wizard +from wizard import log + +## -- Global Functions -- def getInstallLines(vs): """Retrieves a list of lines from the version directory that @@ -12,6 +15,8 @@ def getInstallLines(vs): return fileinput.input([vs]) return fileinput.input([vs + "/" + f for f in os.listdir(vs)]) +## -- Model Objects -- + class Deployment(object): """Represents a deployment of an autoinstall; i.e. a concrete directory in web_scripts that has .scripts-version in it.""" @@ -22,6 +27,26 @@ class Deployment(object): self.location = location self._version = version self._log = log + @property + def version_file(self): + return os.path.join(self.location, '.scripts-version') + @property + def application(self): + return self.app_version.application + @property + def log(self): + if not self._log: + self._log = log.DeployLog.load(self.version_file) + return self._log + @property + def version(self): + """Returns the distutils Version of the deployment""" + return self.app_version.version + @property + def app_version(self, force = False): + """Returns the ApplicationVersion of the deployment""" + if self._version and not force: return self._version + else: return self.log[-1].version @staticmethod def parse(line): """Parses a line from the results of parallel-find.pl. @@ -33,25 +58,6 @@ class Deployment(object): 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) - 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 class Application(object): """Represents the generic notion of an application, i.e. @@ -59,135 +65,25 @@ class Application(object): def __init__(self, name): self.name = name self.versions = {} - def getRepository(self): + @property + def repository(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): + def makeVersion(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 @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) + def make(name): + """Makes an application, but uses the correct subtype if available.""" try: - fh = open(file) - except IOError: - raise ScriptsVersionNoSuchFile(file) - # XXX: possibly rewrite this parsing code. This might - # be legacy soon - 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 - @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 + __import__("wizard.app." + name) + return getattr(wizard.app, name).Application(name) + except ImportError: + return Application(name) class ApplicationVersion(object): """Represents an abstract notion of a version for an application""" @@ -198,10 +94,12 @@ class ApplicationVersion(object): on the application you want, so that this version gets registered.""" self.version = version self.application = application - def getScriptsTag(self): + @property + def scripts_tag(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. + # 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) @@ -209,8 +107,6 @@ class ApplicationVersion(object): 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 - # /afs/athena.mit.edu/contrib/scripts/wizard/srv/APP.git vx.y.z-scripts for new style installs - # XXX: ^- the above is wrong; there will be no more .scripts-version name = deploydir.split("/")[-1] try: if name.find(" ") != -1: @@ -224,31 +120,18 @@ class ApplicationVersion(object): version = "trunk" except ValueError: # mostly from the a, b = foo.split(' ') raise DeploymentParseError(deploydir, location) - if not applookup: applookup = applications + if not applookup: applookup = applications() try: # defer to the application for version creation - return applookup[app].getVersion(version) + return applookup[app].makeVersion(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 """ +## -- Exceptions -- -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 Error(Exception): + """Base error class for this module""" + pass class NoSuchApplication(Error): def __init__(self, name, location): @@ -264,36 +147,17 @@ class DeploymentParseError(Error): 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 +class NoRepositoryError(Error): + def __init__(self, app): + self.app = app + self.location = "unknown" def __str__(self): return """ -ERROR: File %s didn't exist. -""" % self.file +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 # If you want, you can wrap this up into a registry and access things # through that, but it's not really necessary @@ -304,7 +168,12 @@ application_list = [ # 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 ]) +_applications = None + +def applications(): + """Hash table for looking up string application name to instance""" + global _applications + if not _applications: + _applications = dict([(n,Application.make(n)) for n in application_list ]) + return _applications