378479d7bf13b5ecc22ca676d9bf748af70c504b
[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(StandardError):
34
35     def __init__(self, message):
36         StandardError.__init__(self, message)
37
38
39 #Silence cherrypy logging to screen
40 cherrypy.log.screen = False
41
42 # Regular logging
43 LOGFILE = '/var/log/ipsilon-install.log'
44 logger = logging.getLogger()
45
46
47 def openlogs():
48     global logger  # pylint: disable=W0603
49     if os.path.isfile(LOGFILE):
50         try:
51             created = '%s' % time.ctime(os.path.getctime(LOGFILE))
52             shutil.move(LOGFILE, '%s.%s' % (LOGFILE, created))
53         except IOError:
54             pass
55     logger = logging.getLogger()
56     try:
57         lh = logging.FileHandler(LOGFILE)
58     except IOError, e:
59         print >> sys.stderr, 'Unable to open %s (%s)' % (LOGFILE, str(e))
60         lh = logging.StreamHandler(sys.stderr)
61     formatter = logging.Formatter('[%(asctime)s] %(message)s')
62     lh.setFormatter(formatter)
63     lh.setLevel(logging.DEBUG)
64     logger.addHandler(lh)
65     logger.propagate = False
66     ch = logging.StreamHandler(sys.stdout)
67     formatter = logging.Formatter('%(message)s')
68     ch.setFormatter(formatter)
69     ch.setLevel(logging.INFO)
70     logger.addHandler(ch)
71     cherrypy.log.error_log.setLevel(logging.DEBUG)
72
73
74 def install(plugins, args):
75     logger.info('Installation initiated')
76     now = time.strftime("%Y%m%d%H%M%S", time.gmtime())
77     instance_conf = os.path.join(CONFDIR, args['instance'])
78
79     logger.info('Installing default config files')
80     ipsilon_conf = os.path.join(instance_conf, 'ipsilon.conf')
81     idp_conf = os.path.join(instance_conf, 'idp.conf')
82     args['httpd_conf'] = os.path.join(HTTPDCONFD,
83                                       'ipsilon-%s.conf' % args['instance'])
84     args['data_dir'] = os.path.join(DATADIR, args['instance'])
85     args['public_data_dir'] = os.path.join(args['data_dir'], 'public')
86     args['wellknown_dir'] = os.path.join(args['public_data_dir'],
87                                          'well-known')
88     if os.path.exists(ipsilon_conf):
89         shutil.move(ipsilon_conf, '%s.bakcup.%s' % (ipsilon_conf, now))
90     if os.path.exists(idp_conf):
91         shutil.move(idp_conf, '%s.backup.%s' % (idp_conf, now))
92     if not os.path.exists(instance_conf):
93         os.makedirs(instance_conf, 0700)
94     confopts = {'instance': args['instance'],
95                 'datadir': args['data_dir'],
96                 'publicdatadir': args['public_data_dir'],
97                 'wellknowndir': args['wellknown_dir'],
98                 'sysuser': args['system_user'],
99                 'ipsilondir': BINDIR,
100                 'staticdir': STATICDIR,
101                 'admindb': args['admin_dburi'] or args['database_url'] % {
102                     'datadir': args['data_dir'], 'dbname': 'adminconfig'},
103                 'usersdb': args['users_dburi'] or args['database_url'] % {
104                     'datadir': args['data_dir'], 'dbname': 'userprefs'},
105                 'transdb': args['transaction_dburi'] or args['database_url'] %
106                 {'datadir': args['data_dir'], 'dbname': 'transactions'},
107                 'samlsessionsdb': args['samlsessions_dburi'] or args[
108                     'database_url'] % {'datadir': args['data_dir'],
109                                        'dbname': 'saml2sessions'},
110                 'secure': "False" if args['secure'] == "no" else "True",
111                 'debugging': "True" if args['server_debugging'] else "False"}
112     # Testing database sessions
113     if 'session_type' in args:
114         confopts['sesstype'] = args['session_type']
115     else:
116         confopts['sesstype'] = 'file'
117     if 'session_dburi' in args:
118         confopts['sessopt'] = 'dburi'
119         confopts['sessval'] = args['session_dburi']
120     else:
121         confopts['sessopt'] = 'path'
122         confopts['sessval'] = os.path.join(args['data_dir'], 'sessions')
123     # Whether to disable security (for testing)
124     if args['secure'] == 'no':
125         confopts['secure'] = "False"
126         confopts['sslrequiressl'] = ""
127     else:
128         confopts['secure'] = "True"
129         confopts['sslrequiressl'] = "   SSLRequireSSL"
130     if WSGI_SOCKET_PREFIX:
131         confopts['wsgi_socket'] = 'WSGISocketPrefix %s' % WSGI_SOCKET_PREFIX
132     else:
133         confopts['wsgi_socket'] = ''
134     files.write_from_template(ipsilon_conf,
135                               os.path.join(TEMPLATES, 'ipsilon.conf'),
136                               confopts)
137     files.write_from_template(idp_conf,
138                               os.path.join(TEMPLATES, 'idp.conf'),
139                               confopts)
140     if not os.path.exists(args['httpd_conf']):
141         os.symlink(idp_conf, args['httpd_conf'])
142     if not os.path.exists(args['public_data_dir']):
143         os.makedirs(args['public_data_dir'], 0755)
144     if not os.path.exists(args['wellknown_dir']):
145         os.makedirs(args['wellknown_dir'], 0755)
146     sessdir = os.path.join(args['data_dir'], 'sessions')
147     if not os.path.exists(sessdir):
148         os.makedirs(sessdir, 0700)
149     data_conf = os.path.join(args['data_dir'], 'ipsilon.conf')
150     if not os.path.exists(data_conf):
151         os.symlink(ipsilon_conf, data_conf)
152     # Load the cherrypy config from the newly installed file so
153     # that db paths and all is properly set before configuring
154     # components
155     cherrypy.config.update(ipsilon_conf)
156
157     # Prepare to allow plugins to save things changed during install
158     changes = {'env_helper': {},
159                'login_manager': {},
160                'info_provider': {},
161                'auth_provider': {}}
162
163     # Move pre-existing admin db away
164     admin_db = cherrypy.config['admin.config.db']
165     if os.path.exists(admin_db):
166         shutil.move(admin_db, '%s.backup.%s' % (admin_db, now))
167
168     # Rebuild user db
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     db = UserStore()
173     db.save_user_preferences(args['admin_user'], {'is_admin': 1})
174
175     logger.info('Configuring environment helpers')
176     for plugin_name in plugins['Environment Helpers']:
177         plugin = plugins['Environment Helpers'][plugin_name]
178         plugin_changes = {}
179         if plugin.configure_server(args, plugin_changes) == False:
180             msg = 'Configuration of environment helper %s failed' % plugin_name
181             raise ConfigurationError(msg)
182         changes['env_helper'][plugin_name] = plugin_changes
183
184     logger.info('Configuring login managers')
185     for plugin_name in args['lm_order']:
186         try:
187             plugin = plugins['Login Managers'][plugin_name]
188         except KeyError:
189             sys.exit('Login provider %s not installed' % plugin_name)
190         plugin_changes = {}
191         if plugin.configure(args, plugin_changes) == False:
192             msg = 'Configuration of login manager %s failed' % plugin_name
193             raise ConfigurationError(msg)
194         changes['login_manager'][plugin_name] = plugin_changes
195
196     logger.info('Configuring Info provider')
197     for plugin_name in plugins['Info Provider']:
198         plugin = plugins['Info Provider'][plugin_name]
199         plugin_changes = {}
200         if plugin.configure(args, plugin_changes) == False:
201             msg = 'Configuration of info provider %s failed' % plugin_name
202             raise ConfigurationError(msg)
203         changes['info_provider'][plugin_name] = plugin_changes
204
205     logger.info('Configuring Authentication Providers')
206     for plugin_name in plugins['Auth Providers']:
207         plugin = plugins['Auth Providers'][plugin_name]
208         plugin_changes = {}
209         if plugin.configure(args, plugin_changes) == False:
210             msg = 'Configuration of auth provider %s failed' % plugin_name
211             raise ConfigurationError(msg)
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():
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 and args['ipa'] != 'yes':
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)