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
15 from wizard import git, old_log, util
17 ## -- Global Functions --
19 def get_install_lines(versions_store):
21 Low level function that retrieves a list of lines from the
22 :term:`versions store` that can be passed to :meth:`Deployment.parse`.
24 if os.path.isfile(versions_store):
25 return fileinput.input([versions_store])
26 return fileinput.input([versions_store + "/" + f for f in sorted(os.listdir(versions_store))])
28 def parse_install_lines(show, versions_store, yield_errors = False):
30 Generator function for iterating through all autoinstalls.
31 Each item is an instance of :class:`Deployment`, or possibly
32 a :class:`wizard.deploy.Error` if ``yield_errors`` is ``True``. You can
33 filter out applications and versions by specifying ``app``
34 or ``app-1.2.3`` in ``show``. This function may generate
37 if not show: show = applications()
38 show = frozenset(show)
39 for line in get_install_lines(versions_store):
42 d = Deployment.parse(line)
43 name = d.application.name
44 except NoSuchApplication as e:
49 # we consider this a worse error
50 logging.warning("Error with '%s'" % line.rstrip())
53 if name + "-" + str(d.version) in show or name in show:
60 ## -- Model Objects --
62 class Deployment(object):
64 Represents a deployment of an autoinstall, e.g. directory in a user's
65 web_scripts that has ``.scripts`` directory or ``.scripts-version``
66 file in it. Supply ``version`` with an :class:`ApplicationVersion` only if
67 you were reading from the :term:`versions store` and care about
68 speed (data from there can be stale).
70 #: Absolute path to the deployment
72 def __init__(self, location, version=None):
73 self.location = os.path.abspath(location)
74 self._app_version = version
75 # some cache variables
78 def read(self, file, force = False):
80 Reads a file's contents, possibly from cache unless ``force``
83 if force or file not in self._read_cache:
84 f = open(os.path.join(self.location, file))
85 self._read_cache[file] = f.read()
87 return self._read_cache[file]
90 Extracts all the values of all variables from deployment.
91 These variables may be used for parametrizing generic parent
92 commits and include things such as database access credentials
93 and local configuration.
95 return self.application.extract(self)
96 def parametrize(self, dir):
98 Edits files in ``dir`` to replace WIZARD_* variables with literal
99 instances. This is used for constructing virtual merge bases, and
100 as such dir will generally not equal :attr:`location`.
102 return self.application.parametrize(self, dir)
103 def prepareConfig(self):
105 Edits files in the deployment such that any user-specific configuration
106 is replaced with generic WIZARD_* variables.
108 return self.application.prepareConfig(self)
111 """Whether or not the autoinstalls has been migrated."""
112 return os.path.isdir(self.scripts_dir)
114 def scripts_dir(self):
115 """The absolute path of the ``.scripts`` directory."""
116 return os.path.join(self.location, '.scripts')
118 def old_version_file(self):
120 The absolute path of either ``.scripts-version`` (for unmigrated
121 installs) or ``.scripts/version``.
125 Use of this is discouraged for migrated installs.
128 return os.path.join(self.scripts_dir, 'old-version')
130 return os.path.join(self.location, '.scripts-version')
132 def version_file(self):
133 """The absolute path of the ``.scripts/version`` file."""
134 return os.path.join(self.scripts_dir, 'version')
136 def application(self):
137 """The :class:`Application` of this deployment."""
138 return self.app_version.application
142 The :class:`wizard.old_log.Log` of this deployment. This
143 is only applicable to un-migrated autoinstalls.
145 if not self._old_log:
146 self._old_log = old_log.DeployLog.load(self)
151 The :class:`distutils.version.LooseVersion` of this
154 return self.app_version.version
156 def app_version(self):
157 """The :class:`ApplicationVersion` of this deployment."""
158 if not self._app_version:
159 if os.path.isdir(os.path.join(self.location, ".git")):
160 with util.ChangeDirectory(self.location):
161 appname, _, version = git.describe().partition('-')
162 self._app_version = ApplicationVersion.make(appname, version)
164 self._app_version = self.old_log[-1].version
165 return self._app_version
169 Parses a line from the :term:`versions store`.
173 Use this method only when speed is of the utmost
174 importance. You should prefer to directly create a deployment
175 with only a ``location`` when possible.
179 location, deploydir = line.split(":")
181 return Deployment(line) # lazy loaded version
183 return Deployment(location, version=ApplicationVersion.parse(deploydir))
185 e.location = location
188 class Application(object):
189 """Represents an application, i.e. mediawiki or phpbb."""
190 #: String name of the application
192 #: Dictionary of version strings to :class:`ApplicationVersion`.
193 #: See also :meth:`makeVersion`.
195 #: List of files that need to be modified when parametrizing.
196 #: This is a class-wide constant, and should not normally be modified.
197 parametrized_files = []
198 def __init__(self, name):
202 self._extractors = {}
203 self._substitutions = {}
204 def repository(self, srv_path):
206 Returns the Git repository that would contain this application.
207 ``srv_path`` corresponds to ``options.srv_path`` from the global baton.
208 Throws :exc:`NoRepositoryError` if the calculated path does not
211 repo = os.path.join(srv_path, self.name + ".git")
212 if not os.path.isdir(repo):
213 repo = os.path.join(srv_path, self.name, ".git")
214 if not os.path.isdir(repo):
215 raise NoRepositoryError(self.name)
217 def makeVersion(self, version):
219 Creates or retrieves the :class:`ApplicationVersion` singleton for the
222 if version not in self.versions:
223 self.versions[version] = ApplicationVersion(distutils.version.LooseVersion(version), self)
224 return self.versions[version]
225 def extract(self, deployment):
226 """Extracts wizard variables from a deployment."""
228 for k,extractor in self.extractors.items():
229 result[k] = extractor(deployment)
231 def parametrize(self, deployment, dir):
233 Takes a generic source checkout at dir and parametrizes
234 it according to the values of deployment.
236 variables = deployment.extract()
237 for file in self.parametrized_files:
238 fullpath = os.path.join(dir, file)
240 f = open(fullpath, "r")
245 for key, value in variables.items():
246 if value is None: continue
247 contents = contents.replace(key, value)
248 tmp = tempfile.NamedTemporaryFile(delete=False)
251 os.rename(tmp.name, fullpath)
252 def prepareConfig(self, deployment):
254 Takes a deployment and replaces any explicit instances
255 of a configuration variable with generic WIZARD_* constants.
256 There is a sane default implementation built on substitutions;
257 you can override this method to provide arbitrary extra
260 for key, subst in self.substitutions.items():
261 subs = subst(deployment)
262 if not subs and key not in self.deprecated_keys:
263 logging.warning("No substitutions for %s" % key)
264 def install(self, options):
266 Run for 'wizard configure' (and, by proxy, 'wizard install')
267 to configure an application.
270 def upgrade(self, options):
272 Run for 'wizard upgrade' to upgrade database schemas and other
273 non-versioned data in an application.
277 def extractors(self):
279 Dictionary of variable names to extractor functions. These functions
280 take a :class:`Deployment` as an argument and return the value of
281 the variable, or ``None`` if it could not be found.
282 See also :func:`wizard.app.filename_regex_extractor`.
286 def substitutions(self):
288 Dictionary of variable names to substitution functions. These functions
289 take a :class:`Deployment` as an argument and modify the deployment such
290 that an explicit instance of the variable is released with the generic
291 WIZARD_* constant. See also :func:`wizard.app.filename_regex_substitution`.
296 """Makes an application, but uses the correct subtype if available."""
298 __import__("wizard.app." + name)
299 return getattr(wizard.app, name).Application(name)
301 return Application(name)
303 class ApplicationVersion(object):
304 """Represents an abstract notion of a version for an application, where
305 ``version`` is a :class:`distutils.version.LooseVersion` and
306 ``application`` is a :class:`Application`."""
307 #: The :class:`distutils.version.LooseVersion` of this instance.
309 #: The :class:`Application` of this instance.
311 def __init__(self, version, application):
312 self.version = version
313 self.application = application
315 def scripts_tag(self):
317 Returns the name of the Git tag for this version.
319 end = str(self.version).partition('-scripts')[2].partition('-')[0]
320 return "%s-scripts%s" % (self.pristine_tag, end)
322 def pristine_tag(self):
324 Returns the name of the Git tag for the pristine version corresponding
327 return "%s-%s" % (self.application.name, str(self.version).partition('-scripts')[0])
329 return cmp(x.version, y.version)
333 Parses a line from the :term:`versions store` and return
334 :class:`ApplicationVersion`.
336 Use this only for cases when speed is of primary importance;
337 the data in version is unreliable and when possible, you should
338 prefer directly instantiating a Deployment and having it query
339 the autoinstall itself for information.
341 The `value` to parse will vary. For old style installs, it
344 /afs/athena.mit.edu/contrib/scripts/deploy/APP-x.y.z
346 For new style installs, it will look like::
350 name = value.split("/")[-1]
352 if name.find("-") != -1:
353 app, _, version = name.partition("-")
355 # kind of poor, maybe should error. Generally this
356 # will actually result in a not found error
360 raise DeploymentParseError(deploydir)
361 return ApplicationVersion.make(app, version)
363 def make(app, version):
365 Makes/retrieves a singleton :class:`ApplicationVersion` from
366 a``app`` and ``version`` string.
369 # defer to the application for version creation to enforce
371 return applications()[app].makeVersion(version)
373 raise NoSuchApplication(app)
377 class Error(wizard.Error):
378 """Base error class for this module"""
381 class NoSuchApplication(Error):
383 You attempted to reference a :class:`Application` named
384 ``app``, which is not recognized by Wizard.
386 #: The name of the application that does not exist.
388 #: The location of the autoinstall that threw this variable.
389 #: This should be set by error handling code when it is availble.
391 def __init__(self, app):
394 class DeploymentParseError(Error):
396 Could not parse ``value`` from :term:`versions store`.
398 #: The value that failed to parse.
400 #: The location of the autoinstall that threw this variable.
401 #: This should be set by error handling code when it is available.
403 def __init__(self, value):
406 class NoRepositoryError(Error):
408 :class:`Application` does not appear to have a Git repository
409 in the normal location.
411 #: The name of the application that does not have a Git repository.
413 def __init__(self, app):
416 return """Could not find Git repository for '%s'. If you would like to use a local version, try specifying --srv-path or WIZARD_SRV_PATH.""" % self.app
418 # If you want, you can wrap this up into a registry and access things
419 # through that, but it's not really necessary
421 _application_list = [
422 "mediawiki", "wordpress", "joomla", "e107", "gallery2",
423 "phpBB", "advancedbook", "phpical", "trac", "turbogears", "django",
424 # these are technically deprecated
425 "advancedpoll", "gallery",
430 """Hash table for looking up string application name to instance"""
432 if not _applications:
433 _applications = dict([(n,Application.make(n)) for n in _application_list ])