Create database upgrade framework
[cascardo/ipsilon.git] / ipsilon / install / ipsilon-server-install
index edcccb6..b4a9085 100755 (executable)
@@ -1,31 +1,16 @@
 #!/usr/bin/python
 #!/usr/bin/python
-#
-# Copyright (C) 2014  Simo Sorce <simo@redhat.com>
-#
-# see file 'COPYING' for use and warranty information
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+# Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING
 
 from ipsilon.login.common import LoginMgrsInstall
 from ipsilon.info.common import InfoProviderInstall
 from ipsilon.providers.common import ProvidersInstall
 from ipsilon.helpers.common import EnvHelpersInstall
 from ipsilon.util.data import UserStore
 
 from ipsilon.login.common import LoginMgrsInstall
 from ipsilon.info.common import InfoProviderInstall
 from ipsilon.providers.common import ProvidersInstall
 from ipsilon.helpers.common import EnvHelpersInstall
 from ipsilon.util.data import UserStore
-from ipsilon.tools import files
+from ipsilon.tools import files, dbupgrade
 import ConfigParser
 import argparse
 import cherrypy
 import ConfigParser
 import argparse
 import cherrypy
+import json
 import logging
 import os
 import pwd
 import logging
 import os
 import pwd
@@ -40,19 +25,15 @@ TEMPLATES = '/usr/share/ipsilon/templates/install'
 CONFDIR = '/etc/ipsilon'
 DATADIR = '/var/lib/ipsilon'
 HTTPDCONFD = '/etc/httpd/conf.d'
 CONFDIR = '/etc/ipsilon'
 DATADIR = '/var/lib/ipsilon'
 HTTPDCONFD = '/etc/httpd/conf.d'
-BINDIR = '/usr/sbin'
+BINDIR = '/usr/libexec'
 STATICDIR = '/usr/share/ipsilon'
 WSGI_SOCKET_PREFIX = None
 
 
 STATICDIR = '/usr/share/ipsilon'
 WSGI_SOCKET_PREFIX = None
 
 
-class ConfigurationError(Exception):
+class ConfigurationError(StandardError):
 
     def __init__(self, message):
 
     def __init__(self, message):
-        super(ConfigurationError, self).__init__(message)
-        self.message = message
-
-    def __str__(self):
-        return repr(self.message)
+        StandardError.__init__(self, message)
 
 
 #Silence cherrypy logging to screen
 
 
 #Silence cherrypy logging to screen
@@ -79,7 +60,15 @@ def openlogs():
         lh = logging.StreamHandler(sys.stderr)
     formatter = logging.Formatter('[%(asctime)s] %(message)s')
     lh.setFormatter(formatter)
         lh = logging.StreamHandler(sys.stderr)
     formatter = logging.Formatter('[%(asctime)s] %(message)s')
     lh.setFormatter(formatter)
+    lh.setLevel(logging.DEBUG)
     logger.addHandler(lh)
     logger.addHandler(lh)
+    logger.propagate = False
+    ch = logging.StreamHandler(sys.stdout)
+    formatter = logging.Formatter('%(message)s')
+    ch.setFormatter(formatter)
+    ch.setLevel(logging.INFO)
+    logger.addHandler(ch)
+    cherrypy.log.error_log.setLevel(logging.DEBUG)
 
 
 def install(plugins, args):
 
 
 def install(plugins, args):
@@ -109,15 +98,17 @@ def install(plugins, args):
                 'sysuser': args['system_user'],
                 'ipsilondir': BINDIR,
                 'staticdir': STATICDIR,
                 'sysuser': args['system_user'],
                 'ipsilondir': BINDIR,
                 'staticdir': STATICDIR,
-                'admindb': args['database_url'] % {
+                'admindb': args['admin_dburi'] or args['database_url'] % {
                     'datadir': args['data_dir'], 'dbname': 'adminconfig'},
                     'datadir': args['data_dir'], 'dbname': 'adminconfig'},
