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