Use python logging in install / log cherrypy at right severity
[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/libexec'
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     lh.setLevel(logging.DEBUG)
83     logger.addHandler(lh)
84     logger.propagate = False
85     ch = logging.StreamHandler(sys.stdout)
86     formatter = logging.Formatter('%(message)s')
87     ch.setFormatter(formatter)
88     ch.setLevel(logging.INFO)
89     logger.addHandler(ch)
90     cherrypy.log.error_log.setLevel(logging.DEBUG)
91
92
93 def install(plugins, args):
94     logger.info('Installation initiated')
95     now = time.strftime("%Y%m%d%H%M%S", time.gmtime())
96     instance_conf = os.path.join(CONFDIR, args['instance'])
97
98     logger.info('Installing default config files')
99     ipsilon_conf = os.path.join(instance_conf, 'ipsilon.conf')
100     idp_conf = os.path.join(instance_conf, 'idp.conf')
101     args['httpd_conf'] = os.path.join(HTTPDCONFD,
102                                       'ipsilon-%s.conf' % args['instance'])
103     args['data_dir'] = os.path.join(DATADIR, args['instance'])
104     args['public_data_dir'] = os.path.join(args['data_dir'], 'public')
105     args['wellknown_dir'] = os.path.join(args['public_data_dir'],
106                                          'well-known')
107     if os.path.exists(ipsilon_conf):
108         shutil.move(ipsilon_conf, '%s.bakcup.%s' % (ipsilon_conf, now))
109     if os.path.exists(idp_conf):
110         shutil.move(idp_conf, '%s.backup.%s' % (idp_conf, now))
111     if not os.path.exists(instance_conf):
112         os.makedirs(instance_conf, 0700)
113     confopts = {'instance': args['instance'],
114                 'datadir': args['data_dir'],
115                 'publicdatadir': args['public_data_dir'],
116                 'wellknowndir': args['wellknown_dir'],
117                 'sysuser': args['system_user'],
118                 'ipsilondir': BINDIR,
119                 'staticdir': STATICDIR,
120                 'admindb': args['admin_dburi'] or args['database_url'] % {
121                     'datadir': args['data_dir'], 'dbname': 'adminconfig'},
122                 'usersdb': args['users_dburi'] or args['database_url'] % {
123                     'datadir': args['data_dir'], 'dbname': 'userprefs'},
124                 'transdb': args['transaction_dburi'] or args['database_url'] %
125                 {'datadir': args['data_dir'], 'dbname': 'transactions'},
126                 'secure': "False" if args['secure'] == "no" else "True",
127                 'debugging': "True" if args['server_debugging'] else "False"}
128     # Testing database sessions
129     if 'session_type' in args:
130         confopts['sesstype'] = args['session_type']
131     else:
132         confopts['sesstype'] = 'file'
133     if 'session_dburi' in args:
134         confopts['sessopt'] = 'dburi'
135         confopts['sessval'] = args['session_dburi']
136     else:
137         confopts['sessopt'] = 'path'
138         confopts['sessval'] = os.path.join(args['data_dir'], 'sessions')
139     # Whether to disable security (for testing)
140     if args['secure'] == 'no':
141         confopts['secure'] = "False"
142         confopts['sslrequiressl'] = ""
143     else:
144         confopts['secure'] = "True"
145         confopts['sslrequiressl'] = "   SSLRequireSSL"
146     if WSGI_SOCKET_PREFIX:
147         confopts['wsgi_socket'] = 'WSGISocketPrefix %s' % WSGI_SOCKET_PREFIX
148     else:
149         confopts['wsgi_socket'] = ''
150     files.write_from_template(ipsilon_conf,
151                               os.path.join(TEMPLATES, 'ipsilon.conf'),
152                               confopts)
153     files.write_from_template(idp_conf,
154                               os.path.join(TEMPLATES, 'idp.conf'),
155                               confopts)
156     if not os.path.exists(args['httpd_conf']):
157         os.symlink(idp_conf, args['httpd_conf'])
158     if not os.path.exists(args['public_data_dir']):
159         os.makedirs(args['public_data_dir'], 0755)
160     if not os.path.exists(args['wellknown_dir']):
161         os.makedirs(args['wellknown_dir'], 0755)
162     sessdir = os.path.join(args['data_dir'], 'sessions')
163     if not os.path.exists(sessdir):
164         os.makedirs(sessdir, 0700)
165     data_conf = os.path.join(args['data_dir'], 'ipsilon.conf')
166     if not os.path.exists(data_conf):
167         os.symlink(ipsilon_conf, data_conf)
168     # Load the cherrypy config from the newly installed file so
169     # that db paths and all is properly set before configuring
170     # components
171     cherrypy.config.update(ipsilon_conf)
172
173     # Move pre-existing admin db away
174     admin_db = cherrypy.config['admin.config.db']
175     if os.path.exists(admin_db):
176         shutil.move(admin_db, '%s.backup.%s' % (admin_db, now))
177
178     # Rebuild user db
179     users_db = cherrypy.config['user.prefs.db']
180     if os.path.exists(users_db):
181         shutil.move(users_db, '%s.backup.%s' % (users_db, now))
182     db = UserStore()
183     db.save_user_preferences(args['admin_user'], {'is_admin': 1})
184
185     logger.info('Configuring environment helpers')
186     for plugin_name in plugins['Environment Helpers']:
187         plugin = plugins['Environment Helpers'][plugin_name]
188         if plugin.configure_server(args) == False:
189             logger.info('Configuration of environment helper %s failed' % plugin_name)
190
191     logger.info('Configuring login managers')
192     for plugin_name in args['lm_order']:
193         try:
194             plugin = plugins['Login Managers'][plugin_name]
195         except KeyError:
196             sys.exit('Login provider %s not installed' % plugin_name)
197         if plugin.configure(args) == False:
198             logger.info('Configuration of login manager %s failed' % plugin_name)
199
200     logger.info('Configuring Info provider')
201     for plugin_name in plugins['Info Provider']:
202         plugin = plugins['Info Provider'][plugin_name]
203         if plugin.configure(args) == False:
204             logger.info('Configuration of info provider %s failed' % plugin_name)
205
206     logger.info('Configuring Authentication Providers')
207     for plugin_name in plugins['Auth Providers']:
208         plugin = plugins['Auth Providers'][plugin_name]
209         if plugin.configure(args) == False:
210             logger.info('Configuration of auth provider %s failed' % plugin_name)
211
212     # Fixup permissions so only the ipsilon user can read these files
213     files.fix_user_dirs(instance_conf, opts['system_user'])
214     files.fix_user_dirs(args['data_dir'], opts['system_user'])
215     try:
216         subprocess.call(['/usr/sbin/restorecon', '-R', args['data_dir']])
217     except Exception:  # pylint: disable=broad-except
218         pass
219
220
221 def uninstall(plugins, args):
222     logger.info('Uninstallation initiated')
223     instance_conf = os.path.join(CONFDIR, args['instance'])
224
225     httpd_conf = os.path.join(HTTPDCONFD,
226                               'ipsilon-%s.conf' % args['instance'])
227     data_dir = os.path.join(DATADIR, args['instance'])
228
229     if not os.path.exists(instance_conf):
230         raise Exception('Could not find instance %s configuration'
231                         % args['instance'])
232     if not os.path.exists(httpd_conf):
233         raise Exception('Could not find instance %s httpd configuration'
234                         % args['instance'])
235     if not args['yes']:
236         sure = raw_input(('Are you certain you want to erase instance %s ' +
237                           '[yes/NO]: ')
238                          % args['instance'])
239         if sure != 'yes':
240             raise Exception('Aborting')
241
242     logger.info('Removing environment helpers')
243     for plugin_name in plugins['Environment Helpers']:
244         plugin = plugins['Environment Helpers'][plugin_name]
245         if plugin.unconfigure(args) == False:
246             logger.info('Removal of environment helper %s failed' % plugin_name)
247
248     logger.info('Removing login managers')
249     for plugin_name in plugins['Login Managers']:
250         plugin = plugins['Login Managers'][plugin_name]
251         if plugin.unconfigure(args) == False:
252             logger.info('Removal of login manager %s failed' % plugin_name)
253
254     logger.info('Removing Info providers')
255     for plugin_name in plugins['Info Provider']:
256         plugin = plugins['Info Provider'][plugin_name]
257         if plugin.unconfigure(args) == False:
258             logger.info('Removal of info provider %s failed' % plugin_name)
259
260     logger.info('Removing Authentication Providers')
261     for plugin_name in plugins['Auth Providers']:
262         plugin = plugins['Auth Providers'][plugin_name]
263         if plugin.unconfigure(args) == False:
264             logger.info('Removal of auth provider %s failed' % plugin_name)
265
266     logger.info('Removing httpd configuration')
267     os.remove(httpd_conf)
268     logger.info('Erasing instance configuration')
269     shutil.rmtree(instance_conf)
270     logger.info('Erasing instance data')
271     shutil.rmtree(data_dir)
272     logger.info('Uninstalled instance %s' % args['instance'])
273
274
275 def find_plugins():
276     plugins = {
277         'Environment Helpers': EnvHelpersInstall().plugins,
278         'Login Managers': LoginMgrsInstall().plugins,
279         'Info Provider': InfoProviderInstall().plugins,
280         'Auth Providers': ProvidersInstall().plugins
281     }
282     return plugins
283
284
285 def parse_config_profile(args):
286     config = ConfigParser.RawConfigParser()
287     files = config.read(args['config_profile'])
288     if len(files) == 0:
289         raise ConfigurationError('Config Profile file %s not found!' %
290                                  args['config_profile'])
291
292     if 'globals' in config.sections():
293         G = config.options('globals')
294         for g in G:
295             val = config.get('globals', g)
296             if g in globals():
297                 globals()[g] = val
298             else:
299                 for k in globals().keys():
300                     if k.lower() == g.lower():
301                         globals()[k] = val
302                         break
303
304     if 'arguments' in config.sections():
305         A = config.options('arguments')
306         for a in A:
307             args[a] = config.get('arguments', a)
308
309     return args
310
311
312 def parse_args(plugins):
313     parser = argparse.ArgumentParser(description='Ipsilon Install Options')
314     parser.add_argument('--version',
315                         action='version', version='%(prog)s 0.1')
316     parser.add_argument('-o', '--login-managers-order', dest='lm_order',
317                         help='Comma separated list of login managers')
318     parser.add_argument('--hostname',
319                         help="Machine's fully qualified host name")
320     parser.add_argument('--instance', default='idp',
321                         help="IdP instance name, each is a separate idp")
322     parser.add_argument('--system-user', default='ipsilon',
323                         help="User account used to run the server")
324     parser.add_argument('--admin-user', default='admin',
325                         help="User account that is assigned admin privileges")
326     parser.add_argument('--database-url',
327                         default='sqlite:///%(datadir)s/%(dbname)s.sqlite',
328                         help="The (templatized) database URL to use")
329     parser.add_argument('--secure', choices=['yes', 'no'], default='yes',
330                         help="Turn on all security checks")
331     parser.add_argument('--config-profile', default=None,
332                         help=argparse.SUPPRESS)
333     parser.add_argument('--server-debugging', action='store_true',
334                         help="Enable debugging")
335     parser.add_argument('--uninstall', action='store_true',
336                         help="Uninstall the server and all data")
337     parser.add_argument('--yes', action='store_true',
338                         help="Always answer yes")
339     parser.add_argument('--admin-dburi',
340                         help='Configuration database URI (override template)')
341     parser.add_argument('--users-dburi',
342                         help='User configuration database URI (override '
343                              'template)')
344     parser.add_argument('--transaction-dburi',
345                         help='Transaction database URI (override template)')
346
347     lms = []
348
349     for plugin_group in plugins:
350         group = parser.add_argument_group(plugin_group)
351         for plugin_name in plugins[plugin_group]:
352             plugin = plugins[plugin_group][plugin_name]
353             if plugin.ptype == 'login':
354                 lms.append(plugin.name)
355             plugin.install_args(group)
356
357     args = vars(parser.parse_args())
358
359     if args['config_profile']:
360         args = parse_config_profile(args)
361
362     if not args['hostname']:
363         args['hostname'] = socket.getfqdn()
364
365     if args['uninstall']:
366         return args
367
368     if len(args['hostname'].split('.')) < 2:
369         raise ConfigurationError('Hostname: %s is not a FQDN')
370
371     for plugin_group in plugins:
372         for plugin_name in plugins[plugin_group]:
373             plugin = plugins[plugin_group][plugin_name]
374             plugin.validate_args(args)
375
376     try:
377         pwd.getpwnam(args['system_user'])
378     except KeyError:
379         raise ConfigurationError('User: %s not found on the system')
380
381     if args['lm_order'] is None:
382         args['lm_order'] = []
383         for name in lms:
384             if args[name] == 'yes':
385                 args['lm_order'].append(name)
386     else:
387         args['lm_order'] = args['lm_order'].split(',')
388
389     if len(args['lm_order']) == 0:
390         sys.exit('No login plugins are enabled.')
391
392     #FIXME: check instance is only alphanums
393
394     return args
395
396 if __name__ == '__main__':
397     opts = []
398     out = 0
399     openlogs()
400     try:
401         fplugins = find_plugins()
402         opts = parse_args(fplugins)
403
404         logger.setLevel(logging.DEBUG)
405
406         logger.debug('Installation arguments:')
407         for k in sorted(opts.iterkeys()):
408             logger.debug('%s: %s', k, opts[k])
409
410         if 'uninstall' in opts and opts['uninstall'] is True:
411             if not os.path.exists(os.path.join(CONFDIR, opts['instance'])):
412                 logger.info('Instance %s could not be found' % opts['instance'])
413                 sys.exit(0)
414             uninstall(fplugins, opts)
415         else:
416             install(fplugins, opts)
417     except Exception, e:  # pylint: disable=broad-except
418         logger.exception(e)
419         if 'uninstall' in opts and opts['uninstall'] is True:
420             logger.info('Uninstallation aborted.')
421         else:
422             logger.info('Installation aborted.')
423         logger.info('See log file %s for details' % LOGFILE)
424         out = 1
425     except SystemExit:
426         out = 1
427         raise
428     finally:
429         if out == 0:
430             if 'uninstall' in opts and opts['uninstall'] is True:
431                 logger.info('Uninstallation complete.')
432             else:
433                 logger.info('Installation complete.')
434                 logger.info('Please restart HTTPD to enable the IdP instance.')
435     sys.exit(out)