502d4ebe86a64658909d66677335ec5523564608
[cascardo/ipsilon.git] / ipsilon / install / ipsilon-server-install
1 #!/usr/bin/python
2 #
3 # Copyright (C) 2014  Simo Sorce <simo@redhat.com>
4 #
5 # see file 'COPYING' for use and warranty information
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20 from ipsilon.login.common import LoginMgrsInstall
21 from ipsilon.info.common import InfoProviderInstall
22 from ipsilon.providers.common import ProvidersInstall
23 from ipsilon.helpers.common import EnvHelpersInstall
24 from ipsilon.util.data import UserStore
25 from ipsilon.tools import files
26 import ConfigParser
27 import argparse
28 import cherrypy
29 import logging
30 import os
31 import pwd
32 import shutil
33 import socket
34 import subprocess
35 import sys
36 import time
37
38
39 TEMPLATES = '/usr/share/ipsilon/templates/install'
40 CONFDIR = '/etc/ipsilon'
41 DATADIR = '/var/lib/ipsilon'
42 HTTPDCONFD = '/etc/httpd/conf.d'
43 BINDIR = '/usr/sbin'
44 STATICDIR = '/usr/share/ipsilon'
45 WSGI_SOCKET_PREFIX = None
46
47
48 class ConfigurationError(Exception):
49
50     def __init__(self, message):
51         super(ConfigurationError, self).__init__(message)
52         self.message = message
53
54     def __str__(self):
55         return repr(self.message)
56
57
58 #Silence cherrypy logging to screen
59 cherrypy.log.screen = False
60
61 # Regular logging
62 LOGFILE = '/var/log/ipsilon-install.log'
63 logger = logging.getLogger()
64
65
66 def openlogs():
67     global logger  # pylint: disable=W0603
68     if os.path.isfile(LOGFILE):
69         try:
70             created = '%s' % time.ctime(os.path.getctime(LOGFILE))
71             shutil.move(LOGFILE, '%s.%s' % (LOGFILE, created))
72         except IOError:
73             pass
74     logger = logging.getLogger()
75     try:
76         lh = logging.FileHandler(LOGFILE)
77     except IOError, e:
78         print >> sys.stderr, 'Unable to open %s (%s)' % (LOGFILE, str(e))
79         lh = logging.StreamHandler(sys.stderr)
80     formatter = logging.Formatter('[%(asctime)s] %(message)s')
81     lh.setFormatter(formatter)
82     logger.addHandler(lh)
83
84
85 def install(plugins, args):
86     logger.info('Installation initiated')
87     now = time.strftime("%Y%m%d%H%M%S", time.gmtime())
88     instance_conf = os.path.join(CONFDIR, args['instance'])
89
90     logger.info('Installing default config files')
91     ipsilon_conf = os.path.join(instance_conf, 'ipsilon.conf')
92     idp_conf = os.path.join(instance_conf, 'idp.conf')
93     args['httpd_conf'] = os.path.join(HTTPDCONFD,
94                                       'ipsilon-%s.conf' % args['instance'])
95     args['data_dir'] = os.path.join(DATADIR, args['instance'])
96     args['public_data_dir'] = os.path.join(args['data_dir'], 'public')
97     args['wellknown_dir'] = os.path.join(args['public_data_dir'],
98                                          'well-known')
99     if os.path.exists(ipsilon_conf):
100         shutil.move(ipsilon_conf, '%s.bakcup.%s' % (ipsilon_conf, now))
101     if os.path.exists(idp_conf):
102         shutil.move(idp_conf, '%s.backup.%s' % (idp_conf, now))
103     if not os.path.exists(instance_conf):
104         os.makedirs(instance_conf, 0700)
105     confopts = {'instance': args['instance'],
106                 'datadir': args['data_dir'],
107                 'publicdatadir': args['public_data_dir'],
108                 'wellknowndir': args['wellknown_dir'],
109                 'sysuser': args['system_user'],
110                 'ipsilondir': BINDIR,
111                 'staticdir': STATICDIR,
112                 'admindb': args['admin_dburi'] or args['database_url'] % {
113                     'datadir': args['data_dir'], 'dbname': 'adminconfig'},
114                 'usersdb': args['users_dburi'] or args['database_url'] % {
115                     'datadir': args['data_dir'], 'dbname': 'userprefs'},
116                 'transdb': args['transaction_dburi'] or args['database_url'] %
117                 {'datadir': args['data_dir'], 'dbname': 'transactions'},
118                 'secure': "False" if args['secure'] == "no" else "True",
119                 'debugging': "True" if args['server_debugging'] else "False"}
120     # Testing database sessions
121     if 'session_type' in args:
122         confopts['sesstype'] = args['session_type']
123     else:
124         confopts['sesstype'] = 'file'
125     if 'session_dburi' in args:
126         confopts['sessopt'] = 'dburi'
127         confopts['sessval'] = args['session_dburi']
128     else:
129         confopts['sessopt'] = 'path'
130         confopts['sessval'] = os.path.join(args['data_dir'], 'sessions')
131     # Whether to disable security (for testing)
132     if args['secure'] == 'no':
133         confopts['secure'] = "False"
134         confopts['sslrequiressl'] = ""
135     else:
136         confopts['secure'] = "True"
137         confopts['sslrequiressl'] = "   SSLRequireSSL"
138     if WSGI_SOCKET_PREFIX:
139         confopts['wsgi_socket'] = 'WSGISocketPrefix %s' % WSGI_SOCKET_PREFIX
140     else:
141         confopts['wsgi_socket'] = ''
142     files.write_from_template(ipsilon_conf,
143                               os.path.join(TEMPLATES, 'ipsilon.conf'),
144                               confopts)
145     files.write_from_template(idp_conf,
146                               os.path.join(TEMPLATES, 'idp.conf'),
147                               confopts)
148     if not os.path.exists(args['httpd_conf']):
149         os.symlink(idp_conf, args['httpd_conf'])
150     if not os.path.exists(args['public_data_dir']):
151         os.makedirs(args['public_data_dir'], 0755)
152     if not os.path.exists(args['wellknown_dir']):
153         os.makedirs(args['wellknown_dir'], 0755)
154     sessdir = os.path.join(args['data_dir'], 'sessions')
155     if not os.path.exists(sessdir):
156         os.makedirs(sessdir, 0700)
157     data_conf = os.path.join(args['data_dir'], 'ipsilon.conf')
158     if not os.path.exists(data_conf):
159         os.symlink(ipsilon_conf, data_conf)
160     # Load the cherrypy config from the newly installed file so
161     # that db paths and all is properly set before configuring
162     # components
163     cherrypy.config.update(ipsilon_conf)
164
165     # Move pre-existing admin db 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
170     # Rebuild user db
171     users_db = cherrypy.config['user.prefs.db']
172     if os.path.exists(users_db):
173         shutil.move(users_db, '%s.backup.%s' % (users_db, now))
174     db = UserStore()
175     db.save_user_preferences(args['admin_user'], {'is_admin': 1})
176
177     logger.info('Configuring environment helpers')
178     for plugin_name in plugins['Environment Helpers']:
179         plugin = plugins['Environment Helpers'][plugin_name]
180         if plugin.configure_server(args) == False:
181             print 'Configuration of environment helper %s failed' % plugin_name
182
183     logger.info('Configuring login managers')
184     for plugin_name in args['lm_order']:
185         try:
186             plugin = plugins['Login Managers'][plugin_name]
187         except KeyError:
188             sys.exit('Login provider %s not installed' % plugin_name)
189         if plugin.configure(args) == False:
190             print 'Configuration of login manager %s failed' % plugin_name
191
192     logger.info('Configuring Info provider')
193     for plugin_name in plugins['Info Provider']:
194         plugin = plugins['Info Provider'][plugin_name]
195         if plugin.configure(args) == False:
196             print 'Configuration of info provider %s failed' % plugin_name
197
198     logger.info('Configuring Authentication Providers')
199     for plugin_name in plugins['Auth Providers']:
200         plugin = plugins['Auth Providers'][plugin_name]
201         if plugin.configure(args) == False:
202             print 'Configuration of auth provider %s failed' % plugin_name
203
204     # Fixup permissions so only the ipsilon user can read these files
205     files.fix_user_dirs(instance_conf, opts['system_user'])
206     files.fix_user_dirs(args['data_dir'], opts['system_user'])
207     try:
208         subprocess.call(['/usr/sbin/restorecon', '-R', args['data_dir']])
209     except Exception:  # pylint: disable=broad-except
210         pass
211
212
213 def uninstall(plugins, args):
214     logger.info('Uninstallation initiated')
215     instance_conf = os.path.join(CONFDIR, args['instance'])
216
217     httpd_conf = os.path.join(HTTPDCONFD,
218                               'ipsilon-%s.conf' % args['instance'])
219     data_dir = os.path.join(DATADIR, args['instance'])
220
221     if not os.path.exists(instance_conf):
222         raise Exception('Could not find instance %s configuration'
223                         % args['instance'])
224     if not os.path.exists(httpd_conf):
225         raise Exception('Could not find instance %s httpd configuration'
226                         % args['instance'])
227     if not args['yes']:
228         sure = raw_input(('Are you certain you want to erase instance %s ' +
229                           '[yes/NO]: ')
230                          % args['instance'])
231         if sure != 'yes':
232             raise Exception('Aborting')
233
234     logger.info('Removing environment helpers')
235     for plugin_name in plugins['Environment Helpers']:
236         plugin = plugins['Environment Helpers'][plugin_name]
237         if plugin.unconfigure(args) == False:
238             print 'Removal of environment helper %s failed' % plugin_name
239
240     logger.info('Removing login managers')
241     for plugin_name in plugins['Login Managers']:
242         plugin = plugins['Login Managers'][plugin_name]
243         if plugin.unconfigure(args) == False:
244             print 'Removal of login manager %s failed' % plugin_name
245
246     logger.info('Removing Info providers')
247     for plugin_name in plugins['Info Provider']:
248         plugin = plugins['Info Provider'][plugin_name]
249         if plugin.unconfigure(args) == False:
250             print 'Removal of info provider %s failed' % plugin_name
251
252     logger.info('Removing Authentication Providers')
253     for plugin_name in plugins['Auth Providers']:
254         plugin = plugins['Auth Providers'][plugin_name]
255         if plugin.unconfigure(args) == False:
256             print 'Removal of auth provider %s failed' % plugin_name
257
258     logger.info('Removing httpd configuration')
259     os.remove(httpd_conf)
260     logger.info('Erasing instance configuration')
261     shutil.rmtree(instance_conf)
262     logger.info('Erasing instance data')
263     shutil.rmtree(data_dir)
264     logger.info('Uninstalled instance %s' % args['instance'])
265
266
267 def find_plugins():
268     plugins = {
269         'Environment Helpers': EnvHelpersInstall().plugins,
270         'Login Managers': LoginMgrsInstall().plugins,
271         'Info Provider': InfoProviderInstall().plugins,
272         'Auth Providers': ProvidersInstall().plugins
273     }
274     return plugins
275
276
277 def parse_config_profile(args):
278     config = ConfigParser.RawConfigParser()
279     files = config.read(args['config_profile'])
280     if len(files) == 0:
281         raise ConfigurationError('Config Profile file %s not found!' %
282                                  args['config_profile'])
283
284     if 'globals' in config.sections():
285         G = config.options('globals')
286         for g in G:
287             val = config.get('globals', g)
288             if g in globals():
289                 globals()[g] = val
290             else:
291                 for k in globals().keys():
292                     if k.lower() == g.lower():
293                         globals()[k] = val
294                         break
295
296     if 'arguments' in config.sections():
297         A = config.options('arguments')
298         for a in A:
299             args[a] = config.get('arguments', a)
300
301     return args
302
303
304 def parse_args(plugins):
305     parser = argparse.ArgumentParser(description='Ipsilon Install Options')
306     parser.add_argument('--version',
307                         action='version', version='%(prog)s 0.1')
308     parser.add_argument('-o', '--login-managers-order', dest='lm_order',
309                         help='Comma separated list of login managers')
310     parser.add_argument('--hostname',
311                         help="Machine's fully qualified host name")
312     parser.add_argument('--instance', default='idp',
313                         help="IdP instance name, each is a separate idp")
314     parser.add_argument('--system-user', default='ipsilon',
315                         help="User account used to run the server")
316     parser.add_argument('--admin-user', default='admin',
317                         help="User account that is assigned admin privileges")
318     parser.add_argument('--database-url',
319                         default='sqlite:///%(datadir)s/%(dbname)s.sqlite',
320                         help="The (templatized) database URL to use")
321     parser.add_argument('--secure', choices=['yes', 'no'], default='yes',
322                         help="Turn on all security checks")
323     parser.add_argument('--config-profile', default=None,
324                         help=argparse.SUPPRESS)
325     parser.add_argument('--server-debugging', action='store_true',
326                         help="Enable debugging")
327     parser.add_argument('--uninstall', action='store_true',
328                         help="Uninstall the server and all data")
329     parser.add_argument('--yes', action='store_true',
330                         help="Always answer yes")
331     parser.add_argument('--admin-dburi',
332                         help='Configuration database URI (override template)')
333     parser.add_argument('--users-dburi',
334                         help='User configuration database URI (override '
335                              'template)')
336     parser.add_argument('--transaction-dburi',
337                         help='Transaction database URI (override template)')
338
339     lms = []
340
341     for plugin_group in plugins:
342         group = parser.add_argument_group(plugin_group)
343         for plugin_name in plugins[plugin_group]:
344             plugin = plugins[plugin_group][plugin_name]
345             if plugin.ptype == 'login':
346                 lms.append(plugin.name)
347             plugin.install_args(group)
348
349     args = vars(parser.parse_args())
350
351     if args['config_profile']:
352         args = parse_config_profile(args)
353
354     if not args['hostname']:
355         args['hostname'] = socket.getfqdn()
356
357     if args['uninstall']:
358         return args
359
360     if len(args['hostname'].split('.')) < 2:
361         raise ConfigurationError('Hostname: %s is not a FQDN')
362
363     for plugin_group in plugins:
364         for plugin_name in plugins[plugin_group]:
365             plugin = plugins[plugin_group][plugin_name]
366             plugin.validate_args(args)
367
368     try:
369         pwd.getpwnam(args['system_user'])
370     except KeyError:
371         raise ConfigurationError('User: %s not found on the system')
372
373     if args['lm_order'] is None:
374         args['lm_order'] = []
375         for name in lms:
376             if args[name] == 'yes':
377                 args['lm_order'].append(name)
378     else:
379         args['lm_order'] = args['lm_order'].split(',')
380
381     if len(args['lm_order']) == 0:
382         sys.exit('No login plugins are enabled.')
383
384     #FIXME: check instance is only alphanums
385
386     return args
387
388 if __name__ == '__main__':
389     opts = []
390     out = 0
391     openlogs()
392     try:
393         fplugins = find_plugins()
394         opts = parse_args(fplugins)
395
396         logger.setLevel(logging.DEBUG)
397
398         logger.info('Intallation arguments:')
399         for k in sorted(opts.iterkeys()):
400             logger.info('%s: %s', k, opts[k])
401
402         if 'uninstall' in opts and opts['uninstall'] is True:
403             if not os.path.exists(os.path.join(CONFDIR, opts['instance'])):
404                 print 'Instance %s could not be found' % opts['instance']
405                 sys.exit(0)
406             uninstall(fplugins, opts)
407         else:
408             install(fplugins, opts)
409     except Exception, e:  # pylint: disable=broad-except
410         logger.exception(e)
411         if 'uninstall' in opts and opts['uninstall'] is True:
412             print 'Uninstallation aborted.'
413         else:
414             print 'Installation aborted.'
415         print 'See log file %s for details' % LOGFILE
416         out = 1
417     except SystemExit:
418         out = 1
419         raise
420     finally:
421         if out == 0:
422             if 'uninstall' in opts and opts['uninstall'] is True:
423                 print 'Uninstallation complete.'
424             else:
425                 print 'Installation complete.'
426                 print 'Please restart HTTPD to enable the IdP instance.'
427     sys.exit(out)