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