-                'usersdb': args['database_url'] % {
+                'usersdb': args['users_dburi'] or args['database_url'] % {
                     'datadir': args['data_dir'], 'dbname': 'userprefs'},
                     'datadir': args['data_dir'], 'dbname': 'userprefs'},
-                'transdb': args['database_url'] % {
-                    'datadir': args['data_dir'], 'dbname': 'transactions'},
+                'transdb': args['transaction_dburi'] or args['database_url'] %
+                {'datadir': args['data_dir'], 'dbname': 'transactions'},
+                'samlsessionsdb': args['samlsessions_dburi'] or args[
+                    'database_url'] % {'datadir': args['data_dir'],
+                                       'dbname': 'saml2sessions'},
                 'secure': "False" if args['secure'] == "no" else "True",
                 'secure': "False" if args['secure'] == "no" else "True",
-                'debugging': "True" if args['server_debugging'] else "False",
-               }
+                'debugging': "True" if args['server_debugging'] else "False"}
     # Testing database sessions
     if 'session_type' in args:
         confopts['sesstype'] = args['session_type']
     # Testing database sessions
     if 'session_type' in args:
         confopts['sesstype'] = args['session_type']
@@ -129,7 +120,7 @@ def install(plugins, args):
     else:
         confopts['sessopt'] = 'path'
         confopts['sessval'] = os.path.join(args['data_dir'], 'sessions')
     else:
         confopts['sessopt'] = 'path'
         confopts['sessval'] = os.path.join(args['data_dir'], 'sessions')
-    # Whetehr to disable security (for testing)
+    # Whether to disable security (for testing)
     if args['secure'] == 'no':
         confopts['secure'] = "False"
         confopts['sslrequiressl'] = ""
     if args['secure'] == 'no':
         confopts['secure'] = "False"
         confopts['sslrequiressl'] = ""
@@ -163,41 +154,74 @@ def install(plugins, args):
     # components
     cherrypy.config.update(ipsilon_conf)
 
     # components
     cherrypy.config.update(ipsilon_conf)
 
-    # Move pre-existing admin db away
+    # Prepare to allow plugins to save things changed during install
+    changes = {'env_helper': {},
+               'login_manager': {},
+               'info_provider': {},
+               'auth_provider': {}}
+
+    # Move pre-existing dbs away
     admin_db = cherrypy.config['admin.config.db']
     if os.path.exists(admin_db):
         shutil.move(admin_db, '%s.backup.%s' % (admin_db, now))
     admin_db = cherrypy.config['admin.config.db']
     if os.path.exists(admin_db):
         shutil.move(admin_db, '%s.backup.%s' % (admin_db, now))
-
-    # Rebuild user db
     users_db = cherrypy.config['user.prefs.db']
     if os.path.exists(users_db):
         shutil.move(users_db, '%s.backup.%s' % (users_db, now))
     users_db = cherrypy.config['user.prefs.db']
     if os.path.exists(users_db):
         shutil.move(users_db, '%s.backup.%s' % (users_db, now))
+
+    # Initialize initial database schemas
+    dbupgrade.execute_upgrade(ipsilon_conf)
+
+    # Store primary admin
     db = UserStore()
     db.save_user_preferences(args['admin_user'], {'is_admin': 1})
 
     logger.info('Configuring environment helpers')
     for plugin_name in plugins['Environment Helpers']:
         plugin = plugins['Environment Helpers'][plugin_name]
     db = UserStore()
     db.save_user_preferences(args['admin_user'], {'is_admin': 1})
 
     logger.info('Configuring environment helpers')
     for plugin_name in plugins['Environment Helpers']:
         plugin = plugins['Environment Helpers'][plugin_name]
