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