]> scripts.mit.edu Git - wizard.git/blobdiff - wizard/app/__init__.py
Add inclusions and exclusions for web verification.
[wizard.git] / wizard / app / __init__.py
index 597dbc0c9465d2181073cd9480b70a760c08ab4f..ae99e3e58dfa34aebe3916c4f370f186a2bcfe00 100644 (file)
@@ -28,6 +28,10 @@ import shlex
 import logging
 import shutil
 import sqlalchemy
+import random
+import string
+import urlparse
+import tempfile
 
 import wizard
 from wizard import resolve, scripts, shell, util
@@ -47,6 +51,9 @@ def applications():
         _applications = dict([(n,Application.make(n)) for n in _application_list ])
     return _applications
 
+def getApplication(appname):
+    """Retrieves application instance given a name"""
+    return applications()[appname]
 
 class Application(object):
     """
@@ -67,7 +74,9 @@ class Application(object):
     parametrized_files = []
     #: Keys that are used in older versions of the application, but
     #: not for the most recent version.
-    deprecated_keys = []
+    deprecated_keys = set()
+    #: Keys that we can simply generate random strings for if they're missing
+    random_keys = set()
     #: Dictionary of variable names to extractor functions.  These functions
     #: take a :class:`wizard.deploy.Deployment` as an argument and return the value of
     #: the variable, or ``None`` if it could not be found.
@@ -124,6 +133,10 @@ class Application(object):
         result = {}
         for k,extractor in self.extractors.items():
             result[k] = extractor(deployment)
+        # XXX: ugh... we have to do quoting
+        for k in self.random_keys:
+            if result[k] is None:
+                result[k] = "'%s'" % ''.join(random.choice(string.letters + string.digits) for i in xrange(30))
         return result
     def dsn(self, deployment):
         """
@@ -171,6 +184,34 @@ class Application(object):
         # XXX: You'd have to put support for an explicit different database
         # type here
         return sqlalchemy.engine.url.URL(self.database, username=user, password=password, host=host, database=database)
+    def url(self, deployment):
+        """
+        Returns the deployment specific web URL.  Uses the override file
+        in :file:`.scripts` if it exists, and otherwise attempt to extract
+        the variables from the source files.
+
+        This function might return ``None``, which indicates we couldn't figure
+        it out.
+        """
+        url = self.urlFromOverride(deployment)
+        if url:
+            return url
+        return self.urlFromExtract(deployment)
+    def urlFromOverride(self, deployment):
+        """
+        Extracts URL from explicit url override file.
+        """
+        try:
+            return urlparse.urlparse(open(deployment.url_file).read().strip())
+        except IOError:
+            return None
+    def urlFromExtract(self, deployment):
+        """
+        Extracts URL from a deployment, and returns ``None`` if we can't
+        figure it out.  Default implementation is to fail; we might
+        do something clever with extractable variables in the future.
+        """
+        return None
     def parametrize(self, deployment, ref_deployment):
         """
         Takes a generic source checkout and parametrizes it according to the
@@ -199,9 +240,22 @@ class Application(object):
         default implementation uses :attr:`resolutions`.
         """
         resolved = True
-        sh = shell.Shell()
-        for status in sh.eval("git", "ls-files", "--unmerged").splitlines():
-            file = status.split()[-1]
+        files = set()
+        files = {}
+        for status in shell.eval("git", "ls-files", "--unmerged").splitlines():
+            mode, hash, role, name = status.split()
+            files.setdefault(name, set()).add(int(role))
+        for file, roles in files.items():
+            # some automatic resolutions
+            if 1 not in roles and 2 not in roles and 3 in roles:
+                # upstream added a file, but it conflicted for whatever reason
+                shell.call("git", "add", file)
+                continue
+            elif 1 in roles and 2 not in roles and 3 in roles:
+                # user deleted the file, but upstream changed it
+                shell.call("git", "rm", file)
+                continue
+            # manual resolutions
             if file in self.resolutions:
                 contents = open(file, "r").read()
                 for spec, result in self.resolutions[file]:
@@ -211,7 +265,7 @@ class Application(object):
                         logging.info("Did resolution with spec:\n" + spec)
                 open(file, "w").write(contents)
                 if not resolve.is_conflict(contents):
-                    sh.call("git", "add", file)
+                    shell.call("git", "add", file)
                 else:
                     resolved = False
             else:
