Add options to explicitly set database uris during install
[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         plugin = plugins['Login Managers'][plugin_name]
186         if plugin.configure(args) == False:
187             print 'Configuration of login manager %s failed' % plugin_name
188
189     logger.info('Configuring Info provider')
190     for plugin_name in plugins['Info Provider']:
191         plugin = plugins['Info Provider'][plugin_name]
192         if plugin.configure(args) == False:
193             print 'Configuration of info provider %s failed' % plugin_name
194
195     logger.info('Configuring Authentication Providers')
196     for plugin_name in plugins['Auth Providers']:
197         plugin = plugins['Auth Providers'][plugin_name]
198         if plugin.configure(args) == False:
199             print 'Configuration of auth provider %s failed' % plugin_name
200
201     # Fixup permissions so only the ipsilon user can read these files
202     files.fix_user_dirs(instance_conf, opts['system_user'])
203     files.fix_user_dirs(args['data_dir'], opts['system_user'])
204     try:
205         subprocess.call(['/usr/sbin/restorecon', '-R', args['data_dir']])
206     except Exception:  # pylint: disable=broad-except
207         pass
208
209
210 def uninstall(plugins, args):
211     logger.info('Uninstallation initiated')
212     instance_conf = os.path.join(CONFDIR, args['instance'])
213
214     httpd_conf = os.path.join(HTTPDCONFD,
215                               'ipsilon-%s.conf' % args['instance'])
216     data_dir = os.path.join(DATADIR, args['instance'])
217
218     if not os.path.exists(instance_conf):
219         raise Exception('Could not find instance %s configuration'
220                         % args['instance'])
221     if not os.path.exists(httpd_conf):
222         raise Exception('Could not find instance %s httpd configuration'
223                         % args['instance'])
224     if not args['yes']:
225         sure = raw_input(('Are you certain you want to erase instance %s ' +
226                           '[yes/NO]: ')
227                          % args['instance'])
228         if sure != 'yes':
229             raise Exception('Aborting')
230
231     logger.info('Removing environment helpers')
232     for plugin_name in plugins['Environment Helpers']:
233         plugin = plugins['Environment Helpers'][plugin_name]
234         if plugin.unconfigure(args) == False:
235             print 'Removal of environment helper %s failed' % plugin_name
236
237     logger.info('Removing login managers')
238     for plugin_name in args['lm_order']:
239         plugin = plugins['Login Managers'][plugin_name]
240         if plugin.unconfigure(args) == False:
241             print 'Removal of login manager %s failed' % plugin_name
242
243     logger.info('Removing Info providers')
244     for plugin_name in plugins['Info Provider']:
245         plugin = plugins['Info Provider'][plugin_name]
246         if plugin.unconfigure(args) == False:
247             print 'Removal of info provider %s failed' % plugin_name
248
249     logger.info('Removing Authentication Providers')
250     for plugin_name in plugins['Auth Providers']:
251         plugin = plugins['Auth Providers'][plugin_name]
252         if plugin.unconfigure(args) == False:
253             print 'Removal of auth provider %s failed' % plugin_name
254
255     logger.info('Removing httpd configuration')
256     os.remove(httpd_conf)
257     logger.info('Erasing instance configuration')
258     shutil.rmtree(instance_conf)
259     logger.info('Erasing instance data')
260     shutil.rmtree(data_dir)
261     logger.info('Uninstalled instance %s' % args['instance'])
262
263
264 def find_plugins():
265     plugins = {
266         'Environment Helpers': EnvHelpersInstall().plugins,
267         'Login Managers': LoginMgrsInstall().plugins,
268         'Info Provider': InfoProviderInstall().plugins,
269         'Auth Providers': ProvidersInstall().plugins
270     }
271     return plugins
272
273
274 def parse_config_profile(args):
275     config = ConfigParser.RawConfigParser()
276     files = config.read(args['config_profile'])
277     if len(files) == 0:
278         raise ConfigurationError('Config Profile file %s not found!' %
279                                  args['config_profile'])
280
281     if 'globals' in config.sections():
282         G = config.options('globals')
283         for g in G:
284             val = config.get('globals', g)
285             if g in globals():
286                 globals()[g] = val
287             else:
288                 for k in globals().keys():
289                     if k.lower() == g.lower():
290                         globals()[k] = val
291                         break
292
293     if 'arguments' in config.sections():
294         A = config.options('arguments')
295         for a in A:
296             args[a] = config.get('arguments', a)
297
298     return args
299
300
301 def parse_args(plugins):
302     parser = argparse.ArgumentParser(description='Ipsilon Install Options')
303     parser.add_argument('--version',
304                         action='version', version='%(prog)s 0.1')
305     parser.add_argument('-o', '--login-managers-order', dest='lm_order',
306                         help='Comma separated list of login managers')
307     parser.add_argument('--hostname',
308                         help="Machine's fully qualified host name")
309     parser.add_argument('--instance', default='idp',
310                         help="IdP instance name, each is a separate idp")
311     parser.add_argument('--system-user', default='ipsilon',
312                         help="User account used to run the server")
313     parser.add_argument('--admin-user', default='admin',
314                         help="User account that is assigned admin privileges")
315     parser.add_argument('--database-url',
316                         default='sqlite:///%(datadir)s/%(dbname)s.sqlite',
317                         help="The (templatized) database URL to use")
318     parser.add_argument('--secure', choices=['yes', 'no'], default='yes',
319                         help="Turn on all security checks")
320     parser.add_argument('--config-profile', default=None,
321                         help="File containing install options")
322     parser.add_argument('--server-debugging', action='store_true',
323                         help="Enable debugging")
324     parser.add_argument('--uninstall', action='store_true',
325                         help="Uninstall the server and all data")
326     parser.add_argument('--yes', action='store_true',
327                         help="Always answer yes")
328     parser.add_argument('--admin-dburi',
329                         help='Configuration database URI (override template)')
330     parser.add_argument('--users-dburi',
331                         help='User configuration database URI (override '
332                              'template)')
333     parser.add_argument('--transaction-dburi',
334                         help='Transaction database URI (override template)')
335
336     lms = []
337
338     for plugin_group in plugins:
339         group = parser.add_argument_group(plugin_group)
340         for plugin_name in plugins[plugin_group]:
341             plugin = plugins[plugin_group][plugin_name]
342             if plugin.ptype == 'login':
343                 lms.append(plugin.name)
344             plugin.install_args(group)
345
346     args = vars(parser.parse_args())
347
348     if args['config_profile']:
349         args = parse_config_profile(args)
350
351     if not args['hostname']:
352         args['hostname'] = socket.getfqdn()
353
354     if len(args['hostname'].split('.')) < 2:
355         raise ConfigurationError('Hostname: %s is not a FQDN')
356
357     for plugin_group in plugins:
358         for plugin_name in plugins[plugin_group]:
359             plugin = plugins[plugin_group][plugin_name]
360             plugin.validate_args(args)
361
362     try:
363         pwd.getpwnam(args['system_user'])
364     except KeyError:
365         raise ConfigurationError('User: %s not found on the system')
366
367     if args['lm_order'] is None:
368         args['lm_order'] = []
369         for name in lms:
370             if args[name] == 'yes':
371                 args['lm_order'].append(name)
372     else:
373         args['lm_order'] = args['lm_order'].split(',')
374
375     if len(args['lm_order']) == 0:
376         #force the basic pam provider if nothing else is selected
377         if 'pam' not in args:
378             parser.print_help()
379             sys.exit(-1)
380         args['lm_order'] = ['pam']
381         args['pam'] = 'yes'
382
383     #FIXME: check instance is only alphanums
384
385     return args
386
387 if __name__ == '__main__':
388     opts = []
389     out = 0
390     openlogs()
391     try:
392         fplugins = find_plugins()
393         opts = parse_args(fplugins)
394
395         logger.setLevel(logging.DEBUG)
396
397         logger.info('Intallation arguments:')
398         for k in sorted(opts.iterkeys()):
399             logger.info('%s: %s', k, opts[k])
400
401         if 'uninstall' in opts and opts['uninstall'] is True:
402             if not os.path.exists(os.path.join(CONFDIR, opts['instance'])):
403                 print 'Instance %s could not be found' % opts['instance']
404                 sys.exit(0)
405             uninstall(fplugins, opts)
406         else:
407             install(fplugins, opts)
408     except Exception, e:  # pylint: disable=broad-except
409         logger.exception(e)
410         if 'uninstall' in opts and opts['uninstall'] is True:
411             print 'Uninstallation aborted.'
412         else:
413             print 'Installation aborted.'
414         print 'See log file %s for details' % LOGFILE
415         out = 1
416     finally:
417         if out == 0:
418             if 'uninstall' in opts and opts['uninstall'] is True:
419                 print 'Uninstallation complete.'
420             else:
421                 print 'Installation complete.'
422                 print 'Please restart HTTPD to enable the IdP instance.'
423     sys.exit(out)