-        if plugin.configure_server(args) == False:
-            print 'Configuration of environment helper %s failed' % plugin_name
+        plugin_changes = {}
+        if plugin.configure_server(args, plugin_changes) == False:
+            msg = 'Configuration of environment helper %s failed' % plugin_name
+            raise ConfigurationError(msg)
+        changes['env_helper'][plugin_name] = plugin_changes
 
     logger.info('Configuring login managers')
     for plugin_name in args['lm_order']:
 
     logger.info('Configuring login managers')
     for plugin_name in args['lm_order']:
-        plugin = plugins['Login Managers'][plugin_name]
-        if plugin.configure(args) == False:
-            print 'Configuration of login manager %s failed' % plugin_name
+        try:
+            plugin = plugins['Login Managers'][plugin_name]
+        except KeyError:
+            sys.exit('Login provider %s not installed' % plugin_name)
+        plugin_changes = {}
+        if plugin.configure(args, plugin_changes) == False:
+            msg = 'Configuration of login manager %s failed' % plugin_name
+            raise ConfigurationError(msg)
+        changes['login_manager'][plugin_name] = plugin_changes
 
     logger.info('Configuring Info provider')
     for plugin_name in plugins['Info Provider']:
         plugin = plugins['Info Provider'][plugin_name]
 
     logger.info('Configuring Info provider')
     for plugin_name in plugins['Info Provider']:
         plugin = plugins['Info Provider'][plugin_name]
-        if plugin.configure(args) == False:
-            print 'Configuration of info provider %s failed' % plugin_name
+        plugin_changes = {}
+        if plugin.configure(args, plugin_changes) == False:
+            msg = 'Configuration of info provider %s failed' % plugin_name
+            raise ConfigurationError(msg)
+        changes['info_provider'][plugin_name] = plugin_changes
 
     logger.info('Configuring Authentication Providers')
     for plugin_name in plugins['Auth Providers']:
         plugin = plugins['Auth Providers'][plugin_name]
 
     logger.info('Configuring Authentication Providers')
     for plugin_name in plugins['Auth Providers']:
         plugin = plugins['Auth Providers'][plugin_name]
-        if plugin.configure(args) == False:
-            print 'Configuration of auth provider %s failed' % plugin_name
+        plugin_changes = {}
+        if plugin.configure(args, plugin_changes) == False:
+            msg = 'Configuration of auth provider %s failed' % plugin_name
+            raise ConfigurationError(msg)
+        changes['auth_provider'][plugin_name] = plugin_changes
+
+    # Save any changes that were made
+    install_changes = os.path.join(instance_conf, 'install_changes')
+    changes = json.dumps(changes)
+    with open(install_changes, 'w+') as f:
+        f.write(changes)
+
+    # Initialize extra database schemas
+    dbupgrade.execute_upgrade(ipsilon_conf)
 
     # Fixup permissions so only the ipsilon user can read these files
     files.fix_user_dirs(instance_conf, opts['system_user'])
 
     # Fixup permissions so only the ipsilon user can read these files
     files.fix_user_dirs(instance_conf, opts['system_user'])
@@ -229,29 +253,38 @@ def uninstall(plugins, args):
         if sure != 'yes':
             raise Exception('Aborting')
 
         if sure != 'yes':
             raise Exception('Aborting')
 
+    # Get the details of what we changed during installation
+    install_changes = os.path.join(instance_conf, 'install_changes')
+    with open(install_changes, 'r') as f:
+        changes = json.loads(f.read())
+
     logger.info('Removing environment helpers')
     for plugin_name in plugins['Environment Helpers']:
         plugin = plugins['Environment Helpers'][plugin_name]
     logger.info('Removing environment helpers')
     for plugin_name in plugins['Environment Helpers']:
         plugin = plugins['Environment Helpers'][plugin_name]
-        if plugin.unconfigure(args) == False:
-            print 'Removal of environment helper %s failed' % plugin_name
+        plugin_changes = changes['env_helper'].get(plugin_name, {})
+        if plugin.unconfigure(args, plugin_changes) == False:
+            logger.info('Removal of environment helper %s failed' % plugin_name)
 
     logger.info('Removing login managers')
 
     logger.info('Removing login managers')
