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