]> scripts.mit.edu Git - wizard.git/commitdiff
Implement upgrades for Wordpress, refactoring.
authorEdward Z. Yang <ezyang@mit.edu>
Wed, 25 Nov 2009 05:18:56 +0000 (00:18 -0500)
committerEdward Z. Yang <ezyang@mit.edu>
Wed, 25 Nov 2009 05:18:56 +0000 (00:18 -0500)
* Add some missing imports
* Restructure failure exceptions to have a superclass;
  moved install.Failure over.
* Fix some spurious names
* Added detectVersionFromFile helper method (more to come!)
* Made MediaWiki use the php parametrized files
* Fix some stylistic problems in Sphinx extension.

Signed-off-by: Edward Z. Yang <ezyang@mit.edu>
12 files changed:
TODO
doc/create.rst
doc/module/wizard.app.rst
doc/module/wizard.deploy.rst
doc/module/wizard.install.rst
tests/test-upgrade-wordpress.sh [new file with mode: 0755]
wizard/app/__init__.py
wizard/app/mediawiki.py
wizard/app/wordpress.py
wizard/command/upgrade.py
wizard/install/__init__.py
wizard/sphinx/supplement.py

diff --git a/TODO b/TODO
index 923d0b0b6617e7c1f486a2de7c71cbcaa7ceab05..c5f7aa751265a40672c3ab9f3d6f4072d51a2707 100644 (file)
--- a/TODO
+++ b/TODO
@@ -2,6 +2,24 @@ The Git Autoinstaller
 
 TODO NOW:
 
+- --retry option for install, so it won't complain about a directory already
+  being there.
+- The calling web code invocations are a mess, with stubs living
+  in the install, deploy modules and the real deal living in util.  Furthermore,
+  we use the scripts-specific heuristic to determine where the app
+  lives, and the only reason my test scripts work is because they
+  get manually fed the domain and path by my environment variables.
+
+  We will record the URL used for the initial installation, and save it in
+  .scripts/url.  If autodetection in either direction is
+  available, we verify this value against the actual file path the installation
+  lives in (for the scripts case, we can do a file-level comparison because we
+  know the web root of any given file).  If they mismatch, we error out
+  and have someone manually resolve the problem.  If autodetection is not
+  available, we use the saved .scripts/url for operations.
+
+- Test code duplication (app specific parameters), also naming sucks for autocomplete
+- wizard install wordpress should ask for password
 - Test code should auto-nuke the database using `wizard remove` before doing a new install
 - git diff :1:$file :2:$file to find out what the user did, or is it :3:?
 - Document how to fix a broken upgrade
index cce7047929bfa423034e3cc740184445a126d00d..352cc63aa4e9118eb3c49496f21eb6af4857a4ac 100644 (file)
@@ -117,6 +117,7 @@ create :file:`$WIZARD/wizard/app/wordpress.py` and fill it in with a bare bones
     import os
     import re
     import logging
+    import distutils
 
     from wizard import app, install, resolve, sql, util
     from wizard.app import php
index fad4854398e47c5e59550d41bbc5dc7cc1fba028..d876c6bddfd762b6d7ef3cebc9e27178f4b71f61 100644 (file)
@@ -25,14 +25,21 @@ Functions
 Exceptions
 ----------
 .. autoexception:: Error
-.. autoexception:: RecoverableFailure
-    :members:
 .. autoexception:: NoRepositoryError
     :members:
 .. autoexception:: DeploymentParseError
     :members:
 .. autoexception:: NoSuchApplication
     :members:
+
+Failures
+''''''''
+.. autoexception:: Failure
+.. autoexception:: InstallFailure
+    :members:
+.. autoexception:: RecoverableInstallFailure
+    :members:
+    :show-inheritance:
 .. autoexception:: UpgradeFailure
     :members:
 .. autoexception:: UpgradeVerificationFailure
index 2182263d7148a39edd7c8f1d563c26d14cccf7d1..cffdf72e1d2ec90dec815c8713fa0a66d734948d 100644 (file)
@@ -31,8 +31,6 @@ Exceptions
     :members:
 .. autoexception:: CorruptedAutoinstallError
     :members:
-.. autoexception:: NotAutoinstallError
-    :members:
 .. autoexception:: NoTagError
     :members:
 .. autoexception:: NoLocalTagError
