4 import distutils.version
8 def getInstallLines(vs):
9 """Retrieves a list of lines from the version directory that
10 can be passed to Deployment.parse()"""
11 if os.path.isfile(vs):
12 return fileinput.input([vs])
13 return fileinput.input([vs + "/" + f for f in os.listdir(vs)])
15 class Deployment(object):
16 """Represents a deployment of an autoinstall; i.e. a concrete
17 directory in web_scripts that has .scripts-version in it."""
18 def __init__(self, location, log=None, version=None):
19 """ `location` Location of the deployment
20 `version` ApplicationVersion of the app (this is cached info)
21 `log` DeployLog of the app"""
22 self.location = location
23 self._version = version
28 """Parses a line from the results of parallel-find.pl.
29 This will work out of the box with fileinput, see
33 location, deploydir = line.split(":")
35 return Deployment(line) # lazy loaded version
36 return Deployment(location, version=ApplicationVersion.parse(deploydir, location))
39 """Lazily creates a deployment from a directory"""
40 return Deployment(dir)
42 def getVersionFile(self):
43 return os.path.join(self.location, '.scripts-version')
45 def getApplication(self):
46 return self.getAppVersion().application
50 self._log = DeployLog.load(self.getVersionFile())
54 """Returns the distutils Version of the deployment"""
55 return self.getAppVersion().version
57 def getAppVersion(self, force = False):
58 """Returns the ApplicationVersion of the deployment"""
59 if self._version and not force: return self._version
60 else: return self.getLog()[-1].version
62 class Application(object):
63 """Represents the generic notion of an application, i.e.
64 mediawiki or phpbb."""
65 def __init__(self, name):
69 def getRepository(self):
70 """Returns the Git repository that would contain this application."""
71 repo = os.path.join("/afs/athena.mit.edu/contrib/scripts/git/autoinstalls", self.name + ".git")
72 if not os.path.isdir(repo):
73 raise NoRepositoryError(app)
75 def getVersion(self, version):
76 if version not in self.versions:
77 self.versions[version] = ApplicationVersion(distutils.version.LooseVersion(version), self)
78 return self.versions[version]
80 class DeployLog(list):
81 # As per #python; if you decide to start overloading magic methods,
82 # we should remove this subclass
83 """Equivalent to .scripts-version: a series of DeployRevisions."""
84 def __init__(self, revs = []):
85 """`revs` List of DeployRevision objects"""
86 list.__init__(self, revs) # pass to list
90 """Loads a scripts version file and parses it into
91 DeployLog and DeployRevision objects"""
92 # XXX: DIRTY DIRTY HACK
93 # What we should actually do is parse the git logs
94 scriptsdir = os.path.join(os.path.dirname(file), ".scripts")
95 if os.path.isdir(scriptsdir):
96 file = os.path.join(scriptsdir, "old-version")
98 rev = DeployRevision()
103 raise ScriptsVersionNotEnoughFieldsError(file)
108 raise ScriptsVersionNoSuchFile(file)
114 rev = DeployRevision()
117 # we need the dateutil parser in order to
118 # be able to parse time offsets
119 rev.datetime = dateutil.parser.parse(line)
123 rev.source = DeploySource.parse(line)
125 rev.version = ApplicationVersion.parse(line, rev.source)
128 raise ScriptsVersionTooManyFieldsError(file)
131 return DeployLog(revs)
133 return '<DeployLog %s>' % list.__repr__(self)
135 class DeployRevision(object):
136 """A single entry in the .scripts-version file. Contains who deployed
137 this revision, what application version this is, etc."""
138 def __init__(self, datetime=None, user=None, source=None, version=None):
139 """ `datetime` Time this revision was deployed
140 `user` Person who deployed this revision, in user@host format.
141 `source` Instance of DeploySource
142 `version` Instance of ApplicationVersion
143 Note: This object is typically built incrementally."""
144 self.datetime = datetime
147 self.version = version
149 class DeploySource(object):
150 """Source of the deployment; see subclasses for examples"""
152 raise NotImplementedError # abstract class
156 # munge out common prefix
157 rel = os.path.relpath(line, "/afs/athena.mit.edu/contrib/scripts/")
158 parts = rel.split("/")
159 if parts[0] == "wizard":
160 return WizardUpdate()
161 elif parts[0] == "deploy" or parts[0] == "deploydev":
162 isDev = ( parts[0] == "deploydev" )
164 if parts[1] == "updates":
165 return OldUpdate(isDev)
167 return TarballInstall(line, isDev)
170 return UnknownDeploySource(line)
172 class TarballInstall(DeploySource):
173 """Original installation from tarball, characterized by
174 /afs/athena.mit.edu/contrib/scripts/deploy/APP-x.y.z.tar.gz
176 def __init__(self, location, isDev):
177 self.location = location
180 class OldUpdate(DeploySource):
181 """Upgrade using old upgrade infrastructure, characterized by
182 /afs/athena.mit.edu/contrib/scripts/deploydev/updates/update-scripts-version.pl
184 def __init__(self, isDev):
187 class WizardUpdate(DeploySource):
188 """Upgrade using wizard infrastructure, characterized by
189 /afs/athena.mit.edu/contrib/scripts/wizard/bin/wizard HASHGOBBLEDYGOOK
194 class UnknownDeploySource(DeploySource):
195 """Deployment that we don't know the meaning of. Wot!"""
196 def __init__(self, line):
199 class ApplicationVersion(object):
200 """Represents an abstract notion of a version for an application"""
201 def __init__(self, version, application):
202 """ `version` Instance of distutils.LooseVersion
203 `application` Instance of Application
204 WARNING: Please don't call this directly; instead, use getVersion()
205 on the application you want, so that this version gets registered."""
206 self.version = version
207 self.application = application
209 def getScriptsTag(self):
210 """Returns the name of the Git tag for this version"""
211 # XXX: This assumes that there's only a -scripts version
212 # which will not be true in the future. Unfortunately, finding
213 # the "true" latest version is computationally expensive
214 return "v%s-scripts" % self.version
216 return cmp(x.version, y.version)
217 # XXX: move to factory
219 def parse(deploydir,location,applookup=None):
220 # The version of the deployment, will be:
221 # /afs/athena.mit.edu/contrib/scripts/deploy/APP-x.y.z for old style installs
222 name = deploydir.split("/")[-1]
224 if name.find(" ") != -1:
225 raw_app, raw_version = name.split(" ")
226 version = raw_version[1:] # remove leading v
227 app, _ = raw_app.split(".") # remove trailing .git
228 elif name.find("-") != -1:
229 app, _, version = name.partition("-")
233 except ValueError: # mostly from the a, b = foo.split(' ')
234 raise DeploymentParseError(deploydir, location)
235 if not applookup: applookup = applications
237 # defer to the application for version creation
238 return applookup[app].getVersion(version)
240 raise NoSuchApplication(app, location)
242 class Error(wizard.Error):
243 """Base error class for deploy errors"""
244 def __init__(self, location):
245 self.location = location
247 return "ERROR: Generic error at %s" % self.location
249 class NoRepositoryError(Error):
250 def __init__(self, app):
252 self.location = "unknown"
256 ERROR: Could not find repository for this application. Have
257 you converted the repository over? Is the name %s
258 the same as the name of the .git folder?
261 class NoSuchApplication(Error):
262 def __init__(self, name, location):
264 self.location = location
266 return "ERROR: Unrecognized app '%s' at %s" % (self.name, self.location)
268 class DeploymentParseError(Error):
269 def __init__(self, malformed, location):
270 self.malformed = malformed
271 self.location = location
273 return """ERROR: Unparseable '%s' at %s""" % (self.malformed, self.location)
275 class ScriptsVersionError(Error):
276 """Errors specific to the parsing of a full .scripts-version file
277 (errors that could also be triggered while parsing a parallel-find
278 output should not be this subclass.)"""
281 class ScriptsVersionTooManyFieldsError(ScriptsVersionError):
285 ERROR: Could not parse .scripts-version file. It
286 contained too many fields.
289 class ScriptsVersionNotEnoughFieldsError(ScriptsVersionError):
293 ERROR: Could not parse .scripts-version file. It
294 didn't contain enough fields.
297 class ScriptsVersionNoSuchFile(ScriptsVersionError):
298 def __init__(self, file):
303 ERROR: File %s didn't exist.
306 # If you want, you can wrap this up into a registry and access things
307 # through that, but it's not really necessary
310 "mediawiki", "wordpress", "joomla", "e107", "gallery2",
311 "phpBB", "advancedbook", "phpical", "trac", "turbogears", "django",
312 # these are technically deprecated
313 "advancedpoll", "gallery",
316 """Hash table for looking up string application name to instance"""
317 applications = dict([(n,Application(n)) for n in application_list ])