2 Object model for querying information and manipulating deployments
3 of autoinstalls. Every :class:`Deployment` has an :class:`ApplicationVersion`
4 which in turn has an :class:`Application`.
10 import distutils.version
14 from wizard import log
16 ## -- Global Functions --
18 def get_install_lines(versions_store):
20 Low level function that retrieves a list of lines from the
21 :term:`versions store` that can be passed to :meth:`Deployment.parse`.
23 if os.path.isfile(versions_store):
24 return fileinput.input([versions_store])
25 return fileinput.input([versions_store + "/" + f for f in os.listdir(versions_path)])
27 def parse_install_lines(show, versions_store, yield_errors = False):
29 Generator function for iterating through all autoinstalls.
30 Each item is an instance of :class:`Deployment`, or possibly
31 a :class:`wizard.deploy.Error` if ``yield_errors`` is ``True``. You can
32 filter out applications and versions by specifying ``app``
33 or ``app-1.2.3`` in ``show``. This function may generate
36 if not show: show = applications()
37 show = frozenset(show)
38 for line in get_install_lines(versions_store):
41 d = Deployment.parse(line)
42 name = d.application.name
43 except NoSuchApplication as e:
48 # we consider this a worse error
49 logging.warning("Error with '%s'" % line.rstrip())
52 if name + "-" + str(d.version) in show or name in show:
59 ## -- Model Objects --
61 class Deployment(object):
63 Represents a deployment of an autoinstall, e.g. directory in a user's
64 web_scripts that has ``.scripts`` directory or ``.scripts-version``
65 file in it. Supply ``version`` with an :class:`ApplicationVersion` only if
66 you were reading from the :term:`versions store` and care about
67 speed (data from there can be stale).
69 #: Absolute path to the deployment
71 def __init__(self, location, version=None):
72 self.location = os.path.realpath(location)
73 self._app_version = version
74 # some cache variables
77 def read(self, file, force = False):
79 Reads a file's contents, possibly from cache unless ``force``
82 if force or file not in self._read_cache:
83 f = open(os.path.join(self.location, file))
84 self._read_cache[file] = f.read()
86 return self._read_cache[file]
89 Extracts all the values of all variables from deployment.
90 These variables may be used for parametrizing generic parent
91 commits and include things such as database access credentials
92 and local configuration.
94 return self.application.extract(self)
95 def parametrize(self, dir):
97 Edits files in ``dir`` to replace WIZARD_* variables with literal
98 instances. This is used for constructing virtual merge bases, and
99 as such dir will generally not equal :attr:`location`.
101 return self.application.parametrize(self, dir)
102 def updateVersion(self, version):
104 Update the version of this deployment.
106 This method will update the version of this deployment in memory
107 and on disk. It doesn't actually do an upgrade. The version
108 string you pass here should have ``-scripts`` as a suffix.
110 self._app_version = self.application.makeVersion(version)
111 f = open(os.path.join(self.scripts_dir, 'version'), 'w')
112 f.write(self.application.name + '-' + version + "\n")
114 def scriptsifyVersion(self):
116 Converts from ``v1.0`` to ``v1.0-scripts``; use at end of migration.
120 This makes the assumption that a migration will be to
121 a ``-scripts`` tag and not a ``-scripts2`` tag. If you botch
122 migration, blow away the tag and try again.
124 self.updateVersion(self.app_version.scripts_tag)
127 """Whether or not the autoinstalls has been migrated."""
128 return os.path.isdir(self.scripts_dir)
130 def scripts_dir(self):
131 """The absolute path of the ``.scripts`` directory."""
132 return os.path.join(self.location, '.scripts')
134 def old_version_file(self):
136 The absolute path of either ``.scripts-version`` (for unmigrated
137 installs) or ``.scripts/version``.
141 Use of this is discouraged for migrated installs.
144 return os.path.join(self.scripts_dir, 'old-version')
146 return os.path.join(self.location, '.scripts-version')
148 def version_file(self):
149 """The absolute path of the ``.scripts/version`` file."""
150 return os.path.join(self.scripts_dir, 'version')
152 def application(self):
153 """The :class:`Application` of this deployment."""
154 return self.app_version.application
157 """The :class:`wizard.deploy.Log` of this deployment."""
159 self._log = log.DeployLog.load(self)
164 The :class:`distutils.version.LooseVersion` of this
167 return self.app_version.version
169 def app_version(self):
170 """The :class:`ApplicationVersion` of this deployment."""
171 if not self._app_version:
172 if os.path.isfile(self.version_file):
173 fh = open(self.version_file)
174 appname, _, version = fh.read().rstrip().partition('-')
176 self._app_version = ApplicationVersion.make(appname, version)
178 self._app_version = self.log[-1].version
179 return self._app_version
183 Parses a line from the :term:`versions store`.
187 Use this method only when speed is of the utmost
188 importance. You should prefer to directly create a deployment
189 with only a ``location`` when possible.
193 location, deploydir = line.split(":")
195 return Deployment(line) # lazy loaded version
197 return Deployment(location, version=ApplicationVersion.parse(deploydir))
199 e.location = location
202 class Application(object):
203 """Represents an application, i.e. mediawiki or phpbb."""
204 #: String name of the application
206 #: Dictionary of version strings to :class:`ApplicationVersion`.
207 #: See also :meth:`makeVersion`.
209 #: List of files that need to be modified when parametrizing.
210 #: This is a class-wide constant, and should not normally be modified.
211 parametrized_files = []
212 def __init__(self, name):
216 self._extractors = {}
217 self._parametrizers = {}
219 def repository(self):
221 Returns the Git repository that would contain this application.
222 Throws :exc:`NoRepositoryError` if the calculated path does not
225 repo = os.path.join("/afs/athena.mit.edu/contrib/scripts/git/autoinstalls", self.name + ".git")
226 if not os.path.isdir(repo):
227 raise NoRepositoryError(app)
229 def makeVersion(self, version):
231 Creates or retrieves the :class:`ApplicationVersion` singleton for the
234 if version not in self.versions:
235 self.versions[version] = ApplicationVersion(distutils.version.LooseVersion(version), self)
236 return self.versions[version]
237 def extract(self, deployment):
238 """Extracts wizard variables from a deployment."""
240 for k,extractor in self.extractors.items():
241 result[k] = extractor(deployment)
243 def parametrize(self, deployment, dir):
245 Takes a generic source checkout at dir and parametrizes
246 it according to the values of deployment.
248 variables = deployment.extract()
249 for file in self.parametrized_files:
250 fullpath = os.path.join(dir, file)
251 f = open(fullpath, "r")
254 for key, value in variables.items():
255 if value is None: continue
256 contents = contents.replace(key, value)
257 tmp = tempfile.NamedTemporaryFile(delete=False)
260 os.rename(tmp.name, fullpath)
262 def extractors(self):
264 Dictionary of variable names to extractor functions. These functions
265 take a :class:`Deployment` as an argument and return the value of
266 the variable, or ``None`` if it could not be found.
267 See also :func:`wizard.app.filename_regex_extractor`.
272 """Makes an application, but uses the correct subtype if available."""
274 __import__("wizard.app." + name)
275 return getattr(wizard.app, name).Application(name)
277 return Application(name)
279 class ApplicationVersion(object):
280 """Represents an abstract notion of a version for an application, where
281 ``version`` is a :class:`distutils.version.LooseVersion` and
282 ``application`` is a :class:`Application`."""
283 #: The :class:`distutils.version.LooseVersion` of this instance.
285 #: The :class:`Appliation` of this instance.
287 def __init__(self, version, application):
288 self.version = version
289 self.application = application
291 def scripts_tag(self):
293 Returns the name of the Git tag for this version.
297 Use this function only during migration, as it does
298 not account for the existence of ``-scripts2``.
300 return "v%s-scripts" % self.version
302 return cmp(x.version, y.version)
306 Parses a line from the :term:`versions store` and return
307 :class:`ApplicationVersion`.
309 Use this only for cases when speed is of primary importance;
310 the data in version is unreliable and when possible, you should
311 prefer directly instantiating a Deployment and having it query
312 the autoinstall itself for information.
314 The `value` to parse will vary. For old style installs, it
317 /afs/athena.mit.edu/contrib/scripts/deploy/APP-x.y.z
319 For new style installs, it will look like::
323 name = value.split("/")[-1]
325 if name.find("-") != -1:
326 app, _, version = name.partition("-")
328 # kind of poor, maybe should error. Generally this
329 # will actually result in a not found error
333 raise DeploymentParseError(deploydir)
334 return ApplicationVersion.make(app, version)
336 def make(app, version):
338 Makes/retrieves a singleton :class:`ApplicationVersion` from
339 a``app`` and ``version`` string.
342 # defer to the application for version creation to enforce
344 return applications()[app].makeVersion(version)
346 raise NoSuchApplication(app)
350 class Error(wizard.Error):
351 """Base error class for this module"""
354 class NoSuchApplication(Error):
356 You attempted to reference a :class:`Application` named
357 ``app``, which is not recognized by Wizard.
359 #: The name of the application that does not exist.
361 #: The location of the autoinstall that threw this variable.
362 #: This should be set by error handling code when it is availble.
364 def __init__(self, app):
367 class DeploymentParseError(Error):
369 Could not parse ``value`` from :term:`versions store`.
371 #: The value that failed to parse.
373 #: The location of the autoinstall that threw this variable.
374 #: This should be set by error handling code when it is available.
376 def __init__(self, value):
379 class NoRepositoryError(Error):
381 :class:`Application` does not appear to have a Git repository
382 in the normal location.
384 #: The name of the application that does not have a Git repository.
386 def __init__(self, app):
389 # If you want, you can wrap this up into a registry and access things
390 # through that, but it's not really necessary
392 _application_list = [
393 "mediawiki", "wordpress", "joomla", "e107", "gallery2",
394 "phpBB", "advancedbook", "phpical", "trac", "turbogears", "django",
395 # these are technically deprecated
396 "advancedpoll", "gallery",
401 """Hash table for looking up string application name to instance"""
403 if not _applications:
404 _applications = dict([(n,Application.make(n)) for n in _application_list ])