5 import distutils.version
9 class Error(wizard.Error):
10 """Base error class for deploy errors"""
13 class NoSuchApplication(Error):
16 class DeploymentParseError(Error):
17 def __init__(self, malformed):
18 self.malformed = malformed
22 ERROR: Could not parse deployment string:
26 class ScriptsVersionTooManyFieldsError(Error):
30 ERROR: Could not parse .scripts-version file. It
31 contained too many fields.
34 class ScriptsVersionNotEnoughFieldsError(Error):
38 ERROR: Could not parse .scripts-version file. It
39 didn't contain enough fields.
42 class ScriptsVersionNoSuchFile(Error):
43 def __init__(self, file):
48 ERROR: File %s didn't exist.
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)])
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
71 """Parses a line from the results of parallel-find.pl.
72 This will work out of the box with fileinput, see
76 location, deploydir = line.split(":")
78 return Deployment(line) # lazy loaded version
79 return Deployment(location, version=ApplicationVersion.parse(deploydir))
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
90 self._log = DeployLog.load(self.getVersionFile())
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
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)
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)
111 class Application(object):
112 """Represents the generic notion of an application, i.e.
113 mediawiki or phpbb."""
114 def __init__(self, name):
117 # Some cache variables for fast access of calculated data
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
129 return '+' * int(math.ceil(float(v)/self._max * self.HISTOGRAM_WIDTH))
131 if not self.versions: return "%-11s no installs" % self.name
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)
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
148 """Loads a scripts version file and parses it into
149 DeployLog and DeployRevision objects"""
151 rev = DeployRevision()
156 raise ScriptsVersionNotEnoughFieldsError()
161 raise ScriptsVersionNoSuchFile(file)
167 rev = DeployRevision()
170 rev.datetime = dateutil.parser.parse(line)
174 rev.source = DeploySource.parse(line)
176 rev.version = ApplicationVersion.parse(line)
179 raise ScriptsVersionTooManyFieldsError()
182 return DeployLog(revs)
184 return '<DeployLog %s>' % list.__repr__(self)
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
198 self.version = version
200 class DeploySource(object):
201 """Source of the deployment; see subclasses for examples"""
203 raise NotImplementedError # abstract class
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" )
214 if parts[1] == "updates":
215 return OldUpdate(isDev)
217 return TarballInstall(line, isDev)
220 return UnknownDeploySource(line)
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
226 def __init__(self, location, isDev):
227 self.location = location
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
234 def __init__(self, isDev):
237 class WizardUpdate(DeploySource):
238 """Upgrade using wizard infrastructure, characterized by
239 /afs/athena.mit.edu/contrib/scripts/wizard/bin/wizard HASHGOBBLEDYGOOK
244 class UnknownDeploySource(DeploySource):
245 """Deployment that we don't know the meaning of. Wot!"""
246 def __init__(self, line):
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
261 return cmp(x.version, y.version)
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]
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
278 version = "0.1-scripts"
280 raise DeploymentParseError(deploydir)
281 except ValueError: # mostly from the a, b = foo.split(' ')
282 raise DeploymentParseError(deploydir)
283 if not applookup: applookup = applications
285 # defer to the application for version creation
286 return applookup[app].getVersion(version)
288 raise NoSuchApplication
289 # This is summary specific code
290 def count(self, deployment):
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
301 return " %-12s %3d %s" \
302 % (self.version, self.c, self.application._graph(self.c))
304 # If you want, you can wrap this up into a registry and access things
305 # through that, but it's not really necessary
308 "mediawiki", "wordpress", "joomla", "e107", "gallery2",
309 "phpBB", "advancedbook", "phpical", "trac", "turbogears", "django",
310 # these are technically deprecated
311 "advancedpoll", "gallery",
314 """Hash table for looking up string application name to instance"""
315 applications = dict([(n,Application(n)) for n in application_list ])