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 git, log, util
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_store)])
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.isdir(os.path.join(self.location, ".git")):
173 with util.ChangeDirectory(self.location):
174 appname, _, version = git.describe().partition('-')
175 self._app_version = ApplicationVersion.make(appname, version)
177 self._app_version = self.log[-1].version
178 return self._app_version
182 Parses a line from the :term:`versions store`.
186 Use this method only when speed is of the utmost
187 importance. You should prefer to directly create a deployment
188 with only a ``location`` when possible.
192 location, deploydir = line.split(":")
194 return Deployment(line) # lazy loaded version
196 return Deployment(location, version=ApplicationVersion.parse(deploydir))
198 e.location = location
201 class Application(object):
202 """Represents an application, i.e. mediawiki or phpbb."""
203 #: String name of the application
205 #: Dictionary of version strings to :class:`ApplicationVersion`.
206 #: See also :meth:`makeVersion`.
208 #: List of files that need to be modified when parametrizing.
209 #: This is a class-wide constant, and should not normally be modified.
210 parametrized_files = []
211 def __init__(self, name):
215 self._extractors = {}
216 self._parametrizers = {}
217 def repository(self, srv_path):
219 Returns the Git repository that would contain this application.
220 ``srv_path`` corresponds to ``options.srv_path`` from the global baton.
221 Throws :exc:`NoRepositoryError` if the calculated path does not
224 repo = os.path.join(srv_path, self.name + ".git")
225 if not os.path.isdir(repo):
226 repo = os.path.join(srv_path, self.name, ".git")
227 if not os.path.isdir(repo):
228 raise NoRepositoryError(self.name)
230 def makeVersion(self, version):
232 Creates or retrieves the :class:`ApplicationVersion` singleton for the
235 if version not in self.versions:
236 self.versions[version] = ApplicationVersion(distutils.version.LooseVersion(version), self)
237 return self.versions[version]
238 def extract(self, deployment):
239 """Extracts wizard variables from a deployment."""
241 for k,extractor in self.extractors.items():
242 result[k] = extractor(deployment)
244 def parametrize(self, deployment, dir):
246 Takes a generic source checkout at dir and parametrizes
247 it according to the values of deployment.
249 variables = deployment.extract()
250 for file in self.parametrized_files:
251 fullpath = os.path.join(dir, file)
252 f = open(fullpath, "r")
255 for key, value in variables.items():
256 if value is None: continue
257 contents = contents.replace(key, value)
258 tmp = tempfile.NamedTemporaryFile(delete=False)
261 os.rename(tmp.name, fullpath)
263 def extractors(self):
265 Dictionary of variable names to extractor functions. These functions
266 take a :class:`Deployment` as an argument and return the value of
267 the variable, or ``None`` if it could not be found.
268 See also :func:`wizard.app.filename_regex_extractor`.
273 """Makes an application, but uses the correct subtype if available."""
275 __import__("wizard.app." + name)
276 return getattr(wizard.app, name).Application(name)
278 return Application(name)
280 class ApplicationVersion(object):
281 """Represents an abstract notion of a version for an application, where
282 ``version`` is a :class:`distutils.version.LooseVersion` and
283 ``application`` is a :class:`Application`."""
284 #: The :class:`distutils.version.LooseVersion` of this instance.
286 #: The :class:`Application` of this instance.
288 def __init__(self, version, application):
289 self.version = version
290 self.application = application
292 def scripts_tag(self):
294 Returns the name of the Git tag for this version.
298 Use this function only during migration, as it does
299 not account for the existence of ``-scripts2``.
301 return "%s-scripts" % self.pristine_tag
303 def pristine_tag(self):
305 Returns the name of the Git tag for the pristine version corresponding
308 return "%s-%s" % (self.application.name, self.version)
310 return cmp(x.version, y.version)
314 Parses a line from the :term:`versions store` and return
315 :class:`ApplicationVersion`.
317 Use this only for cases when speed is of primary importance;
318 the data in version is unreliable and when possible, you should
319 prefer directly instantiating a Deployment and having it query
320 the autoinstall itself for information.
322 The `value` to parse will vary. For old style installs, it
325 /afs/athena.mit.edu/contrib/scripts/deploy/APP-x.y.z
327 For new style installs, it will look like::
331 name = value.split("/")[-1]
333 if name.find("-") != -1:
334 app, _, version = name.partition("-")
336 # kind of poor, maybe should error. Generally this
337 # will actually result in a not found error
341 raise DeploymentParseError(deploydir)
342 return ApplicationVersion.make(app, version)
344 def make(app, version):
346 Makes/retrieves a singleton :class:`ApplicationVersion` from
347 a``app`` and ``version`` string.
350 # defer to the application for version creation to enforce
352 return applications()[app].makeVersion(version)
354 raise NoSuchApplication(app)
358 class Error(wizard.Error):
359 """Base error class for this module"""
362 class NoSuchApplication(Error):
364 You attempted to reference a :class:`Application` named
365 ``app``, which is not recognized by Wizard.
367 #: The name of the application that does not exist.
369 #: The location of the autoinstall that threw this variable.
370 #: This should be set by error handling code when it is availble.
372 def __init__(self, app):
375 class DeploymentParseError(Error):
377 Could not parse ``value`` from :term:`versions store`.
379 #: The value that failed to parse.
381 #: The location of the autoinstall that threw this variable.
382 #: This should be set by error handling code when it is available.
384 def __init__(self, value):
387 class NoRepositoryError(Error):
389 :class:`Application` does not appear to have a Git repository
390 in the normal location.
392 #: The name of the application that does not have a Git repository.
394 def __init__(self, app):
397 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
399 # If you want, you can wrap this up into a registry and access things
400 # through that, but it's not really necessary
402 _application_list = [
403 "mediawiki", "wordpress", "joomla", "e107", "gallery2",
404 "phpBB", "advancedbook", "phpical", "trac", "turbogears", "django",
405 # these are technically deprecated
406 "advancedpoll", "gallery",
411 """Hash table for looking up string application name to instance"""
413 if not _applications:
414 _applications = dict([(n,Application.make(n)) for n in _application_list ])