-    for plugin_name in args['lm_order']:
+    for plugin_name in plugins['Login Managers']:
         plugin = plugins['Login Managers'][plugin_name]
         plugin = plugins['Login Managers'][plugin_name]
-        if plugin.unconfigure(args) == False:
-            print 'Removal of login manager %s failed' % plugin_name
+        plugin_changes = changes['login_manager'].get(plugin_name, {})
+        if plugin.unconfigure(args, plugin_changes) == False:
+            logger.info('Removal of login manager %s failed' % plugin_name)
 
     logger.info('Removing Info providers')
     for plugin_name in plugins['Info Provider']:
         plugin = plugins['Info Provider'][plugin_name]
 
     logger.info('Removing Info providers')
     for plugin_name in plugins['Info Provider']:
         plugin = plugins['Info Provider'][plugin_name]
-        if plugin.unconfigure(args) == False:
-            print 'Removal of info provider %s failed' % plugin_name
+        plugin_changes = changes['info_provider'].get(plugin_name, {})
+        if plugin.unconfigure(args, plugin_changes) == False:
+            logger.info('Removal of info provider %s failed' % plugin_name)
 
     logger.info('Removing Authentication Providers')
     for plugin_name in plugins['Auth Providers']:
         plugin = plugins['Auth Providers'][plugin_name]
 
     logger.info('Removing Authentication Providers')
     for plugin_name in plugins['Auth Providers']:
         plugin = plugins['Auth Providers'][plugin_name]
-        if plugin.unconfigure(args) == False:
-            print 'Removal of auth provider %s failed' % plugin_name
+        plugin_changes = changes['auth_provider'].get(plugin_name, {})
+        if plugin.unconfigure(args, plugin_changes) == False:
+            logger.info('Removal of auth provider %s failed' % plugin_name)
 
     logger.info('Removing httpd configuration')
     os.remove(httpd_conf)
 
     logger.info('Removing httpd configuration')
     os.remove(httpd_conf)
@@ -286,7 +319,7 @@ def parse_config_profile(args):
             if g in globals():
                 globals()[g] = val
             else:
             if g in globals():
                 globals()[g] = val
             else:
-                for k in globals().keys():
+                for k in globals():
                     if k.lower() == g.lower():
                         globals()[k] = val
                         break
                     if k.lower() == g.lower():
                         globals()[k] = val
                         break
@@ -319,13 +352,22 @@ def parse_args(plugins):
     parser.add_argument('--secure', choices=['yes', 'no'], default='yes',
                         help="Turn on all security checks")
     parser.add_argument('--config-profile', default=None,
     parser.add_argument('--secure', choices=['yes', 'no'], default='yes',
                         help="Turn on all security checks")
     parser.add_argument('--config-profile', default=None,
-                        help="File containing install options")
+                        help=argparse.SUPPRESS)
     parser.add_argument('--server-debugging', action='store_true',
                         help="Enable debugging")
     parser.add_argument('--uninstall', action='store_true',
                         help="Uninstall the server and all data")
     parser.add_argument('--yes', action='store_true',
                         help="Always answer yes")
     parser.add_argument('--server-debugging', action='store_true',
                         help="Enable debugging")
     parser.add_argument('--uninstall', action='store_true',
                         help="Uninstall the server and all data")
     parser.add_argument('--yes', action='store_true',
                         help="Always answer yes")
+    parser.add_argument('--admin-dburi',
+                        help='Configuration database URI (override template)')
+    parser.add_argument('--users-dburi',
+                        help='User configuration database URI (override '
+                             'template)')
+    parser.add_argument('--transaction-dburi',
+                        help='Transaction database URI (override template)')
+    parser.add_argument('--samlsessions-dburi',
+                        help='SAML 2 sessions database URI (override template)')
 
     lms = []
 
 
     lms = []
 
