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