]> scripts.mit.edu Git - wizard.git/blob - wizard/deploy.py
Massive refactor; use batons, wizard summary SUBCOMMAND
[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     @staticmethod
26     def parse(line):
27         """Parses a line from the results of parallel-find.pl.
28         This will work out of the box with fileinput, see
29         getInstallLines()"""
30         line = line.rstrip()
31         try:
32             location, deploydir = line.split(":")
33         except ValueError:
34             return Deployment(line) # lazy loaded version
35         return Deployment(location, version=ApplicationVersion.parse(deploydir))
36     @staticmethod
37     def fromDir(dir):
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
44     def getLog(self):
45         if not self._log:
46             self._log = DeployLog.load(self.getVersionFile())
47         return self._log
48     def getVersion(self):
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
55
56 class Application(object):
57     """Represents the generic notion of an application, i.e.
58     mediawiki or phpbb."""
59     def __init__(self, name):
60         self.name = name
61         self.versions = {}
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)
67         return repo
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]
72
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
80     @staticmethod
81     def load(file):
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")
89         i = 0
90         rev = DeployRevision()
91         revs = []
92         def append(rev):
93             if i:
94                 if i != 4:
95                     raise ScriptsVersionNotEnoughFieldsError()
96                 revs.append(rev)
97         try:
98             fh = open(file)
99         except IOError:
100             raise ScriptsVersionNoSuchFile(file)
101         # XXX: possibly rewrite this parsing code. This might
102         # be legacy soon
103         for line in fh:
104             line = line.rstrip()
105             if not line:
106                 append(rev)
107                 i = 0
108                 rev = DeployRevision()
109                 continue
110             if i == 0:
111                 # we need the dateutil parser in order to
112                 # be able to parse time offsets
113                 rev.datetime = dateutil.parser.parse(line)
114             elif i == 1:
115                 rev.user = line
116             elif i == 2:
117                 rev.source = DeploySource.parse(line)
118             elif i == 3:
119                 rev.version = ApplicationVersion.parse(line)
120             else:
121                 # ruh oh
122                 raise ScriptsVersionTooManyFieldsError()
123             i += 1
124         append(rev)
125         return DeployLog(revs)
126     def __repr__(self):
127         return '<DeployLog %s>' % list.__repr__(self)
128
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
139         self.user = user
140         self.source = source
141         self.version = version
142
143 class DeploySource(object):
144     """Source of the deployment; see subclasses for examples"""
145     def __init__(self):
146         raise NotImplementedError # abstract class
147     @staticmethod
148     def parse(line):
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" )
156             try:
157                 if parts[1] == "updates":
158                     return OldUpdate(isDev)
159                 else:
160                     return TarballInstall(line, isDev)
161             except IndexError:
162                 pass
163         return UnknownDeploySource(line)
164
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
168     """
169     def __init__(self, location, isDev):
170         self.location = location
171         self.isDev = isDev
172
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
176     """
177     def __init__(self, isDev):
178         self.isDev = isDev
179
180 class WizardUpdate(DeploySource):
181     """Upgrade using wizard infrastructure, characterized by
182     /afs/athena.mit.edu/contrib/scripts/wizard/bin/wizard HASHGOBBLEDYGOOK
183     """
184     def __init__(self):
185         pass
186
187 class UnknownDeploySource(DeploySource):
188     """Deployment that we don't know the meaning of. Wot!"""
189     def __init__(self, line):
190         self.line = line
191
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
206     def __cmp__(x, y):
207         return cmp(x.version, y.version)
208     @staticmethod
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]
215         try:
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()
225             else:
226                 raise DeploymentParseError(deploydir)
227         except ValueError: # mostly from the a, b = foo.split(' ')
228             raise DeploymentParseError(deploydir)
229         if not applookup: applookup = applications
230         try:
231             # defer to the application for version creation
232             return applookup[app].getVersion(version)
233         except KeyError:
234             raise NoSuchApplication()
235
236 class Error(wizard.Error):
237     """Base error class for deploy errors"""
238     pass
239
240 class NoSuchApplication(Error):
241     pass
242
243 class DeploymentParseError(Error):
244     def __init__(self, malformed):
245         self.malformed = malformed
246     def __str__(self):
247         return """
248
249 ERROR: Could not parse deployment string:
250 %s
251 """ % self.malformed
252
253 class ScriptsVersionTooManyFieldsError(Error):
254     def __str__(self):
255         return """
256
257 ERROR: Could not parse .scripts-version file.  It
258 contained too many fields.
259 """
260
261 class ScriptsVersionNotEnoughFieldsError(Error):
262     def __str__(self):
263         return """
264
265 ERROR: Could not parse .scripts-version file. It
266 didn't contain enough fields.
267 """
268
269 class ScriptsVersionNoSuchFile(Error):
270     def __init__(self, file):
271         self.file = file
272     def __str__(self):
273         return """
274
275 ERROR: File %s didn't exist.
276 """ % self.file
277
278 # If you want, you can wrap this up into a registry and access things
279 # through that, but it's not really necessary
280
281 application_list = [
282     "mediawiki", "wordpress", "joomla", "e107", "gallery2",
283     "phpBB", "advancedbook", "phpical", "trac", "turbogears", "django",
284     # these are technically deprecated
285     "advancedpoll", "gallery",
286 ]
287
288 """Hash table for looking up string application name to instance"""
289 applications = dict([(n,Application(n)) for n in application_list ])
290