@@ -345,9 +387,17 @@ def parse_args(plugins):
     if not args['hostname']:
         args['hostname'] = socket.getfqdn()
 
     if not args['hostname']:
         args['hostname'] = socket.getfqdn()
 
+    if args['uninstall']:
+        return args
+
     if len(args['hostname'].split('.')) < 2:
         raise ConfigurationError('Hostname: %s is not a FQDN')
 
     if len(args['hostname'].split('.')) < 2:
         raise ConfigurationError('Hostname: %s is not a FQDN')
 
+    for plugin_group in plugins:
+        for plugin_name in plugins[plugin_group]:
+            plugin = plugins[plugin_group][plugin_name]
+            plugin.validate_args(args)
+
     try:
         pwd.getpwnam(args['system_user'])
     except KeyError:
     try:
         pwd.getpwnam(args['system_user'])
     except KeyError:
@@ -361,13 +411,8 @@ def parse_args(plugins):
     else:
         args['lm_order'] = args['lm_order'].split(',')
 
     else:
         args['lm_order'] = args['lm_order'].split(',')
 
-    if len(args['lm_order']) == 0:
-        #force the basic pam provider if nothing else is selected
-        if 'pam' not in args:
-            parser.print_help()
-            sys.exit(-1)
-        args['lm_order'] = ['pam']
-        args['pam'] = 'yes'
+    if len(args['lm_order']) == 0 and args['ipa'] != 'yes':
+        sys.exit('No login plugins are enabled.')
 
     #FIXME: check instance is only alphanums
 
 
     #FIXME: check instance is only alphanums
 
@@ -383,30 +428,34 @@ if __name__ == '__main__':
 
         logger.setLevel(logging.DEBUG)
 
 
         logger.setLevel(logging.DEBUG)
 
-        logger.info('Intallation arguments:')
+        logger.debug('Installation arguments:')
         for k in sorted(opts.iterkeys()):
         for k in sorted(opts.iterkeys()):
-            logger.info('%s: %s', k, opts[k])
+            logger.debug('%s: %s', k, opts[k])
 
         if 'uninstall' in opts and opts['uninstall'] is True:
             if not os.path.exists(os.path.join(CONFDIR, opts['instance'])):
 
         if 'uninstall' in opts and opts['uninstall'] is True:
             if not os.path.exists(os.path.join(CONFDIR, opts['instance'])):
-                print 'Instance %s could not be found' % opts['instance']
+                logger.info('Instance %s could not be found' % opts['instance'])
                 sys.exit(0)
             uninstall(fplugins, opts)
         else:
             install(fplugins, opts)
     except Exception, e:  # pylint: disable=broad-except
                 sys.exit(0)
             uninstall(fplugins, opts)
         else:
             install(fplugins, opts)
     except Exception, e:  # pylint: disable=broad-except
-        logger.exception(e)
+        logger.debug(e, exc_info=1)
+
         if 'uninstall' in opts and opts['uninstall'] is True:
         if 'uninstall' in opts and opts['uninstall'] is True:
-            print 'Uninstallation aborted.'
+            logger.info('Uninstallation aborted.')
         else:
         else:
-            print 'Installation aborted.'
-        print 'See log file %s for details' % LOGFILE
+            logger.info('Installation aborted.')
+        logger.info('See log file %s for details' % LOGFILE)
+        out = 1
+    except SystemExit:
         out = 1
         out = 1
+        raise
     finally:
         if out == 0:
             if 'uninstall' in opts and opts['uninstall'] is True:
     finally:
         if out == 0:
             if 'uninstall' in opts and opts['uninstall'] is True:
-                print 'Uninstallation complete.'
+                logger.info('Uninstallation complete.')
             else:
             else:
-                print 'Installation complete.'
-                print 'Please restart HTTPD to enable the IdP instance.'
+                logger.info('Installation complete.')
+                logger.info('Please restart HTTPD to enable the IdP instance.')
     sys.exit(out)
     sys.exit(out)