]> scripts.mit.edu Git - wizard.git/blob - wizard/deploy.py
Add comments on possible ways to fix theoretical 64KB buffer problem.
[wizard.git] / wizard / deploy.py
1 import os.path
2 import math
3 import fileinput
4 import dateutil.parser
5 import distutils.version
6
7 import wizard
8
9 class Error(wizard.Error):
10     """Base error class for deploy errors"""
11     pass
12
13 class NoSuchApplication(Error):
14     pass
15
16 class DeploymentParseError(Error):
17     def __init__(self, malformed):
18         self.malformed = malformed
19     def __str__(self):
20         return """
21
22 ERROR: Could not parse deployment string:
23 %s
24 """ % self.malformed
25
26 class ScriptsVersionTooManyFieldsError(Error):
27     def __str__(self):
28         return """
29
30 ERROR: Could not parse .scripts-version file.  It
31 contained too many fields.
32 """
33
34 class ScriptsVersionNotEnoughFieldsError(Error):
35     def __str__(self):
36         return """
37
38 ERROR: Could not parse .scripts-version file. It
39 didn't contain enough fields.
40 """
41
42 class ScriptsVersionNoSuchFile(Error):
43     def __init__(self, file):
44         self.file = file
45     def __str__(self):
46         return """
47
48 ERROR: File %s didn't exist.
49 """ % self.file
50
51 def getInstallLines(global_options):
52     """Retrieves a list of lines from the version directory that
53     can be passed to Deployment.parse()"""
54     vs = global_options.versions
55     if os.path.isfile(vs):
56         return fileinput.input([vs])
57     return fileinput.input([vs + "/" + f for f in os.listdir(vs)])
58
59 class Deployment(object):
60     """Represents a deployment of an autoinstall; i.e. a concrete
61     directory in web_scripts that has .scripts-version in it."""
62     def __init__(self, location, log=None, version=None):
63         """ `location`  Location of the deployment
64             `version`   ApplicationVersion of the app (this is cached info)
65             `log`       DeployLog of the app"""
66         self.location = location
67         self._version = version
68         self._log = log
69     @staticmethod
70     def parse(line):
71         """Parses a line from the results of parallel-find.pl.
72         This will work out of the box with fileinput, see
73         getInstallLines()"""
74         line = line.rstrip()
75         try:
76             location, deploydir = line.split(":")
77         except ValueError:
78             return Deployment(line) # lazy loaded version
79         return Deployment(location, version=ApplicationVersion.parse(deploydir))
80     @staticmethod
81     def fromDir(dir):
82         """Lazily creates a deployment from a directory"""
83         return Deployment(dir)
84     def getVersionFile(self):
85         return os.path.join(self.location, '.scripts-version')
86     def getApplication(self):
87         return self.getAppVersion().application
88     def getLog(self):
89         if not self._log:
90             self._log = DeployLog.load(self.getVersionFile())
91         return self._log
92     def getVersion(self):
93         """Returns the distutils Version of the deployment"""
94         return self.getAppVersion().version
95     def getAppVersion(self, force = False):
96         """Returns the ApplicationVersion of the deployment"""
97         if self._version and not force: return self._version
98         else: return self.getLog()[-1].version
99     def count(self):
100         """Simple method which registers the deployment as a +1 on the
101         appropriate version. No further inspection is done."""
102         self.getAppVersion().count(self)
103         return True
104     def count_exists(self, file):
105         """Checks if the codebase has a certain file/directory in it."""
106         if os.path.exists(self.location + "/" + file):
107             self.getAppVersion().count_exists(self, file)
108             return True
109         return False
110
111 class Application(object):
112     """Represents the generic notion of an application, i.e.
113     mediawiki or phpbb."""
114     def __init__(self, name):
115         self.name = name
116         self.versions = {}
117         # Some cache variables for fast access of calculated data
118         self._total = 0
119         self._max   = 0
120         self._c_exists = {}
121     def getVersion(self, version):
122         if version not in self.versions:
123             self.versions[version] = ApplicationVersion(distutils.version.LooseVersion(version), self)
124         return self.versions[version]
125     # XXX: This code should go in summary.py; maybe as a mixin, maybe as
126     # a visitor acceptor
127     HISTOGRAM_WIDTH = 30
128     def _graph(self, v):
129         return '+' * int(math.ceil(float(v)/self._max * self.HISTOGRAM_WIDTH))
130     def report(self):
131         if not self.versions: return "%-11s   no installs" % self.name
132         ret = \
133             ["%-16s %3d installs" % (self.name, self._total)] + \
134             [v.report() for v in sorted(self.versions.values())]
135         for f,c in self._c_exists.items():
136             ret.append("%d users have %s" % (c,f))
137         return "\n".join(ret)
138
139 class DeployLog(list):
140     # As per #python; if you decide to start overloading magic methods,
141     # we should remove this subclass
142     """Equivalent to .scripts-version: a series of DeployRevisions."""
143     def __init__(self, revs = []):
144         """`revs`  List of DeployRevision objects"""
145         list.__init__(self, revs) # pass to list
146     @staticmethod
147     def load(file):
148         """Loads a scripts version file and parses it into
149         DeployLog and DeployRevision objects"""
150         i = 0
151         rev = DeployRevision()
152         revs = []
153         def append(rev):
154             if i:
155                 if i != 4:
156                     raise ScriptsVersionNotEnoughFieldsError()
157                 revs.append(rev)
158         try:
159             fh = open(file)
160         except IOError:
161             raise ScriptsVersionNoSuchFile(file)
162         for line in fh:
163             line = line.rstrip()
164             if not line:
165                 append(rev)
166                 i = 0
167                 rev = DeployRevision()
168                 continue
169             if i == 0:
170                 rev.datetime = dateutil.parser.parse(line)
171             elif i == 1:
172                 rev.user = line
173             elif i == 2:
174                 rev.source = DeploySource.parse(line)
175             elif i == 3:
176                 rev.version = ApplicationVersion.parse(line)
177             else:
178                 # ruh oh
179                 raise ScriptsVersionTooManyFieldsError()
180             i += 1
181         append(rev)
182         return DeployLog(revs)
183     def __repr__(self):
184         return '<DeployLog %s>' % list.__repr__(self)
185
186 class DeployRevision(object):
187     """A single entry in the .scripts-version file. Contains who deployed
188     this revision, what application version this is, etc."""
189     def __init__(self, datetime=None, user=None, source=None, version=None):
190         """ `datetime`  Time this revision was deployed
191             `user`      Person who deployed this revision, in user@host format.
192             `source`    Instance of DeploySource
193             `version`   Instance of ApplicationVersion
194         Note: This object is typically built incrementally."""
195         self.datetime = datetime
196         self.user = user
197         self.source = source
198         self.version = version
199
200 class DeploySource(object):
201     """Source of the deployment; see subclasses for examples"""
202     def __init__(self):
203         raise NotImplementedError # abstract class
204     @staticmethod
205     def parse(line):
206         # munge out common prefix
207         rel = os.path.relpath(line, "/afs/athena.mit.edu/contrib/scripts/")
208         parts = rel.split("/")
209         if parts[0] == "wizard":
210             return WizardUpdate()
211         elif parts[0] == "deploy" or parts[0] == "deploydev":
212             isDev = ( parts[0] == "deploydev" )
213             try:
214                 if parts[1] == "updates":
215                     return OldUpdate(isDev)
216                 else:
217                     return TarballInstall(line, isDev)
218             except IndexError:
219                 pass
220         return UnknownDeploySource(line)
221
222 class TarballInstall(DeploySource):
223     """Original installation from tarball, characterized by
224     /afs/athena.mit.edu/contrib/scripts/deploy/APP-x.y.z.tar.gz
225     """
226     def __init__(self, location, isDev):
227         self.location = location
228         self.isDev = isDev
229
230 class OldUpdate(DeploySource):
231     """Upgrade using old upgrade infrastructure, characterized by
232     /afs/athena.mit.edu/contrib/scripts/deploydev/updates/update-scripts-version.pl
233     """
234     def __init__(self, isDev):
235         self.isDev = isDev
236
237 class WizardUpdate(DeploySource):
238     """Upgrade using wizard infrastructure, characterized by
239     /afs/athena.mit.edu/contrib/scripts/wizard/bin/wizard HASHGOBBLEDYGOOK
240     """
241     def __init__(self):
242         pass
243
244 class UnknownDeploySource(DeploySource):
245     """Deployment that we don't know the meaning of. Wot!"""
246     def __init__(self, line):
247         self.line = line
248
249 class ApplicationVersion(object):
250     """Represents an abstract notion of a version for an application"""
251     def __init__(self, version, application):
252         """ `version`       Instance of distutils.LooseVersion
253             `application`   Instance of Application
254         WARNING: Please don't call this directly; instead, use getVersion()
255         on the application you want, so that this version gets registered."""
256         self.version = version
257         self.application = application
258         self.c = 0
259         self.c_exists = {}
260     def __cmp__(x, y):
261         return cmp(x.version, y.version)
262     @staticmethod
263     def parse(deploydir,applookup=None):
264         # The version of the deployment, will be:
265         #   /afs/athena.mit.edu/contrib/scripts/deploy/APP-x.y.z for old style installs
266         #   /afs/athena.mit.edu/contrib/scripts/wizard/srv/APP.git vx.y.z-scripts for new style installs
267         name = deploydir.split("/")[-1]
268         try:
269             if name.find(" ") != -1:
270                 raw_app, raw_version = name.split(" ")
271                 version = raw_version[1:] # remove leading v
272                 app, _ = raw_app.split(".") # remove trailing .git
273             elif name.find("-") != -1:
274                 app, version = name.split("-")
275             elif name == "deploy":
276                 # Assume that it's django, since those were botched
277                 app = "django"
278                 version = "0.1-scripts"
279             else:
280                 raise DeploymentParseError(deploydir)
281         except ValueError: # mostly from the a, b = foo.split(' ')
282             raise DeploymentParseError(deploydir)
283         if not applookup: applookup = applications
284         try:
285             # defer to the application for version creation
286             return applookup[app].getVersion(version)
287         except KeyError:
288             raise NoSuchApplication
289     # This is summary specific code
290     def count(self, deployment):
291         self.c += 1
292         self.application._total += 1
293         if self.c > self.application._max:
294             self.application._max = self.c
295     def count_exists(self, deployment, n):
296         if n in self.c_exists: self.c_exists[n] += 1
297         else: self.c_exists[n] = 1
298         if n in self.application._c_exists: self.application._c_exists[n] += 1
299         else: self.application._c_exists[n] = 1
300     def report(self):
301         return "    %-12s %3d  %s" \
302             % (self.version, self.c, self.application._graph(self.c))
303
304 # If you want, you can wrap this up into a registry and access things
305 # through that, but it's not really necessary
306
307 application_list = [
308     "mediawiki", "wordpress", "joomla", "e107", "gallery2",
309     "phpBB", "advancedbook", "phpical", "trac", "turbogears", "django",
310     # these are technically deprecated
311     "advancedpoll", "gallery",
312 ]
313
314 """Hash table for looking up string application name to instance"""
315 applications = dict([(n,Application(n)) for n in application_list ])
316