5 import distutils.version
9 def getInstallLines(global_options):
10 """Retrieves a list of lines from the version directory that
11 can be passed to Deployment.parse()"""
12 vs = global_options.versions
13 if os.path.isfile(vs):
14 return fileinput.input([vs])
15 return fileinput.input([vs + "/" + f for f in os.listdir(vs)])
17 class Deployment(object):
18 """Represents a deployment of an autoinstall; i.e. a concrete
19 directory in web_scripts that has .scripts-version in it."""
20 def __init__(self, location, log=None, version=None):
21 """ `location` Location of the deployment
22 `version` ApplicationVersion of the app (this is cached info)
23 `log` DeployLog of the app"""
24 self.location = location
25 self._version = version
29 """Parses a line from the results of parallel-find.pl.
30 This will work out of the box with fileinput, see
34 location, deploydir = line.split(":")
36 return Deployment(line) # lazy loaded version
37 return Deployment(location, version=ApplicationVersion.parse(deploydir))
40 """Lazily creates a deployment from a directory"""
41 return Deployment(dir)
42 def getVersionFile(self):
43 return os.path.join(self.location, '.scripts-version')
44 def getApplication(self):
45 return self.getAppVersion().application
48 self._log = DeployLog.load(self.getVersionFile())
51 """Returns the distutils Version of the deployment"""
52 return self.getAppVersion().version
53 def getAppVersion(self, force = False):
54 """Returns the ApplicationVersion of the deployment"""
55 if self._version and not force: return self._version
56 else: return self.getLog()[-1].version
58 """Simple method which registers the deployment as a +1 on the
59 appropriate version. No further inspection is done."""
60 self.getAppVersion().count(self)
62 def count_exists(self, file):
63 """Checks if the codebase has a certain file/directory in it."""
64 if os.path.exists(self.location + "/" + file):
65 self.getAppVersion().count_exists(self, file)
69 class Application(object):
70 """Represents the generic notion of an application, i.e.
71 mediawiki or phpbb."""
72 def __init__(self, name):
75 # This is 'wizard summary' specific code
79 def getRepository(self):
80 """Returns the Git repository that would contain this application."""
81 repo = os.path.join("/afs/athena.mit.edu/contrib/scripts/git/autoinstalls", self.name + ".git")
82 if not os.path.isdir(repo):
83 raise NoRepositoryError(app)
85 def getVersion(self, version):
86 if version not in self.versions:
87 self.versions[version] = ApplicationVersion(distutils.version.LooseVersion(version), self)
88 return self.versions[version]
89 # XXX: This code should go in summary.py; maybe as a mixin, maybe as
93 return '+' * int(math.ceil(float(v)/self._max * self.HISTOGRAM_WIDTH))
95 if not self.versions: return "%-11s no installs" % self.name
97 ["%-16s %3d installs" % (self.name, self._total)] + \
98 [v.report() for v in sorted(self.versions.values())]
99 for f,c in self._c_exists.items():
100 ret.append("%d users have %s" % (c,f))
101 return "\n".join(ret)
103 class DeployLog(list):
104 # As per #python; if you decide to start overloading magic methods,
105 # we should remove this subclass
106 """Equivalent to .scripts-version: a series of DeployRevisions."""
107 def __init__(self, revs = []):
108 """`revs` List of DeployRevision objects"""
109 list.__init__(self, revs) # pass to list
112 """Loads a scripts version file and parses it into
113 DeployLog and DeployRevision objects"""
114 # XXX: DIRTY DIRTY HACK
115 # What we should actually do is parse the git logs
116 scriptsdir = os.path.join(os.path.dirname(file), ".scripts")
117 if os.path.isdir(scriptsdir):
118 file = os.path.join(scriptsdir, "old-version")
120 rev = DeployRevision()
125 raise ScriptsVersionNotEnoughFieldsError()
130 raise ScriptsVersionNoSuchFile(file)
131 # XXX: possibly rewrite this parsing code. This might
138 rev = DeployRevision()
141 # we need the dateutil parser in order to
142 # be able to parse time offsets
143 rev.datetime = dateutil.parser.parse(line)
147 rev.source = DeploySource.parse(line)
149 rev.version = ApplicationVersion.parse(line)
152 raise ScriptsVersionTooManyFieldsError()
155 return DeployLog(revs)
157 return '<DeployLog %s>' % list.__repr__(self)
159 class DeployRevision(object):
160 """A single entry in the .scripts-version file. Contains who deployed
161 this revision, what application version this is, etc."""
162 def __init__(self, datetime=None, user=None, source=None, version=None):
163 """ `datetime` Time this revision was deployed
164 `user` Person who deployed this revision, in user@host format.
165 `source` Instance of DeploySource
166 `version` Instance of ApplicationVersion
167 Note: This object is typically built incrementally."""
168 self.datetime = datetime
171 self.version = version
173 class DeploySource(object):
174 """Source of the deployment; see subclasses for examples"""
176 raise NotImplementedError # abstract class
179 # munge out common prefix
180 rel = os.path.relpath(line, "/afs/athena.mit.edu/contrib/scripts/")
181 parts = rel.split("/")
182 if parts[0] == "wizard":
183 return WizardUpdate()
184 elif parts[0] == "deploy" or parts[0] == "deploydev":
185 isDev = ( parts[0] == "deploydev" )
187 if parts[1] == "updates":
188 return OldUpdate(isDev)
190 return TarballInstall(line, isDev)
193 return UnknownDeploySource(line)
195 class TarballInstall(DeploySource):
196 """Original installation from tarball, characterized by
197 /afs/athena.mit.edu/contrib/scripts/deploy/APP-x.y.z.tar.gz
199 def __init__(self, location, isDev):
200 self.location = location
203 class OldUpdate(DeploySource):
204 """Upgrade using old upgrade infrastructure, characterized by
205 /afs/athena.mit.edu/contrib/scripts/deploydev/updates/update-scripts-version.pl
207 def __init__(self, isDev):
210 class WizardUpdate(DeploySource):
211 """Upgrade using wizard infrastructure, characterized by
212 /afs/athena.mit.edu/contrib/scripts/wizard/bin/wizard HASHGOBBLEDYGOOK
217 class UnknownDeploySource(DeploySource):
218 """Deployment that we don't know the meaning of. Wot!"""
219 def __init__(self, line):
222 class ApplicationVersion(object):
223 """Represents an abstract notion of a version for an application"""
224 def __init__(self, version, application):
225 """ `version` Instance of distutils.LooseVersion
226 `application` Instance of Application
227 WARNING: Please don't call this directly; instead, use getVersion()
228 on the application you want, so that this version gets registered."""
229 self.version = version
230 self.application = application
233 def getScriptsTag(self):
234 """Returns the name of the Git tag for this version"""
235 # XXX: This assumes that there's only a -scripts version
236 # which will not be true in the future.
237 return "v%s-scripts" % self.version
239 return cmp(x.version, y.version)
241 def parse(deploydir,applookup=None):
242 # The version of the deployment, will be:
243 # /afs/athena.mit.edu/contrib/scripts/deploy/APP-x.y.z for old style installs
244 # /afs/athena.mit.edu/contrib/scripts/wizard/srv/APP.git vx.y.z-scripts for new style installs
245 # XXX: ^- the above is wrong; there will be no more .scripts-version
246 name = deploydir.split("/")[-1]
248 if name.find(" ") != -1:
249 raw_app, raw_version = name.split(" ")
250 version = raw_version[1:] # remove leading v
251 app, _ = raw_app.split(".") # remove trailing .git
252 elif name.find("-") != -1:
253 app, version = name.split("-")
254 # XXX: this should be removed after the next parallel-find run
255 elif name == "deploy":
256 # Assume that it's django, since those were botched
258 version = "0.1-scripts"
260 raise DeploymentParseError(deploydir)
261 except ValueError: # mostly from the a, b = foo.split(' ')
262 raise DeploymentParseError(deploydir)
263 if not applookup: applookup = applications
265 # defer to the application for version creation
266 return applookup[app].getVersion(version)
268 raise NoSuchApplication
269 # This is summary specific code
270 def count(self, deployment):
272 self.application._total += 1
273 if self.c > self.application._max:
274 self.application._max = self.c
275 def count_exists(self, deployment, n):
276 if n in self.c_exists: self.c_exists[n] += 1
277 else: self.c_exists[n] = 1
278 if n in self.application._c_exists: self.application._c_exists[n] += 1
279 else: self.application._c_exists[n] = 1
281 return " %-12s %3d %s" \
282 % (self.version, self.c, self.application._graph(self.c))
284 class Error(wizard.Error):
285 """Base error class for deploy errors"""
288 class NoSuchApplication(Error):
291 class DeploymentParseError(Error):
292 def __init__(self, malformed):
293 self.malformed = malformed
297 ERROR: Could not parse deployment string:
301 class ScriptsVersionTooManyFieldsError(Error):
305 ERROR: Could not parse .scripts-version file. It
306 contained too many fields.
309 class ScriptsVersionNotEnoughFieldsError(Error):
313 ERROR: Could not parse .scripts-version file. It
314 didn't contain enough fields.
317 class ScriptsVersionNoSuchFile(Error):
318 def __init__(self, file):
323 ERROR: File %s didn't exist.
326 # If you want, you can wrap this up into a registry and access things
327 # through that, but it's not really necessary
330 "mediawiki", "wordpress", "joomla", "e107", "gallery2",
331 "phpBB", "advancedbook", "phpical", "trac", "turbogears", "django",
332 # these are technically deprecated
333 "advancedpoll", "gallery",
336 """Hash table for looking up string application name to instance"""
337 applications = dict([(n,Application(n)) for n in application_list ])