7837527b00f5991898f97f5082d1c0c5625795b9
[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     try:
247         tconf = ConfigParser.SafeConfigParser()
248         tconf.read(os.path.join(instance_conf, 'ipsilon.conf'))
249         cache_dir = tconf.get('global', 'cache_dir')
250     except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
251         cache_dir = None
252     else:
253         cache_dir = cache_dir.replace('"', '')
254
255     if not os.path.exists(instance_conf):
256         raise Exception('Could not find instance %s configuration'
257                         % args['instance'])
258     if not os.path.exists(httpd_conf):
259         raise Exception('Could not find instance %s httpd configuration'
260                         % args['instance'])
261     if not args['yes']:
262         sure = raw_input(('Are you certain you want to erase instance %s ' +
263                           '[yes/NO]: ')
264                          % args['instance'])
265         if sure != 'yes':
266             raise Exception('Aborting')
267
268     # Get the details of what we changed during installation
269     install_changes = os.path.join(instance_conf, 'install_changes')
270     with open(install_changes, 'r') as f:
271         changes = json.loads(f.read())
272
273     logger.info('Removing environment helpers')
274     for plugin_name in plugins['Environment Helpers']:
275         plugin = plugins['Environment Helpers'][plugin_name]
276         plugin_changes = changes['env_helper'].get(plugin_name, {})
277         if plugin.unconfigure(args, plugin_changes) == False:
278             logger.info('Removal of environment helper %s failed' % plugin_name)
279
280     logger.info('Removing login managers')
281     for plugin_name in plugins['Login Managers']:
282         plugin = plugins['Login Managers'][plugin_name]
283         plugin_changes = changes['login_manager'].get(plugin_name, {})
284         if plugin.unconfigure(args, plugin_changes) == False:
285             logger.info('Removal of login manager %s failed' % plugin_name)
286
287     logger.info('Removing Info providers')
288     for plugin_name in plugins['Info Provider']:
289         plugin = plugins['Info Provider'][plugin_name]
290         plugin_changes = changes['info_provider'].get(plugin_name, {})
291         if plugin.unconfigure(args, plugin_changes) == False:
292             logger.info('Removal of info provider %s failed' % plugin_name)
293
294     logger.info('Removing Authentication Providers')
295     for plugin_name in plugins['Auth Providers']:
296         plugin = plugins['Auth Providers'][plugin_name]
297         plugin_changes = changes['auth_provider'].get(plugin_name, {})
298         if plugin.unconfigure(args, plugin_changes) == False:
299             logger.info('Removal of auth provider %s failed' % plugin_name)
300
301     logger.info('Removing httpd configuration')
302     os.remove(httpd_conf)
303     logger.info('Erasing instance configuration')
304     shutil.rmtree(instance_conf)
305     logger.info('Erasing instance data')
306     shutil.rmtree(data_dir)
307     if cache_dir and os.path.exists(cache_dir):
308         for fn in os.listdir(cache_dir):
309             os.unlink(os.path.join(cache_dir, fn))
310     logger.info('Uninstalled instance %s' % args['instance'])
311
312
313 def find_plugins():
314     plugins = {
315         'Environment Helpers': EnvHelpersInstall().plugins,
316         'Login Managers': LoginMgrsInstall().plugins,
317         'Info Provider': InfoProviderInstall().plugins,
318         'Auth Providers': ProvidersInstall().plugins
319     }
320     return plugins
321
322
323 def parse_config_profile(args):
324     config = ConfigParser.RawConfigParser()
325     files = config.read(args['config_profile'])
326     if len(files) == 0:
327         raise ConfigurationError('Config Profile file %s not found!' %
328                                  args['config_profile'])
329
330     if 'globals' in config.sections():
331         G = config.options('globals')
332         for g in G:
333             val = config.get('globals', g)
334             if g in globals():
335                 globals()[g] = val
336             else:
337                 for k in globals():
338                     if k.lower() == g.lower():
339                         globals()[k] = val
340                         break
341
342     if 'arguments' in config.sections():
343         A = config.options('arguments')
344         for a in A:
345             args[a] = config.get('arguments', a)
346
347     return args
348
349
350 def parse_args(plugins):
351     parser = argparse.ArgumentParser(description='Ipsilon Install Options')
352     parser.add_argument('--version',
353                         action='version', version='%(prog)s 0.1')
354     parser.add_argument('-o', '--login-managers-order', dest='lm_order',
355                         help='Comma separated list of login managers')
356     parser.add_argument('--hostname',
357                         help="Machine's fully qualified host name")
358     parser.add_argument('--instance', default='idp',
359                         help="IdP instance name, each is a separate idp")
360     parser.add_argument('--system-user', default='ipsilon',
361                         help="User account used to run the server")
362     parser.add_argument('--admin-user', default='admin',
363                         help="User account that is assigned admin privileges")
364     parser.add_argument('--database-url',
365                         default='sqlite:///%(datadir)s/%(dbname)s.sqlite',
366                         help="The (templatized) database URL to use")
367     parser.add_argument('--secure', choices=['yes', 'no'], default='yes',
368                         help="Turn on all security checks")
369     parser.add_argument('--config-profile', default=None,
370                         help=argparse.SUPPRESS)
371     parser.add_argument('--server-debugging', action='store_true',
372                         help="Enable debugging")
373     parser.add_argument('--uninstall', action='store_true',
374                         help="Uninstall the server and all data")
375     parser.add_argument('--yes', action='store_true',
376                         help="Always answer yes")
377     parser.add_argument('--admin-dburi',
378                         help='Configuration database URI (override template)')
379     parser.add_argument('--users-dburi',
380                         help='User configuration database URI (override '
381                              'template)')
382     parser.add_argument('--transaction-dburi',
383                         help='Transaction database URI (override template)')
384     parser.add_argument('--samlsessions-dburi',
385                         help='SAML 2 sessions database URI (override ' +
386                              'template)')
387     parser.add_argument('--cleanup-interval', default=30,
388                         help='Interval between cleaning up stale database ' +
389                              'entries (in minutes, default: 30 minutes)')
390
391     lms = []
392
393     for plugin_group in plugins:
394         group = parser.add_argument_group(plugin_group)
395         for plugin_name in plugins[plugin_group]:
396             plugin = plugins[plugin_group][plugin_name]
397             if plugin.ptype == 'login':
398                 lms.append(plugin.name)
399             plugin.install_args(group)
400
401     args = vars(parser.parse_args())
402
403     if args['config_profile']:
404         args = parse_config_profile(args)
405
406     if not args['hostname']:
407         args['hostname'] = socket.getfqdn()
408
409     if args['uninstall']:
410         return args
411
412     if len(args['hostname'].split('.')) < 2:
413         raise ConfigurationError('Hostname: %s is not a FQDN')
414
415     for plugin_group in plugins:
416         for plugin_name in plugins[plugin_group]:
417             plugin = plugins[plugin_group][plugin_name]
418             plugin.validate_args(args)
419
420     try:
421         pwd.getpwnam(args['system_user'])
422     except KeyError:
423         raise ConfigurationError('User: %s not found on the system')
424
425     if args['lm_order'] is None:
426         args['lm_order'] = []
427         for name in lms:
428             if args[name] == 'yes':
429                 args['lm_order'].append(name)
430     else:
431         args['lm_order'] = args['lm_order'].split(',')
432
433     if len(args['lm_order']) == 0 and args['ipa'] != 'yes':
434         sys.exit('No login plugins are enabled.')
435
436     #FIXME: check instance is only alphanums
437
438     return args
439
440 if __name__ == '__main__':
441     opts = []
442     out = 0
443     openlogs()
444     try:
445         fplugins = find_plugins()
446         opts = parse_args(fplugins)
447
448         logger.setLevel(logging.DEBUG)
449
450         logger.debug('Installation arguments:')
451         for k in sorted(opts.iterkeys()):
452             logger.debug('%s: %s', k, opts[k])
453
454         if 'uninstall' in opts and opts['uninstall'] is True:
455             if not os.path.exists(os.path.join(CONFDIR, opts['instance'])):
456                 logger.info('Instance %s could not be found' % opts['instance'])
457                 sys.exit(0)
458             uninstall(fplugins, opts)
459         else:
460             install(fplugins, opts)
461     except Exception, e:  # pylint: disable=broad-except
462         logger.debug(e, exc_info=1)
463
464         if 'uninstall' in opts and opts['uninstall'] is True:
465             logger.info('Uninstallation aborted.')
466         else:
467             logger.info('Installation aborted.')
468         logger.info('See log file %s for details' % LOGFILE)
469         out = 1
470     except SystemExit:
471         out = 1
472         raise
473     finally:
474         if out == 0:
475             if 'uninstall' in opts and opts['uninstall'] is True:
476                 logger.info('Uninstallation complete.')
477             else:
478                 logger.info('Installation complete.')
479                 logger.info('Please restart HTTPD to enable the IdP instance.')
480     sys.exit(out)