index 91d59b6faa67aefeeb96ca80ccfea01aec1ec784..3e8091d74e231a36b33a83708c207f97f4ad95a9 100644 (file)
@@ -47,8 +47,6 @@ Functions
 Exceptions
 ----------
 .. autoexception:: Error
-.. autoexception:: Failure
-    :show-inheritance:
 .. autoexception:: StrategyFailed
     :show-inheritance:
 .. autoexception:: UnrecognizedPreloads
diff --git a/tests/test-upgrade-wordpress.sh b/tests/test-upgrade-wordpress.sh
new file mode 100755 (executable)
index 0000000..ef3dfbd
--- /dev/null
@@ -0,0 +1,7 @@
+#!/bin/bash -e
+
+TESTNAME="upgrade_wordpress"
+source ./setup
+
+wizard install wordpress-$VERSION-scripts "$TESTDIR" --non-interactive -- --title="My Blog"
+wizard upgrade "$TESTDIR"
index 0e6bd1aac76c440f0e0e0f3b2c9bd9ac069f5984..ab4d0080423538c7acdb71d932586c87bb97d3ba 100644 (file)
@@ -230,6 +230,16 @@ class Application(object):
         should provide an implementation.
         """
         raise NotImplementedError
+    def detectVersionFromFile(self, filename, regex):
+        """
+        Helper method that detects a version by using a regular expression
+        from a file.  The regexed value is passed through :mod:`shlex`.
+        This assumes that the current working directory is the deployment.
+        """
+        contents = open(filename).read()
+        match = regex.search(contents)
+        if not match: return None
+        return distutils.version.LooseVersion(shlex.split(match.group(2))[0])
     def download(self, version):
         """
         Returns a URL that can be used to download a tarball of ``version`` of
@@ -516,7 +526,7 @@ def get_mysql_args(d):
     # XXX: add support for getting these out of options
     vars = d.extract()
     if 'WIZARD_DBNAME' not in vars:
-        raise app.BackupFailure("Could not determine database name")
+        raise BackupFailure("Could not determine database name")
     triplet = scripts.get_sql_credentials(vars)
     args = []
     if triplet is not None:
@@ -530,19 +540,6 @@ class Error(wizard.Error):
     """Generic error class for this module."""
     pass
 
-class RecoverableFailure(Error):
-    """
-    The installer failed, but we were able to determine what the
-    error was, and should give the user a second chance if we were
-    running interactively.
-    """
-    #: List of the errors that were found.
-    errors = None
-    def __init__(self, errors):
-        self.errors = errors
-    def __str__(self):
-        return """Installation failed due to the following errors: %s""" % ", ".join(self.errors)
-
 class NoRepositoryError(Error):
     """
     :class:`Application` does not appear to have a Git repository
@@ -580,7 +577,32 @@ class NoSuchApplication(Error):
     def __init__(self, app):
         self.app = app
 
-class UpgradeFailure(Error):
+class Failure(Error):
+    """
+    Represents a failure when performing some double-dispatched operation
+    such as an installation or an upgrade.  Failure classes are postfixed
+    with Failure, not Error.
+    """
+    pass
+
+class InstallFailure(Error):
+    """Installation failed for unknown reason."""
+    pass
+
+class RecoverableInstallFailure(InstallFailure):
+    """
+    Installation failed, but we were able to determine what the
+    error was, and should give the user a second chance if we were
+    running interactively.
+    """
+    #: List of the errors that were found.
+    errors = None
+    def __init__(self, errors):
+        self.errors = errors
+    def __str__(self):
+        return """Installation failed due to the following errors: %s""" % ", ".join(self.errors)
+
+class UpgradeFailure(Failure):
     """Upgrade script failed."""
     #: String details of failure (possibly stdout or stderr output)
     details = None
@@ -593,7 +615,7 @@ ERROR: Upgrade script failed, details:
 
 %s""" % self.details
 
-class UpgradeVerificationFailure(Error):
+class UpgradeVerificationFailure(Failure):
     """Upgrade script passed, but website wasn't accessible afterwards"""
     #: String details of failure (possibly stdout or stderr output)
     details = None
