]> scripts.mit.edu Git - wizard.git/blob - wizard/deploy.py
Add refactoring comments.
[wizard.git] / wizard / deploy.py
1 import os.path
2 import fileinput
3 import dateutil.parser
4 import distutils.version
5
6 import wizard
7
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)])
14
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
24         self._log = log
25     # XXX: factory
26     @staticmethod
27     def parse(line):
28         """Parses a line from the results of parallel-find.pl.
29         This will work out of the box with fileinput, see
30         getInstallLines()"""
31         line = line.rstrip()
32         try:
33             location, deploydir = line.split(":")
34         except ValueError:
35             return Deployment(line) # lazy loaded version
36         return Deployment(location, version=ApplicationVersion.parse(deploydir, location))
37     @staticmethod
38     def fromDir(dir):
39         """Lazily creates a deployment from a directory"""
40         return Deployment(dir)
41     # XXX: unclear?
42     def getVersionFile(self):
43         return os.path.join(self.location, '.scripts-version')
44     # XXX: magic accessor
45     def getApplication(self):
46         return self.getAppVersion().application
47     # XXX: magic accessor
48     def getLog(self):
49         if not self._log:
50             self._log = DeployLog.load(self.getVersionFile())
51         return self._log
52     # XXX: magic accessor
53     def getVersion(self):
54         """Returns the distutils Version of the deployment"""
55         return self.getAppVersion().version
56     # XXX: magic accessor
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
61
62 class Application(object):
63     """Represents the generic notion of an application, i.e.
64     mediawiki or phpbb."""
65     def __init__(self, name):
66         self.name = name
67         self.versions = {}
68     # XXX: magic accessor
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)
74         return repo
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]
79
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
87     # XXX: factory
88     @staticmethod
89     def load(file):
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")
97         i = 0
98         rev = DeployRevision()
99         revs = []
100         def append(rev):
101             if i:
102                 if i != 4:
103                     raise ScriptsVersionNotEnoughFieldsError(file)
104                 revs.append(rev)
105         try:
106             fh = open(file)
107         except IOError:
108             raise ScriptsVersionNoSuchFile(file)
109         for line in fh:
110             line = line.rstrip()
111             if not line:
112                 append(rev)
113                 i = 0
114                 rev = DeployRevision()
115                 continue
116             if i == 0:
117                 # we need the dateutil parser in order to
118                 # be able to parse time offsets
119                 rev.datetime = dateutil.parser.parse(line)
120             elif i == 1:
121                 rev.user = line
122             elif i == 2:
123                 rev.source = DeploySource.parse(line)
124             elif i == 3:
125                 rev.version = ApplicationVersion.parse(line, rev.source)
126             else:
127                 # ruh oh
128                 raise ScriptsVersionTooManyFieldsError(file)
129             i += 1
130         append(rev)
131         return DeployLog(revs)
132     def __repr__(self):
133         return '<DeployLog %s>' % list.__repr__(self)
134
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
145         self.user = user
146         self.source = source
147         self.version = version
148
149 class DeploySource(object):
150     """Source of the deployment; see subclasses for examples"""
151     def __init__(self):
152         raise NotImplementedError # abstract class
153     # XXX: factory
154     @staticmethod
155     def parse(line):
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" )
163             try:
164                 if parts[1] == "updates":
165                     return OldUpdate(isDev)
166                 else:
167                     return TarballInstall(line, isDev)
168             except IndexError:
169                 pass
170         return UnknownDeploySource(line)
171
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
175     """
176     def __init__(self, location, isDev):
177         self.location = location
178         self.isDev = isDev
179
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
183     """
184     def __init__(self, isDev):
185         self.isDev = isDev
186
187 class WizardUpdate(DeploySource):
188     """Upgrade using wizard infrastructure, characterized by
189     /afs/athena.mit.edu/contrib/scripts/wizard/bin/wizard HASHGOBBLEDYGOOK
190     """
191     def __init__(self):
192         pass
193
194 class UnknownDeploySource(DeploySource):
195     """Deployment that we don't know the meaning of. Wot!"""
196     def __init__(self, line):
197         self.line = line
198
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
208     # XXX: unclear?
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
215     def __cmp__(x, y):
216         return cmp(x.version, y.version)
217     # XXX: move to factory
218     @staticmethod
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]
223         try:
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("-")
230             else:
231                 app = name
232                 version = "trunk"
233         except ValueError: # mostly from the a, b = foo.split(' ')
234             raise DeploymentParseError(deploydir, location)
235         if not applookup: applookup = applications
236         try:
237             # defer to the application for version creation
238             return applookup[app].getVersion(version)
239         except KeyError:
240             raise NoSuchApplication(app, location)
241
242 class Error(wizard.Error):
243     """Base error class for deploy errors"""
244     def __init__(self, location):
245         self.location = location
246     def __str__(self):
247         return "ERROR: Generic error at %s" % self.location
248
249 class NoRepositoryError(Error):
250     def __init__(self, app):
251         self.app = app
252         self.location = "unknown"
253     def __str__(self):
254         return """
255
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?
259 """ % self.app
260
261 class NoSuchApplication(Error):
262     def __init__(self, name, location):
263         self.name = name
264         self.location = location
265     def __str__(self):
266         return "ERROR: Unrecognized app '%s' at %s" % (self.name, self.location)
267
268 class DeploymentParseError(Error):
269     def __init__(self, malformed, location):
270         self.malformed = malformed
271         self.location = location
272     def __str__(self):
273         return """ERROR: Unparseable '%s' at %s""" % (self.malformed, self.location)
274
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.)"""
279     pass
280
281 class ScriptsVersionTooManyFieldsError(ScriptsVersionError):
282     def __str__(self):
283         return """
284
285 ERROR: Could not parse .scripts-version file.  It
286 contained too many fields.
287 """
288
289 class ScriptsVersionNotEnoughFieldsError(ScriptsVersionError):
290     def __str__(self):
291         return """
292
293 ERROR: Could not parse .scripts-version file. It
294 didn't contain enough fields.
295 """
296
297 class ScriptsVersionNoSuchFile(ScriptsVersionError):
298     def __init__(self, file):
299         self.file = file
300     def __str__(self):
301         return """
302
303 ERROR: File %s didn't exist.
304 """ % self.file
305
306 # If you want, you can wrap this up into a registry and access things
307 # through that, but it's not really necessary
308
309 application_list = [
310     "mediawiki", "wordpress", "joomla", "e107", "gallery2",
311     "phpBB", "advancedbook", "phpical", "trac", "turbogears", "django",
312     # these are technically deprecated
313     "advancedpoll", "gallery",
314 ]
315
316 """Hash table for looking up string application name to instance"""
317 applications = dict([(n,Application(n)) for n in application_list ])
318