Update Copyright header point to COPYING file
[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 logging
14 import os
15 import pwd
16 import shutil
17 import socket
18 import subprocess
19 import sys
20 import time
21
22
23 TEMPLATES = '/usr/share/ipsilon/templates/install'
24 CONFDIR = '/etc/ipsilon'
25 DATADIR = '/var/lib/ipsilon'
26 HTTPDCONFD = '/etc/httpd/conf.d'
27 BINDIR = '/usr/libexec'
28 STATICDIR = '/usr/share/ipsilon'
29 WSGI_SOCKET_PREFIX = None
30
31
32 class ConfigurationError(Exception):
33
34     def __init__(self, message):
35         super(ConfigurationError, self).__init__(message)
36         self.message = message
37
38     def __str__(self):
39         return repr(self.message)
40
41
42 #Silence cherrypy logging to screen
43 cherrypy.log.screen = False
44
45 # Regular logging
46 LOGFILE = '/var/log/ipsilon-install.log'
47 logger = logging.getLogger()
48
49
50 def openlogs():
51     global logger  # pylint: disable=W0603
52     if os.path.isfile(LOGFILE):
53         try:
54             created = '%s' % time.ctime(os.path.getctime(LOGFILE))
55             shutil.move(LOGFILE, '%s.%s' % (LOGFILE, created))
56         except IOError:
57             pass
58     logger = logging.getLogger()
59     try:
60         lh = logging.FileHandler(LOGFILE)
61     except IOError, e:
62         print >> sys.stderr, 'Unable to open %s (%s)' % (LOGFILE, str(e))
63         lh = logging.StreamHandler(sys.stderr)
64     formatter = logging.Formatter('[%(asctime)s] %(message)s')
65     lh.setFormatter(formatter)
66     lh.setLevel(logging.DEBUG)
67     logger.addHandler(lh)
68     logger.propagate = False
69     ch = logging.StreamHandler(sys.stdout)
70     formatter = logging.Formatter('%(message)s')
71     ch.setFormatter(formatter)
72     ch.setLevel(logging.INFO)
73     logger.addHandler(ch)
74     cherrypy.log.error_log.setLevel(logging.DEBUG)
75
76
77 def install(plugins, args):
78     logger.info('Installation initiated')
79     now = time.strftime("%Y%m%d%H%M%S", time.gmtime())
80     instance_conf = os.path.join(CONFDIR, args['instance'])
81
82     logger.info('Installing default config files')
83     ipsilon_conf = os.path.join(instance_conf, 'ipsilon.conf')
84     idp_conf = os.path.join(instance_conf, 'idp.conf')
85     args['httpd_conf'] = os.path.join(HTTPDCONFD,
86                                       'ipsilon-%s.conf' % args['instance'])
87     args['data_dir'] = os.path.join(DATADIR, args['instance'])
88     args['public_data_dir'] = os.path.join(args['data_dir'], 'public')
89     args['wellknown_dir'] = os.path.join(args['public_data_dir'],
90                                          'well-known')
91     if os.path.exists(ipsilon_conf):
92         shutil.move(ipsilon_conf, '%s.bakcup.%s' % (ipsilon_conf, now))
93     if os.path.exists(idp_conf):
94         shutil.move(idp_conf, '%s.backup.%s' % (idp_conf, now))
95     if not os.path.exists(instance_conf):
96         os.makedirs(instance_conf, 0700)
97     confopts = {'instance': args['instance'],
98                 'datadir': args['data_dir'],
99                 'publicdatadir': args['public_data_dir'],
100                 'wellknowndir': args['wellknown_dir'],
101                 'sysuser': args['system_user'],
102                 'ipsilondir': BINDIR,
103                 'staticdir': STATICDIR,
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                 '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     # Move pre-existing admin db away
158     admin_db = cherrypy.config['admin.config.db']
159     if os.path.exists(admin_db):
160         shutil.move(admin_db, '%s.backup.%s' % (admin_db, now))
161
162     # Rebuild user db
163     users_db = cherrypy.config['user.prefs.db']
164     if os.path.exists(users_db):
165         shutil.move(users_db, '%s.backup.%s' % (users_db, now))
166     db = UserStore()
167     db.save_user_preferences(args['admin_user'], {'is_admin': 1})
168
169     logger.info('Configuring environment helpers')
170     for plugin_name in plugins['Environment Helpers']:
171         plugin = plugins['Environment Helpers'][plugin_name]
172         if plugin.configure_server(args) == False:
173             logger.info('Configuration of environment helper %s failed' % plugin_name)
174
175     logger.info('Configuring login managers')
176     for plugin_name in args['lm_order']:
177         try:
178             plugin = plugins['Login Managers'][plugin_name]
179         except KeyError:
180             sys.exit('Login provider %s not installed' % plugin_name)
181         if plugin.configure(args) == False:
182             logger.info('Configuration of login manager %s failed' % plugin_name)
183
184     logger.info('Configuring Info provider')
185     for plugin_name in plugins['Info Provider']:
186         plugin = plugins['Info Provider'][plugin_name]
187         if plugin.configure(args) == False:
188             logger.info('Configuration of info provider %s failed' % plugin_name)
189
190     logger.info('Configuring Authentication Providers')
191     for plugin_name in plugins['Auth Providers']:
192         plugin = plugins['Auth Providers'][plugin_name]
193         if plugin.configure(args) == False:
194             logger.info('Configuration of auth provider %s failed' % plugin_name)
195
196     # Fixup permissions so only the ipsilon user can read these files
197     files.fix_user_dirs(instance_conf, opts['system_user'])
198     files.fix_user_dirs(args['data_dir'], opts['system_user'])
199     try:
200         subprocess.call(['/usr/sbin/restorecon', '-R', args['data_dir']])
201     except Exception:  # pylint: disable=broad-except
202         pass
203
204
205 def uninstall(plugins, args):
206     logger.info('Uninstallation initiated')
207     instance_conf = os.path.join(CONFDIR, args['instance'])
208
209     httpd_conf = os.path.join(HTTPDCONFD,
210                               'ipsilon-%s.conf' % args['instance'])
211     data_dir = os.path.join(DATADIR, args['instance'])
212
213     if not os.path.exists(instance_conf):
214         raise Exception('Could not find instance %s configuration'
215                         % args['instance'])
216     if not os.path.exists(httpd_conf):
217         raise Exception('Could not find instance %s httpd configuration'
218                         % args['instance'])
219     if not args['yes']:
220         sure = raw_input(('Are you certain you want to erase instance %s ' +
221                           '[yes/NO]: ')
222                          % args['instance'])
223         if sure != 'yes':
224             raise Exception('Aborting')
225
226     logger.info('Removing environment helpers')
227     for plugin_name in plugins['Environment Helpers']:
228         plugin = plugins['Environment Helpers'][plugin_name]
229         if plugin.unconfigure(args) == False:
230             logger.info('Removal of environment helper %s failed' % plugin_name)
231
232     logger.info('Removing login managers')
233     for plugin_name in plugins['Login Managers']:
234         plugin = plugins['Login Managers'][plugin_name]
235         if plugin.unconfigure(args) == False:
236             logger.info('Removal of login manager %s failed' % plugin_name)
237
238     logger.info('Removing Info providers')
239     for plugin_name in plugins['Info Provider']:
240         plugin = plugins['Info Provider'][plugin_name]
241         if plugin.unconfigure(args) == False:
242             logger.info('Removal of info provider %s failed' % plugin_name)
243
244     logger.info('Removing Authentication Providers')
245     for plugin_name in plugins['Auth Providers']:
246         plugin = plugins['Auth Providers'][plugin_name]
247         if plugin.unconfigure(args) == False:
248             logger.info('Removal of auth provider %s failed' % plugin_name)
249
250     logger.info('Removing httpd configuration')
251     os.remove(httpd_conf)
252     logger.info('Erasing instance configuration')
253     shutil.rmtree(instance_conf)
254     logger.info('Erasing instance data')
255     shutil.rmtree(data_dir)
256     logger.info('Uninstalled instance %s' % args['instance'])
257
258
259 def find_plugins():
260     plugins = {
261         'Environment Helpers': EnvHelpersInstall().plugins,
262         'Login Managers': LoginMgrsInstall().plugins,
263         'Info Provider': InfoProviderInstall().plugins,
264         'Auth Providers': ProvidersInstall().plugins
265     }
266     return plugins
267
268
269 def parse_config_profile(args):
270     config = ConfigParser.RawConfigParser()
271     files = config.read(args['config_profile'])
272     if len(files) == 0:
273         raise ConfigurationError('Config Profile file %s not found!' %
274                                  args['config_profile'])
275
276     if 'globals' in config.sections():
277         G = config.options('globals')
278         for g in G:
279             val = config.get('globals', g)
280             if g in globals():
281                 globals()[g] = val
282             else:
283                 for k in globals().keys():
284                     if k.lower() == g.lower():
285                         globals()[k] = val
286                         break
287
288     if 'arguments' in config.sections():
289         A = config.options('arguments')
290         for a in A:
291             args[a] = config.get('arguments', a)
292
293     return args
294
295
296 def parse_args(plugins):
297     parser = argparse.ArgumentParser(description='Ipsilon Install Options')
298     parser.add_argument('--version',
299                         action='version', version='%(prog)s 0.1')
300     parser.add_argument('-o', '--login-managers-order', dest='lm_order',
301                         help='Comma separated list of login managers')
302     parser.add_argument('--hostname',
303                         help="Machine's fully qualified host name")
304     parser.add_argument('--instance', default='idp',
305                         help="IdP instance name, each is a separate idp")
306     parser.add_argument('--system-user', default='ipsilon',
307                         help="User account used to run the server")
308     parser.add_argument('--admin-user', default='admin',
309                         help="User account that is assigned admin privileges")
310     parser.add_argument('--database-url',
311                         default='sqlite:///%(datadir)s/%(dbname)s.sqlite',
312                         help="The (templatized) database URL to use")
313     parser.add_argument('--secure', choices=['yes', 'no'], default='yes',
314                         help="Turn on all security checks")
315     parser.add_argument('--config-profile', default=None,
316                         help=argparse.SUPPRESS)
317     parser.add_argument('--server-debugging', action='store_true',
318                         help="Enable debugging")
319     parser.add_argument('--uninstall', action='store_true',
320                         help="Uninstall the server and all data")
321     parser.add_argument('--yes', action='store_true',
322                         help="Always answer yes")
323     parser.add_argument('--admin-dburi',
324                         help='Configuration database URI (override template)')
325     parser.add_argument('--users-dburi',
326                         help='User configuration database URI (override '
327                              'template)')
328     parser.add_argument('--transaction-dburi',
329                         help='Transaction database URI (override template)')
330
331     lms = []
332
333     for plugin_group in plugins:
334         group = parser.add_argument_group(plugin_group)
335         for plugin_name in plugins[plugin_group]:
336             plugin = plugins[plugin_group][plugin_name]
337             if plugin.ptype == 'login':
338                 lms.append(plugin.name)
339             plugin.install_args(group)
340
341     args = vars(parser.parse_args())
342
343     if args['config_profile']:
344         args = parse_config_profile(args)
345
346     if not args['hostname']:
347         args['hostname'] = socket.getfqdn()
348
349     if args['uninstall']:
350         return args
351
352     if len(args['hostname'].split('.')) < 2:
353         raise ConfigurationError('Hostname: %s is not a FQDN')
354
355     for plugin_group in plugins:
356         for plugin_name in plugins[plugin_group]:
357             plugin = plugins[plugin_group][plugin_name]
358             plugin.validate_args(args)
359
360     try:
361         pwd.getpwnam(args['system_user'])
362     except KeyError:
363         raise ConfigurationError('User: %s not found on the system')
364
365     if args['lm_order'] is None:
366         args['lm_order'] = []
367         for name in lms:
368             if args[name] == 'yes':
369                 args['lm_order'].append(name)
370     else:
371         args['lm_order'] = args['lm_order'].split(',')
372
373     if len(args['lm_order']) == 0:
374         sys.exit('No login plugins are enabled.')
375
376     #FIXME: check instance is only alphanums
377
378     return args
379
380 if __name__ == '__main__':
381     opts = []
382     out = 0
383     openlogs()
384     try:
385         fplugins = find_plugins()
386         opts = parse_args(fplugins)
387
388         logger.setLevel(logging.DEBUG)
389
390         logger.debug('Installation arguments:')
391         for k in sorted(opts.iterkeys()):
392             logger.debug('%s: %s', k, opts[k])
393
394         if 'uninstall' in opts and opts['uninstall'] is True:
395             if not os.path.exists(os.path.join(CONFDIR, opts['instance'])):
396                 logger.info('Instance %s could not be found' % opts['instance'])
397                 sys.exit(0)
398             uninstall(fplugins, opts)
399         else:
400             install(fplugins, opts)
401     except Exception, e:  # pylint: disable=broad-except
402         logger.exception(e)
403         if 'uninstall' in opts and opts['uninstall'] is True:
404             logger.info('Uninstallation aborted.')
405         else:
406             logger.info('Installation aborted.')
407         logger.info('See log file %s for details' % LOGFILE)
408         out = 1
409     except SystemExit:
410         out = 1
411         raise
412     finally:
413         if out == 0:
414             if 'uninstall' in opts and opts['uninstall'] is True:
415                 logger.info('Uninstallation complete.')
416             else:
417                 logger.info('Installation complete.')
418                 logger.info('Please restart HTTPD to enable the IdP instance.')
419     sys.exit(out)