@@ -606,7 +628,7 @@ ERROR: Upgrade script passed, but website wasn't accessible afterwards. Details:
 
 %s""" % self.details
 
-class BackupFailure(Error):
+class BackupFailure(Failure):
     """Backup script failed."""
     #: String details of failure
     details = None
@@ -619,7 +641,7 @@ ERROR: Backup script failed, details:
 
 %s""" % self.details
 
-class RestoreFailure(Error):
+class RestoreFailure(Failure):
     """Restore script failed."""
     #: String details of failure
     details = None
index 1de77df95430c46218664ebd2498b6cc2d37a803..fc81613872913f3af87b44d6b3cda0cbd578344e 100644 (file)
@@ -24,7 +24,7 @@ seed = util.dictmap(make_filename_regex, {
         })
 
 class Application(app.Application):
-    parametrized_files = ['LocalSettings.php', 'php.ini']
+    parametrized_files = ['LocalSettings.php'] + php.parametrized_files
     deprecated_keys = set(['WIZARD_IP']) | php.deprecated_keys
     extractors = app.make_extractors(seed)
     extractors.update(php.extractors)
@@ -34,11 +34,7 @@ class Application(app.Application):
     def checkConfig(self, deployment):
         return os.path.isfile("LocalSettings.php")
     def detectVersion(self, deployment):
-        contents = deployment.read("includes/DefaultSettings.php")
-        regex = php.re_var("wgVersion")
-        match = regex.search(contents)
-        if not match: return None
-        return distutils.version.LooseVersion(match.group(2)[1:-1])
+        return self.detectVersionFromFile("includes/DefaultSettings.php", php.re_var("wgVersion"))
     def checkWeb(self, deployment, out=None):
         page = deployment.fetch("/index.php?title=Main_Page")
         if type(out) is list:
@@ -75,8 +71,9 @@ class Application(app.Application):
         if options.verbose or options.debug: print result
         if result.find("Installation successful") == -1:
             if not error_messages:
-                raise install.Failure()
-            raise app.RecoverableFailure(error_messages)
+                raise app.InstallFailure()
+            else:
+                raise app.RecoverableInstallFailure(error_messages)
         os.rename('config/LocalSettings.php', 'LocalSettings.php')
     def upgrade(self, d, version, options):
         sh = shell.Shell()
index 68a451e20cf0fa2758fbddf71892e8e22d6d2bec..ab80825bbb627e0aeb75af5f4dbb3f34952c2839 100644 (file)
@@ -1,6 +1,8 @@
 import os
 import re
 import logging
+import distutils
+import urlparse
 
 from wizard import app, install, resolve, sql, util
 from wizard.app import php
@@ -20,9 +22,6 @@ seed = util.dictmap(make_filename_regex_define, {
     'WIZARD_LOGGED_IN_KEY': 'LOGGED_IN_KEY',
     'WIZARD_NONCE_KEY': 'NONCE_KEY',
     })
-# XXX: I have omitted an implementation for table prefix, on grounds that we
-# do not permit it to be configured. If we do end up growing support for
-# arbitrary table prefixes this should be added.
 
 class Application(app.Application):
     parametrized_files = ['wp-config.php'] + php.parametrized_files
@@ -36,6 +35,11 @@ class Application(app.Application):
         return "http://wordpress.org/wordpress-%s.tar.gz" % version
     def checkConfig(self, deployment):
         return os.path.isfile("wp-config.php")
+    def checkWeb(self, deployment, out=None):
+        page = deployment.fetch("")
+        if type(out) is list:
+            out.append(page)
+        return page.find("<html") != -1
     def detectVersion(self, deployment):
         # XXX: Very duplicate code with MediaWiki; refactor
         contents = deployment.read("wp-includes/version.php")
@@ -43,7 +47,10 @@ class Application(app.Application):
         if not match: return None
         return distutils.version.LooseVersion(match.group(2)[1:-1])
     def prepareMerge(self, deployment):
-        resolve.fix_newlines("wp-config.php")
+        # This file shouldn't really be edited by users, but be careful: it's
+        # stored as DOS and not as UNIX, so you'll get conflicts if you add this line:
+        # resolve.fix_newlines("wp-config.php")
+        pass
     def install(self, version, options):
         # XXX: Hmm... we should figure out something about this
         try:
