Implement automatic database cleanup
[cascardo/ipsilon.git] / ipsilon / install / ipsilon-server-install
1 #!/usr/bin/python
2 # Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING
3
4 from ipsilon.login.common import LoginMgrsInstall
5 from ipsilon.info.common import InfoProviderInstall
6 from ipsilon.providers.common import ProvidersInstall
7 from ipsilon.helpers.common import EnvHelpersInstall
8 from ipsilon.util.data import UserStore
9 from ipsilon.tools import files, dbupgrade
10 import ConfigParser
11 import argparse
12 import cherrypy
13 import json
14 import logging
15 import os
16 import pwd
17 import shutil
18 import socket
19 import subprocess
20 import sys
21 import time
22
23
24 TEMPLATES = '/usr/share/ipsilon/templates/install'
25 CONFDIR = '/etc/ipsilon'
26 DATADIR = '/var/lib/ipsilon'
27 HTTPDCONFD = '/etc/httpd/conf.d'
28 BINDIR = '/usr/libexec'
29 STATICDIR = '/usr/share/ipsilon'
30 CACHEDIR = '/var/cache/ipsilon'
31 WSGI_SOCKET_PREFIX = None
32
33
34 class ConfigurationError(StandardError):
35
36     def __init__(self, message):
37         StandardError.__init__(self, message)
38
39
40 #Silence cherrypy logging to screen
41 cherrypy.log.screen = False
42
43 # Regular logging
44 LOGFILE = '/var/log/ipsilon-install.log'
45 logger = logging.getLogger()
46
47
48 def openlogs():
49     global logger  # pylint: disable=W0603
50     if os.path.isfile(LOGFILE):
51         try:
52             created = '%s' % time.ctime(os.path.getctime(LOGFILE))
53             shutil.move(LOGFILE, '%s.%s' % (LOGFILE, created))
54         except IOError:
55             pass
56     logger = logging.getLogger()
57     try:
58         lh = logging.FileHandler(LOGFILE)
59     except IOError, e:
60         print >> sys.stderr, 'Unable to open %s (%s)' % (LOGFILE, str(e))
61         lh = logging.StreamHandler(sys.stderr)
62     formatter = logging.Formatter('[%(asctime)s] %(message)s')
63     lh.setFormatter(formatter)
64     lh.setLevel(logging.DEBUG)
65     logger.addHandler(lh)
66     logger.propagate = False
67     ch = logging.StreamHandler(sys.stdout)
68     formatter = logging.Formatter('%(message)s')
69     ch.setFormatter(formatter)
70     ch.setLevel(logging.INFO)
71     logger.addHandler(ch)
72     cherrypy.log.error_log.setLevel(logging.DEBUG)
73
74
75 def install(plugins, args):
76     logger.info('Installation initiated')
77     now = time.strftime("%Y%m%d%H%M%S", time.gmtime())
78     instance_conf = os.path.join(CONFDIR, args['instance'])
79
80     logger.info('Installing default config files')
81     ipsilon_conf = os.path.join(instance_conf, 'ipsilon.conf')
82     idp_conf = os.path.join(instance_conf, 'idp.conf')
83     args['httpd_conf'] = os.path.join(HTTPDCONFD,
84                                       'ipsilon-%s.conf' % args['instance'])
85     args['data_dir'] = os.path.join(DATADIR, args['instance'])
86     args['public_data_dir'] = os.path.join(args['data_dir'], 'public')
87     args['wellknown_dir'] = os.path.join(args['public_data_dir'],
88                                          'well-known')
89     if os.path.exists(ipsilon_conf):
90         shutil.move(ipsilon_conf, '%s.bakcup.%s' % (ipsilon_conf, now))
91     if os.path.exists(idp_conf):
92         shutil.move(idp_conf, '%s.backup.%s' % (idp_conf, now))
93     if not os.path.exists(instance_conf):
94         os.makedirs(instance_conf, 0700)
95     confopts = {'instance': args['instance'],
96                 'datadir': args['data_dir'],
97                 'publicdatadir': args['public_data_dir'],
98                 'wellknowndir': args['wellknown_dir'],
99                 'sysuser': args['system_user'],
100                 'cleanup_interval': args['cleanup_interval'],
101                 'ipsilondir': BINDIR,
102                 'staticdir': STATICDIR,
103                 'cachedir': CACHEDIR,
104                 'admindb': args['admin_dburi'] or args['database_url'] % {
105                     'datadir': args['data_dir'], 'dbname': 'adminconfig'},
106                 'usersdb': args['users_dburi'] or args['database_url'] % {
107                     'datadir': args['data_dir'], 'dbname': 'userprefs'},
108                 'transdb': args['transaction_dburi'] or args['database_url'] %
109                 {'datadir': args['data_dir'], 'dbname': 'transactions'},
110                 'samlsessionsdb': args['samlsessions_dburi'] or args[
111                     'database_url'] % {'datadir': args['data_dir'],
112                                        'dbname': 'saml2sessions'},
113                 'secure': "False" if args['secure'] == "no" else "True",
114                 'debugging': "True" if args['server_debugging'] else "False"}
115     # Testing database sessions
116     if 'session_type' in args:
117         confopts['sesstype'] = args['session_type']
118     else:
119         confopts['sesstype'] = 'file'
120     if 'session_dburi' in args:
121         confopts['sessopt'] = 'dburi'
122         confopts['sessval'] = args['session_dburi']
123     else:
124         confopts['sessopt'] = 'path'
125         confopts['sessval'] = os.path.join(args['data_dir'], 'sessions')
126     # Whether to disable security (for testing)
127     if args['secure'] == 'no':
128         confopts['secure'] = "False"
129         confopts['sslrequiressl'] = ""
130     else:
131         confopts['secure'] = "True"
132         confopts['sslrequiressl'] = "   SSLRequireSSL"
133     if WSGI_SOCKET_PREFIX:
134         confopts['wsgi_socket'] = 'WSGISocketPrefix %s' % WSGI_SOCKET_PREFIX
135     else:
136         confopts['wsgi_socket'] = ''
137     files.write_from_template(ipsilon_conf,
138                               os.path.join(TEMPLATES, 'ipsilon.conf'),
139                               confopts)
140     files.write_from_template(idp_conf,
141                               os.path.join(TEMPLATES, 'idp.conf'),
142                               confopts)
143     if not os.path.exists(args['httpd_conf']):
144         os.symlink(idp_conf, args['httpd_conf'])
145     if not os.path.exists(args['public_data_dir']):
146         os.makedirs(args['public_data_dir'], 0755)
147     if not os.path.exists(args['wellknown_dir']):
148         os.makedirs(args['wellknown_dir'], 0755)
149     sessdir = os.path.join(args['data_dir'], 'sessions')
150     if not os.path.exists(sessdir):
151         os.makedirs(sessdir, 0700)
152     data_conf = os.path.join(args['data_dir'], 'ipsilon.conf')
153     if not os.path.exists(data_conf):
154         os.symlink(ipsilon_conf, data_conf)
155     # Load the cherrypy config from the newly installed file so
156     # that db paths and all is properly set before configuring
157     # components
158     cherrypy.config.update(ipsilon_conf)
159
160     # Prepare to allow plugins to save things changed during install
161     changes = {'env_helper': {},
162                'login_manager': {},
163                'info_provider': {},
164                'auth_provider': {}}
165
166     # Move pre-existing dbs away
167     admin_db = cherrypy.config['admin.config.db']
168     if os.path.exists(admin_db):
169         shutil.move(admin_db, '%s.backup.%s' % (admin_db, now))
170     users_db = cherrypy.config['user.prefs.db']
171     if os.path.exists(users_db):
172         shutil.move(users_db, '%s.backup.%s' % (users_db, now))
173
174     # Initialize initial database schemas
175     dbupgrade.execute_upgrade(ipsilon_conf)
176
177     # Store primary admin
178     db = UserStore()
179     db.save_user_preferences(args['admin_user'], {'is_admin': 1})
180
181     logger.info('Configuring environment helpers')
182     for plugin_name in plugins['Environment Helpers']:
183         plugin = plugins['Environment Helpers'][plugin_name]
184         plugin_changes = {}
185         if plugin.configure_server(args, plugin_changes) == False:
186             msg = 'Configuration of environment helper %s failed' % plugin_name
187             raise ConfigurationError(msg)
188         changes['env_helper'][plugin_name] = plugin_changes
189
190     logger.info('Configuring login managers')
191     for plugin_name in args['lm_order']:
192         try:
193             plugin = plugins['Login Managers'][plugin_name]
194         except KeyError:
195             sys.exit('Login provider %s not installed' % plugin_name)
196         plugin_changes = {}
197         if plugin.configure(args, plugin_changes) == False:
198             msg = 'Configuration of login manager %s failed' % plugin_name
199             raise ConfigurationError(msg)
200         changes['login_manager'][plugin_name] = plugin_changes
201
202     logger.info('Configuring Info provider')
203     for plugin_name in plugins['Info Provider']:
204         plugin = plugins['Info Provider'][plugin_name]
205         plugin_changes = {}
206         if plugin.configure(args, plugin_changes) == False:
207             msg = 'Configuration of info provider %s failed' % plugin_name
208             raise ConfigurationError(msg)
209         changes['info_provider'][plugin_name] = plugin_changes
210
211     logger.info('Configuring Authentication Providers')
212     for plugin_name in plugins['Auth Providers']:
213         plugin = plugins['Auth Providers'][plugin_name]
214         plugin_changes = {}
215         if plugin.configure(args, plugin_changes) == False:
216             msg = 'Configuration of auth provider %s failed' % plugin_name
217             raise ConfigurationError(msg)
218         changes['auth_provider'][plugin_name] = plugin_changes
219
220     # Save any changes that were made
221     install_changes = os.path.join(instance_conf, 'install_changes')
222     changes = json.dumps(changes)
223     with open(install_changes, 'w+') as f:
224         f.write(changes)
225
226     # Initialize extra database schemas
227     dbupgrade.execute_upgrade(ipsilon_conf)
228
229     # Fixup permissions so only the ipsilon user can read these files
230     files.fix_user_dirs(instance_conf, opts['system_user'])
231     files.fix_user_dirs(args['data_dir'], opts['system_user'])
232     try:
233         subprocess.call(['/usr/sbin/restorecon', '-R', args['data_dir']])
234     except Exception:  # pylint: disable=broad-except
235         pass
236
237
238 def uninstall(plugins, args):
239     logger.info('Uninstallation initiated')
240     instance_conf = os.path.join(CONFDIR, args['instance'])
241
242     httpd_conf = os.path.join(HTTPDCONFD,
243                               'ipsilon-%s.conf' % args['instance'])
244     data_dir = os.path.join(DATADIR, args['instance'])
245
246     tconf = ConfigParser.SafeConfigParser()
247     tconf.read(os.path.join(instance_conf, 'ipsilon.conf'))
248     cache_dir = tconf.get('global', 'cache_dir').replace('"', '')
249
250     if not os.path.exists(instance_conf):
251         raise Exception('Could not find instance %s configuration'
252                         % args['instance'])
253     if not os.path.exists(httpd_conf):
254         raise Exception('Could not find instance %s httpd configuration'
255                         % args['instance'])
256     if not args['yes']:
257         sure = raw_input(('Are you certain you want to erase instance %s ' +
258                           '[yes/NO]: ')
259                          % args['instance'])
260         if sure != 'yes':
261             raise Exception('Aborting')
262
263     # Get the details of what we changed during installation
264     install_changes = os.path.join(instance_conf, 'install_changes')
265     with open(install_changes, 'r') as f:
266         changes = json.loads(f.read())
267
268     logger.info('Removing environment helpers')
269     for plugin_name in plugins['Environment Helpers']:
270         plugin = plugins['Environment Helpers'][plugin_name]
271         plugin_changes = changes['env_helper'].get(plugin_name, {})
272         if plugin.unconfigure(args, plugin_changes) == False:
273             logger.info('Removal of environment helper %s failed' % plugin_name)
274
275     logger.info('Removing login managers')
276     for plugin_name in plugins['Login Managers']:
277         plugin = plugins['Login Managers'][plugin_name]
278         plugin_changes = changes['login_manager'].get(plugin_name, {})
279         if plugin.unconfigure(args, plugin_changes) == False:
280             logger.info('Removal of login manager %s failed' % plugin_name)
281
282     logger.info('Removing Info providers')
283     for plugin_name in plugins['Info Provider']:
284         plugin = plugins['Info Provider'][plugin_name]
285         plugin_changes = changes['info_provider'].get(plugin_name, {})
286         if plugin.unconfigure(args, plugin_changes) == False:
287             logger.info('Removal of info provider %s failed' % plugin_name)
288
289     logger.info('Removing Authentication Providers')
290     for plugin_name in plugins['Auth Providers']:
291         plugin = plugins['Auth Providers'][plugin_name]
292         plugin_changes = changes['auth_provider'].get(plugin_name, {})
293         if plugin.unconfigure(args, plugin_changes) == False:
294             logger.info('Removal of auth provider %s failed' % plugin_name)
295
296     logger.info('Removing httpd configuration')
297     os.remove(httpd_conf)
298     logger.info('Erasing instance configuration')
299     shutil.rmtree(instance_conf)
300     logger.info('Erasing instance data')
301     shutil.rmtree(data_dir)
302     if cache_dir and os.path.exists(cache_dir):
303         for fn in os.listdir(cache_dir):
304             os.unlink(os.path.join(cache_dir, fn))
305     logger.info('Uninstalled instance %s' % args['instance'])
306
307
308 def find_plugins():
309     plugins = {
310         'Environment Helpers': EnvHelpersInstall().plugins,
311         'Login Managers': LoginMgrsInstall().plugins,
312         'Info Provider': InfoProviderInstall().plugins,
313         'Auth Providers': ProvidersInstall().plugins
314     }
315     return plugins
316
317
318 def parse_config_profile(args):
319     config = ConfigParser.RawConfigParser()
320     files = config.read(args['config_profile'])
321     if len(files) == 0:
322         raise ConfigurationError('Config Profile file %s not found!' %
323                                  args['config_profile'])
324
325     if 'globals' in config.sections():
326         G = config.options('globals')
327         for g in G:
328             val = config.get('globals', g)
329             if g in globals():
330                 globals()[g] = val
331             else:
332                 for k in globals():
333                     if k.lower() == g.lower():
334                         globals()[k] = val
335                         break
336
337     if 'arguments' in config.sections():
338         A = config.options('arguments')
339         for a in A:
340             args[a] = config.get('arguments', a)
341
342     return args
343
344
345 def parse_args(plugins):
346     parser = argparse.ArgumentParser(description='Ipsilon Install Options')
347     parser.add_argument('--version',
348                         action='version', version='%(prog)s 0.1')
349     parser.add_argument('-o', '--login-managers-order', dest='lm_order',
350                         help='Comma separated list of login managers')
351     parser.add_argument('--hostname',
352                         help="Machine's fully qualified host name")
353     parser.add_argument('--instance', default='idp',
354                         help="IdP instance name, each is a separate idp")
355     parser.add_argument('--system-user', default='ipsilon',
356                         help="User account used to run the server")
357     parser.add_argument('--admin-user', default='admin',
358                         help="User account that is assigned admin privileges")
359     parser.add_argument('--database-url',
360                         default='sqlite:///%(datadir)s/%(dbname)s.sqlite',
361                         help="The (templatized) database URL to use")
362     parser.add_argument('--secure', choices=['yes', 'no'], default='yes',
363                         help="Turn on all security checks")
364     parser.add_argument('--config-profile', default=None,
365                         help=argparse.SUPPRESS)
366     parser.add_argument('--server-debugging', action='store_true',
367                         help="Enable debugging")
368     parser.add_argument('--uninstall', action='store_true',
369                         help="Uninstall the server and all data")
370     parser.add_argument('--yes', action='store_true',
371                         help="Always answer yes")
372     parser.add_argument('--admin-dburi',
373                         help='Configuration database URI (override template)')
374     parser.add_argument('--users-dburi',
375                         help='User configuration database URI (override '
376                              'template)')
377     parser.add_argument('--transaction-dburi',
378                         help='Transaction database URI (override template)')
379     parser.add_argument('--samlsessions-dburi',
380                         help='SAML 2 sessions database URI (override ' +
381                              'template)')
382     parser.add_argument('--cleanup-interval', default=30,
383                         help='Interval between cleaning up stale database ' +
384                              'entries (in minutes, default: 30 minutes)')
385
386     lms = []
387
388     for plugin_group in plugins:
389         group = parser.add_argument_group(plugin_group)
390         for plugin_name in plugins[plugin_group]:
391             plugin = plugins[plugin_group][plugin_name]
392             if plugin.ptype == 'login':
393                 lms.append(plugin.name)
394             plugin.install_args(group)
395
396     args = vars(parser.parse_args())
397
398     if args['config_profile']:
399         args = parse_config_profile(args)
400
401     if not args['hostname']:
402         args['hostname'] = socket.getfqdn()
403
404     if args['uninstall']:
405         return args
406
407     if len(args['hostname'].split('.')) < 2:
408         raise ConfigurationError('Hostname: %s is not a FQDN')
409
410     for plugin_group in plugins:
411         for plugin_name in plugins[plugin_group]:
412             plugin = plugins[plugin_group][plugin_name]
413             plugin.validate_args(args)
414
415     try:
416         pwd.getpwnam(args['system_user'])
417     except KeyError:
418         raise ConfigurationError('User: %s not found on the system')
419
420     if args['lm_order'] is None:
421         args['lm_order'] = []
422         for name in lms:
423             if args[name] == 'yes':
424                 args['lm_order'].append(name)
425     else:
426         args['lm_order'] = args['lm_order'].split(',')
427
428     if len(args['lm_order']) == 0 and args['ipa'] != 'yes':
429         sys.exit('No login plugins are enabled.')
430
431     #FIXME: check instance is only alphanums
432
433     return args
434
435 if __name__ == '__main__':
436     opts = []
437     out = 0
438     openlogs()
439     try:
440         fplugins = find_plugins()
441         opts = parse_args(fplugins)
442
443         logger.setLevel(logging.DEBUG)
444
445         logger.debug('Installation arguments:')
446         for k in sorted(opts.iterkeys()):
447             logger.debug('%s: %s', k, opts[k])
448
449         if 'uninstall' in opts and opts['uninstall'] is True:
450             if not os.path.exists(os.path.join(CONFDIR, opts['instance'])):
451                 logger.info('Instance %s could not be found' % opts['instance'])
452                 sys.exit(0)
453             uninstall(fplugins, opts)
454         else:
455             install(fplugins, opts)
456     except Exception, e:  # pylint: disable=broad-except
457         logger.debug(e, exc_info=1)
458
459         if 'uninstall' in opts and opts['uninstall'] is True:
460             logger.info('Uninstallation aborted.')
461         else:
462             logger.info('Installation aborted.')
463         logger.info('See log file %s for details' % LOGFILE)
464         out = 1
465     except SystemExit:
466         out = 1
467         raise
468     finally:
469         if out == 0:
470             if 'uninstall' in opts and opts['uninstall'] is True:
471                 logger.info('Uninstallation complete.')
472             else:
473                 logger.info('Installation complete.')
474                 logger.info('Please restart HTTPD to enable the IdP instance.')
475     sys.exit(out)