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