@@ -319,17 +373,24 @@ class Application(object):
             not to depend on pages that are not the main page.
         """
         raise NotImplementedError
-    def checkWebPage(self, deployment, page, output):
+    def checkWebPage(self, deployment, page, outputs=[], exclude=[]):
         """
         Checks if a given page of an autoinstall contains a particular string.
         """
         page = deployment.fetch(page)
-        result = page.find(output) != -1
-        if result:
+        for x in exclude:
+            if page.find(x) != -1:
+                logging.info("checkWebPage (failed due to %s):\n\n%s", x, page)
+                return False
+        votes = 0
+        for output in outputs:
+            votes += page.find(output) != -1
+        if votes > len(outputs) / 2:
             logging.debug("checkWebPage (passed):\n\n" + page)
+            return True
         else:
             logging.info("checkWebPage (failed):\n\n" + page)
-        return result
+            return False
     def checkConfig(self, deployment):
         """
         Checks whether or not an autoinstall has been configured/installed
@@ -340,6 +401,23 @@ class Application(object):
         # bogus config files in the -scripts versions of installs.  Maybe
         # we should check a hash or something?
         raise NotImplementedError
+    def researchFilter(self, filename, added, deleted):
+        """
+        Allows an application to selectively ignore certain diffstat signatures
+        during research; for example, configuration files will have a very
+        specific set of changes, so ignore them; certain installation files
+        may be removed, etc.  Return ``True`` if a diffstat signature should be
+        ignored,
+        """
+        return False
+    def researchVerbose(self, filename):
+        """
+        Allows an application to exclude certain dirty files from the output
+        report; usually this will just be parametrized files, since those are
+        guaranteed to have changes.  Return ``True`` if a file should only
+        be displayed in verbose mode.
+        """
+        return filename in self.parametrized_files
     @staticmethod
     def make(name):
         """Makes an application, but uses the correct subtype if available."""
@@ -572,13 +650,14 @@ def backup_database(outdir, deployment):
         raise NotImplementedError
 
 def backup_mysql_database(outdir, deployment):
-    sh = shell.Shell()
+    """
+    Database backups for MySQL using the :command:`mysqldump` utility.
+    """
     outfile = os.path.join(outdir, "db.sql")
     try:
-        sh.call("mysqldump", "--compress", "-r", outfile, *get_mysql_args(deployment.dsn))
-        sh.call("gzip", "--best", outfile)
+        shell.call("mysqldump", "--compress", "-r", outfile, *get_mysql_args(deployment.dsn))
+        shell.call("gzip", "--best", outfile)
     except shell.CallError as e:
-        shutil.rmtree(outdir)
         raise BackupFailure(e.stderr)
 
 def restore_database(backup_dir, deployment):
@@ -592,13 +671,15 @@ def restore_database(backup_dir, deployment):
         raise NotImplementedError
 
 def restore_mysql_database(backup_dir, deployment):
-    sh = shell.Shell()
+    """
+    Database restoration for MySQL by piping SQL commands into :command:`mysql`.
+    """
     if not os.path.exists(backup_dir):
         raise RestoreFailure("Backup %s doesn't exist", backup_dir.rpartition("/")[2])
     sql = open(os.path.join(backup_dir, "db.sql"), 'w+')
-    sh.call("gunzip", "-c", os.path.join(backup_dir, "db.sql.gz"), stdout=sql)
+    shell.call("gunzip", "-c", os.path.join(backup_dir, "db.sql.gz"), stdout=sql)
     sql.seek(0)
-    sh.call("mysql", *get_mysql_args(deployment.dsn), stdin=sql)
+    shell.call("mysql", *get_mysql_args(deployment.dsn), stdin=sql)
     sql.close()
 
 def remove_database(deployment):
@@ -606,10 +687,9 @@ def remove_database(deployment):
     Generic database removal function.  Actually, not so generic because we
     go and check if we're on scripts and if we are run a different command.
     """
-    sh = shell.Shell()
     if deployment.dsn.host == "sql.mit.edu":
         try:
-            sh.call("/mit/scripts/sql/bin/drop-database", deployment.dsn.database)
+            shell.call("/mit/scripts/sql/bin/drop-database", deployment.dsn.database)
             return
         except shell.CallError:
             pass