@@ -74,10 +81,19 @@ class Application(app.Application):
         logging.debug("install.php output\n\n" + result)
         os.chmod(".", old_mode)
         if "Finished" not in result and "Success" not in result:
-            raise install.Failure()
+            raise app.InstallFailure()
 
         # not sure what to do about this
         meta = sql.mysql_connect(options)
         wp_options = meta.tables["wp_options"]
         wp_options.update().where(wp_options.c.option_name == 'siteurl').values(option_value=options.web_path).execute()
         wp_options.update().where(wp_options.c.option_name == 'home').values(option_value="http://%s%s" % (options.web_host, options.web_path)).execute() # XXX: what if missing leading slash; this should be put in a function
+        # should also set the username and admin password
+    def upgrade(self, d, version, options):
+        result = d.fetch("wp-admin/upgrade.php?step=1")
+        if "Upgrade Complete" not in result and "No Upgrade Required" not in result:
+            raise app.UpgradeFailure(result)
+    def backup(self, deployment, backup_dir, options):
+        app.backup_database(backup_dir, deployment)
+    def restore(self, deployment, backup_dir, options):
+        app.restore_database(backup_dir, deployment)
index 0d90a562f1f977bb566ac8f975f27abd9658086f..4cb402ffe388754f3f9a9c5427588c85a096eec9 100644 (file)
@@ -232,7 +232,10 @@ def perform_merge(sh, repo, d, wc, version, use_shm, kib_avail):
             "-p", base_virtual_commit, input="", log=True)
     sh.call("git", "checkout", user_virtual_commit, "--")
     wc.prepareMerge()
-    sh.call("git", "commit", "--amend", "-a", "-m", "amendment")
+    try:
+        sh.call("git", "commit", "--amend", "-a", "-m", "amendment")
+    except shell.CallError as e:
+        pass
     try:
         sh.call("git", "merge", next_virtual_commit)
     except shell.CallError as e:
index 609c02c0d47b3885ea06faefd1df3389a989f6a6..e7bf46bb7e92f2109fdb99f448e027d823e11302 100644 (file)
@@ -174,10 +174,6 @@ class Arg(object):
     #: If true, is a password
     password = False
     @property
-    def option(self):
-        """Full string of the option."""
-        return attr_to_option(self.name)
-    @property
     def envname(self):
         """Name of the environment variable containing this arg."""
         return 'WIZARD_' + self.name.upper()
@@ -340,11 +336,6 @@ class Error(wizard.Error):
     """Base error class for this module."""
     pass
 
-# XXX: This is in the wrong place
-class Failure(Error):
-    """Installation failed."""
-    pass
-
 class StrategyFailed(Error):
     """Strategy couldn't figure out values."""
     pass
index b0c976e0eb6e792beec698c1cfa4a31c4aca94e3..64da185cbaf71836eb24d53eb018454aca030469 100644 (file)
@@ -1,6 +1,5 @@
-from docutils import nodes
-from sphinx.util.compat import make_admonition
-from sphinx.util.compat import Directive
+import docutils
+import sphinx.util.compat
 
 def setup(app):
     app.add_node(supplement,
@@ -9,7 +8,7 @@ def setup(app):
                  text=(visit_supplement_node, depart_supplement_node))
     app.add_directive('supplement', SupplementDirective)
 
-class supplement(nodes.Admonition, nodes.Element):
+class supplement(docutils.nodes.Admonition, docutils.nodes.Element):
     pass
 
 def visit_supplement_node(self, node):
@@ -18,15 +17,15 @@ def visit_supplement_node(self, node):
 def depart_supplement_node(self, node):
     self.depart_admonition(node)
 
-class SupplementDirective(Directive):
+class SupplementDirective(sphinx.util.compat.Directive):
     has_content = True
     optional_arguments = 1
     final_argument_whitespace = True
     def run(self):
-        text = _('Supplement')
+        text = 'Supplement'
         if len(self.arguments) > 0:
-            text = "For %s" % _(self.arguments[0])
-        return make_admonition(supplement, self.name, [text], self.options,
+            text = "For %s" % self.arguments[0]
+        return sphinx.util.compat.make_admonition(supplement, self.name, [text], self.options,
                              self.content, self.lineno, self.content_offset,
                              self.block_text, self.state, self.state_machine)