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 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)
109 def updateVersion(self, version):
111 Update the version of this deployment.
113 This method will update the version of this deployment in memory
114 and on disk. It doesn't actually do an upgrade. The version
115 string you pass here should have ``-scripts`` as a suffix.
117 self._app_version = self.application.makeVersion(version)
118 f = open(os.path.join(self.scripts_dir, 'version'), 'w')
119 f.write(self.application.name + '-' + version + "\n")
121 def scriptsifyVersion(self):
123 Converts from ``v1.0`` to ``v1.0-scripts``; use at end of migration.
127 This makes the assumption that a migration will be to
128 a ``-scripts`` tag and not a ``-scripts2`` tag. If you botch
129 migration, blow away the tag and try again.
131 self.updateVersion(self.app_version.scripts_tag)
134 """Whether or not the autoinstalls has been migrated."""
135 return os.path.isdir(self.scripts_dir)
137 def scripts_dir(self):
138 """The absolute path of the ``.scripts`` directory."""
139 return os.path.join(self.location, '.scripts')
141 def old_version_file(self):
143 The absolute path of either ``.scripts-version`` (for unmigrated
144 installs) or ``.scripts/version``.
148 Use of this is discouraged for migrated installs.
151 return os.path.join(self.scripts_dir, 'old-version')
153 return os.path.join(self.location, '.scripts-version')
155 def version_file(self):
156 """The absolute path of the ``.scripts/version`` file."""
157 return os.path.join(self.scripts_dir, 'version')
159 def application(self):
160 """The :class:`Application` of this deployment."""
161 return self.app_version.application
165 The :class:`wizard.old_log.Log` of this deployment. This
166 is only applicable to un-migrated autoinstalls.
168 if not self._old_log:
169 self._old_log = old_log.DeployLog.load(self)
174 The :class:`distutils.version.LooseVersion` of this
177 return self.app_version.version
179 def app_version(self):
180 """The :class:`ApplicationVersion` of this deployment."""
181 if not self._app_version:
182 if os.path.isdir(os.path.join(self.location, ".git")):
183 with util.ChangeDirectory(self.location):
184 appname, _, version = git.describe().partition('-')
185 self._app_version = ApplicationVersion.make(appname, version)
187 self._app_version = self.old_log[-1].version
188 return self._app_version
192 Parses a line from the :term:`versions store`.
196 Use this method only when speed is of the utmost
197 importance. You should prefer to directly create a deployment
198 with only a ``location`` when possible.
202 location, deploydir = line.split(":")
204 return Deployment(line) # lazy loaded version
206 return Deployment(location, version=ApplicationVersion.parse(deploydir))
208 e.location = location
211 class Application(object):
212 """Represents an application, i.e. mediawiki or phpbb."""
213 #: String name of the application
215 #: Dictionary of version strings to :class:`ApplicationVersion`.
216 #: See also :meth:`makeVersion`.
218 #: List of files that need to be modified when parametrizing.
219 #: This is a class-wide constant, and should not normally be modified.
220 parametrized_files = []
221 def __init__(self, name):
225 self._extractors = {}
226 self._substitutions = {}
227 def repository(self, srv_path):
229 Returns the Git repository that would contain this application.
230 ``srv_path`` corresponds to ``options.srv_path`` from the global baton.
231 Throws :exc:`NoRepositoryError` if the calculated path does not
234 repo = os.path.join(srv_path, self.name + ".git")
235 if not os.path.isdir(repo):
236 repo = os.path.join(srv_path, self.name, ".git")
237 if not os.path.isdir(repo):
238 raise NoRepositoryError(self.name)
240 def makeVersion(self, version):
242 Creates or retrieves the :class:`ApplicationVersion` singleton for the
245 if version not in self.versions:
246 self.versions[version] = ApplicationVersion(distutils.version.LooseVersion(version), self)
247 return self.versions[version]
248 def extract(self, deployment):
249 """Extracts wizard variables from a deployment."""
251 for k,extractor in self.extractors.items():
252 result[k] = extractor(deployment)
254 def parametrize(self, deployment, dir):
256 Takes a generic source checkout at dir and parametrizes
257 it according to the values of deployment.
259 variables = deployment.extract()
260 for file in self.parametrized_files:
261 fullpath = os.path.join(dir, file)
262 f = open(fullpath, "r")
265 for key, value in variables.items():
266 if value is None: continue
267 contents = contents.replace(key, value)
268 tmp = tempfile.NamedTemporaryFile(delete=False)
271 os.rename(tmp.name, fullpath)
272 def prepareConfig(self, deployment):
274 Takes a deployment and replaces any explicit instances
275 of a configuration variable with generic WIZARD_* constants.
276 There is a sane default implementation built on substitutions;
277 you can override this method to provide arbitrary extra
280 for key, subst in self.substitutions.items():
281 subs = subst(deployment)
282 if not subs and key not in self.deprecated_keys:
283 logging.warning("No substitutions for %s" % key)
285 def extractors(self):
287 Dictionary of variable names to extractor functions. These functions
288 take a :class:`Deployment` as an argument and return the value of
289 the variable, or ``None`` if it could not be found.
290 See also :func:`wizard.app.filename_regex_extractor`.
294 def substitutions(self):
296 Dictionary of variable names to substitution functions. These functions
297 take a :class:`Deployment` as an argument and modify the deployment such
298 that an explicit instance of the variable is released with the generic
299 WIZARD_* constant. See also :func:`wizard.app.filename_regex_substitution`.
304 """Makes an application, but uses the correct subtype if available."""
306 __import__("wizard.app." + name)
307 return getattr(wizard.app, name).Application(name)
309 return Application(name)
311 class ApplicationVersion(object):
312 """Represents an abstract notion of a version for an application, where
313 ``version`` is a :class:`distutils.version.LooseVersion` and
314 ``application`` is a :class:`Application`."""
315 #: The :class:`distutils.version.LooseVersion` of this instance.
317 #: The :class:`Application` of this instance.
319 def __init__(self, version, application):
320 self.version = version
321 self.application = application
323 def scripts_tag(self):
325 Returns the name of the Git tag for this version.
329 Use this function only during migration, as it does
330 not account for the existence of ``-scripts2``.
332 return "%s-scripts" % self.pristine_tag
334 def pristine_tag(self):
336 Returns the name of the Git tag for the pristine version corresponding
339 return "%s-%s" % (self.application.name, self.version)
341 return cmp(x.version, y.version)
345 Parses a line from the :term:`versions store` and return
346 :class:`ApplicationVersion`.
348 Use this only for cases when speed is of primary importance;
349 the data in version is unreliable and when possible, you should
350 prefer directly instantiating a Deployment and having it query
351 the autoinstall itself for information.
353 The `value` to parse will vary. For old style installs, it
356 /afs/athena.mit.edu/contrib/scripts/deploy/APP-x.y.z
358 For new style installs, it will look like::
362 name = value.split("/")[-1]
364 if name.find("-") != -1:
365 app, _, version = name.partition("-")
367 # kind of poor, maybe should error. Generally this
368 # will actually result in a not found error
372 raise DeploymentParseError(deploydir)
373 return ApplicationVersion.make(app, version)
375 def make(app, version):
377 Makes/retrieves a singleton :class:`ApplicationVersion` from
378 a``app`` and ``version`` string.
381 # defer to the application for version creation to enforce
383 return applications()[app].makeVersion(version)
385 raise NoSuchApplication(app)
389 class Error(wizard.Error):
390 """Base error class for this module"""
393 class NoSuchApplication(Error):
395 You attempted to reference a :class:`Application` named
396 ``app``, which is not recognized by Wizard.
398 #: The name of the application that does not exist.
400 #: The location of the autoinstall that threw this variable.
401 #: This should be set by error handling code when it is availble.
403 def __init__(self, app):
406 class DeploymentParseError(Error):
408 Could not parse ``value`` from :term:`versions store`.
410 #: The value that failed to parse.
412 #: The location of the autoinstall that threw this variable.
413 #: This should be set by error handling code when it is available.
415 def __init__(self, value):
418 class NoRepositoryError(Error):
420 :class:`Application` does not appear to have a Git repository
421 in the normal location.
423 #: The name of the application that does not have a Git repository.
425 def __init__(self, app):
428 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
430 # If you want, you can wrap this up into a registry and access things
431 # through that, but it's not really necessary
433 _application_list = [
434 "mediawiki", "wordpress", "joomla", "e107", "gallery2",
435 "phpBB", "advancedbook", "phpical", "trac", "turbogears", "django",
436 # these are technically deprecated
437 "advancedpoll", "gallery",
442 """Hash table for looking up string application name to instance"""
444 if not _applications:
445 _applications = dict([(n,Application.make(n)) for n in _application_list ])