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
27 """Parses a line from the results of parallel-find.pl.
28 This will work out of the box with fileinput, see
32 location, deploydir = line.split(":")
34 return Deployment(line) # lazy loaded version
35 return Deployment(location, version=ApplicationVersion.parse(deploydir))
38 """Lazily creates a deployment from a directory"""
39 return Deployment(dir)
40 def getVersionFile(self):
41 return os.path.join(self.location, '.scripts-version')
42 def getApplication(self):
43 return self.getAppVersion().application
46 self._log = DeployLog.load(self.getVersionFile())
49 """Returns the distutils Version of the deployment"""
50 return self.getAppVersion().version
51 def getAppVersion(self, force = False):
52 """Returns the ApplicationVersion of the deployment"""
53 if self._version and not force: return self._version
54 else: return self.getLog()[-1].version
56 class Application(object):
57 """Represents the generic notion of an application, i.e.
58 mediawiki or phpbb."""
59 def __init__(self, name):
62 def getRepository(self):
63 """Returns the Git repository that would contain this application."""
64 repo = os.path.join("/afs/athena.mit.edu/contrib/scripts/git/autoinstalls", self.name + ".git")
65 if not os.path.isdir(repo):
66 raise NoRepositoryError(app)
68 def getVersion(self, version):
69 if version not in self.versions:
70 self.versions[version] = ApplicationVersion(distutils.version.LooseVersion(version), self)
71 return self.versions[version]
73 class DeployLog(list):
74 # As per #python; if you decide to start overloading magic methods,
75 # we should remove this subclass
76 """Equivalent to .scripts-version: a series of DeployRevisions."""
77 def __init__(self, revs = []):
78 """`revs` List of DeployRevision objects"""
79 list.__init__(self, revs) # pass to list
82 """Loads a scripts version file and parses it into
83 DeployLog and DeployRevision objects"""
84 # XXX: DIRTY DIRTY HACK
85 # What we should actually do is parse the git logs
86 scriptsdir = os.path.join(os.path.dirname(file), ".scripts")
87 if os.path.isdir(scriptsdir):
88 file = os.path.join(scriptsdir, "old-version")
90 rev = DeployRevision()
95 raise ScriptsVersionNotEnoughFieldsError()
100 raise ScriptsVersionNoSuchFile(file)
101 # XXX: possibly rewrite this parsing code. This might
108 rev = DeployRevision()
111 # we need the dateutil parser in order to
112 # be able to parse time offsets
113 rev.datetime = dateutil.parser.parse(line)
117 rev.source = DeploySource.parse(line)
119 rev.version = ApplicationVersion.parse(line)
122 raise ScriptsVersionTooManyFieldsError()
125 return DeployLog(revs)
127 return '<DeployLog %s>' % list.__repr__(self)
129 class DeployRevision(object):
130 """A single entry in the .scripts-version file. Contains who deployed
131 this revision, what application version this is, etc."""
132 def __init__(self, datetime=None, user=None, source=None, version=None):
133 """ `datetime` Time this revision was deployed
134 `user` Person who deployed this revision, in user@host format.
135 `source` Instance of DeploySource
136 `version` Instance of ApplicationVersion
137 Note: This object is typically built incrementally."""
138 self.datetime = datetime
141 self.version = version
143 class DeploySource(object):
144 """Source of the deployment; see subclasses for examples"""
146 raise NotImplementedError # abstract class
149 # munge out common prefix
150 rel = os.path.relpath(line, "/afs/athena.mit.edu/contrib/scripts/")
151 parts = rel.split("/")
152 if parts[0] == "wizard":
153 return WizardUpdate()
154 elif parts[0] == "deploy" or parts[0] == "deploydev":
155 isDev = ( parts[0] == "deploydev" )
157 if parts[1] == "updates":
158 return OldUpdate(isDev)
160 return TarballInstall(line, isDev)
163 return UnknownDeploySource(line)
165 class TarballInstall(DeploySource):
166 """Original installation from tarball, characterized by
167 /afs/athena.mit.edu/contrib/scripts/deploy/APP-x.y.z.tar.gz
169 def __init__(self, location, isDev):
170 self.location = location
173 class OldUpdate(DeploySource):
174 """Upgrade using old upgrade infrastructure, characterized by
175 /afs/athena.mit.edu/contrib/scripts/deploydev/updates/update-scripts-version.pl
177 def __init__(self, isDev):
180 class WizardUpdate(DeploySource):
181 """Upgrade using wizard infrastructure, characterized by
182 /afs/athena.mit.edu/contrib/scripts/wizard/bin/wizard HASHGOBBLEDYGOOK
187 class UnknownDeploySource(DeploySource):
188 """Deployment that we don't know the meaning of. Wot!"""
189 def __init__(self, line):
192 class ApplicationVersion(object):
193 """Represents an abstract notion of a version for an application"""
194 def __init__(self, version, application):
195 """ `version` Instance of distutils.LooseVersion
196 `application` Instance of Application
197 WARNING: Please don't call this directly; instead, use getVersion()
198 on the application you want, so that this version gets registered."""
199 self.version = version
200 self.application = application
201 def getScriptsTag(self):
202 """Returns the name of the Git tag for this version"""
203 # XXX: This assumes that there's only a -scripts version
204 # which will not be true in the future.
205 return "v%s-scripts" % self.version
207 return cmp(x.version, y.version)
209 def parse(deploydir,applookup=None):
210 # The version of the deployment, will be:
211 # /afs/athena.mit.edu/contrib/scripts/deploy/APP-x.y.z for old style installs
212 # /afs/athena.mit.edu/contrib/scripts/wizard/srv/APP.git vx.y.z-scripts for new style installs
213 # XXX: ^- the above is wrong; there will be no more .scripts-version
214 name = deploydir.split("/")[-1]
216 if name.find(" ") != -1:
217 raw_app, raw_version = name.split(" ")
218 version = raw_version[1:] # remove leading v
219 app, _ = raw_app.split(".") # remove trailing .git
220 elif name.find("-") != -1:
221 app, _, version = name.partition("-")
222 # XXX: this should be removed after the next parallel-find run
223 elif name == "deploy":
224 raise NoSuchApplication()
226 raise DeploymentParseError(deploydir)
227 except ValueError: # mostly from the a, b = foo.split(' ')
228 raise DeploymentParseError(deploydir)
229 if not applookup: applookup = applications
231 # defer to the application for version creation
232 return applookup[app].getVersion(version)
234 raise NoSuchApplication()
236 class Error(wizard.Error):
237 """Base error class for deploy errors"""
240 class NoSuchApplication(Error):
243 class DeploymentParseError(Error):
244 def __init__(self, malformed):
245 self.malformed = malformed
249 ERROR: Could not parse deployment string:
253 class ScriptsVersionTooManyFieldsError(Error):
257 ERROR: Could not parse .scripts-version file. It
258 contained too many fields.
261 class ScriptsVersionNotEnoughFieldsError(Error):
265 ERROR: Could not parse .scripts-version file. It
266 didn't contain enough fields.
269 class ScriptsVersionNoSuchFile(Error):
270 def __init__(self, file):
275 ERROR: File %s didn't exist.
278 # If you want, you can wrap this up into a registry and access things
279 # through that, but it's not really necessary
282 "mediawiki", "wordpress", "joomla", "e107", "gallery2",
283 "phpBB", "advancedbook", "phpical", "trac", "turbogears", "django",
284 # these are technically deprecated
285 "advancedpoll", "gallery",
288 """Hash table for looking up string application name to instance"""
289 applications = dict([(n,Application(n)) for n in application_list ])