5 from distutils.version import LooseVersion as Version
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
12 return fileinput.input([vd + "/" + f for f in os.listdir(vd)])
14 print "No permissions; check if AFS is mounted"
17 class NoSuchApplication(Exception):
20 class DeploymentParseError(Exception):
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
35 """Parses a line from the results of parallel-find.pl.
36 This will work out of the box with fileinput, see
39 location, deploydir = line.rstrip().split(":")
41 raise DeploymentParseError
42 return Deployment(location, version=ApplicationVersion.parse(deploydir))
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
53 self._log = DeployLog.load(self.getVersionFile())
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
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)
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)
74 class Application(object):
75 """Represents the generic notion of an application, i.e.
76 mediawiki or phpbb."""
77 def __init__(self, name):
80 # Some cache variables for fast access of calculated data
84 def getVersion(self, version):
85 if version not in self.versions:
86 self.versions[version] = ApplicationVersion(Version(version), self)
87 return self.versions[version]
88 # XXX: This code should go in summary.py; maybe as a mixin, maybe as
92 return '+' * int(math.ceil(float(v)/self._max * self.HISTOGRAM_WIDTH))
94 if not self.versions: return "%-11s no installs" % self.name
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)
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
111 """Loads a scripts version file and parses it into
112 DeployLog and DeployRevision objects"""
114 rev = DeployRevision()
116 for line in open(file):
119 if i: revs.append(rev)
121 rev = DeployRevision()
124 rev.datetime = dateutil.parser.parse(line)
128 rev.source = DeploySource.parse(line)
130 rev.version = ApplicationVersion.parse(line)
133 raise NotImplementedError
135 if i: revs.append(rev)
136 return DeployLog(revs)
138 return '<DeployLog %s>' % list.__repr__(self)
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
152 self.version = version
154 class DeploySource(object):
155 """Source of the deployment; see subclasses for examples"""
157 raise NotImplementedError # abstract class
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" )
168 if parts[1] == "updates":
169 return OldUpdate(isDev)
171 return TarballInstall(line, isDev)
174 return UnknownDeploySource(line)
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
180 def __init__(self, location, isDev):
181 self.location = location
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
188 def __init__(self, isDev):
191 class WizardUpdate(DeploySource):
192 """Upgrade using wizard infrastructure, characterized by
193 /afs/athena.mit.edu/contrib/scripts/wizard/bin/wizard HASHGOBBLEDYGOOK
198 class UnknownDeploySource(DeploySource):
199 """Deployment that we don't know the meaning of. Wot!"""
200 def __init__(self, line):
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
215 return cmp(x.version, y.version)
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
231 version = "0.1-scripts"
233 raise DeploymentParseError
234 if not applookup: applookup = applications
236 # defer to the application for version creation
237 return applookup[app].getVersion(version)
239 raise NoSuchApplication
240 # This is summary specific code
241 def count(self, deployment):
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
252 return " %-12s %3d %s" \
253 % (self.version, self.c, self.application._graph(self.c))
255 # If you want, you can wrap this up into a registry and access things
256 # through that, but it's not really necessary
259 "mediawiki", "wordpress", "joomla", "e107", "gallery2",
260 "phpBB", "advancedbook", "phpical", "trac", "turbogears", "django",
261 # these are technically deprecated
262 "advancedpoll", "gallery",
265 """Hash table for looking up string application name to instance"""
266 applications = dict([(n,Application(n)) for n in application_list ])