Add uninstallation support.
[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['database_url'] % {
113                     'datadir': args['data_dir'], 'dbname': 'adminconfig'},
114                 'usersdb': args['database_url'] % {
115                     'datadir': args['data_dir'], 'dbname': 'userprefs'},
116                 'transdb': 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     # Whetehr 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
329     lms = []
330
331     for plugin_group in plugins:
332         group = parser.add_argument_group(plugin_group)
333         for plugin_name in plugins[plugin_group]:
334             plugin = plugins[plugin_group][plugin_name]
335             if plugin.ptype == 'login':
336                 lms.append(plugin.name)
337             plugin.install_args(group)
338
339     args = vars(parser.parse_args())
340
341     if args['config_profile']:
342         args = parse_config_profile(args)
343
344     if not args['hostname']:
345         args['hostname'] = socket.getfqdn()
346
347     if len(args['hostname'].split('.')) < 2:
348         raise ConfigurationError('Hostname: %s is not a FQDN')
349
350     try:
351         pwd.getpwnam(args['system_user'])
352     except KeyError:
353         raise ConfigurationError('User: %s not found on the system')
354
355     if args['lm_order'] is None:
356         args['lm_order'] = []
357         for name in lms:
358             if args[name] == 'yes':
359                 args['lm_order'].append(name)
360     else:
361         args['lm_order'] = args['lm_order'].split(',')
362
363     if len(args['lm_order']) == 0:
364         #force the basic pam provider if nothing else is selected
365         if 'pam' not in args:
366             parser.print_help()
367             sys.exit(-1)
368         args['lm_order'] = ['pam']
369         args['pam'] = 'yes'
370
371     #FIXME: check instance is only alphanums
372
373     return args
374
375 if __name__ == '__main__':
376     opts = []
377     out = 0
378     openlogs()
379     try:
380         fplugins = find_plugins()
381         opts = parse_args(fplugins)
382
383         logger.setLevel(logging.DEBUG)
384
385         logger.info('Intallation arguments:')
386         for k in sorted(opts.iterkeys()):
387             logger.info('%s: %s', k, opts[k])
388
389         if 'uninstall' in opts and opts['uninstall'] is True:
390             if not os.path.exists(os.path.join(CONFDIR, opts['instance'])):
391                 print 'Instance %s could not be found' % opts['instance']
392                 sys.exit(0)
393             uninstall(fplugins, opts)
394         else:
395             install(fplugins, opts)
396     except Exception, e:  # pylint: disable=broad-except
397         logger.exception(e)
398         if 'uninstall' in opts and opts['uninstall'] is True:
399             print 'Uninstallation aborted.'
400         else:
401             print 'Installation aborted.'
402         print 'See log file %s for details' % LOGFILE
403         out = 1
404     finally:
405         if out == 0:
406             if 'uninstall' in opts and opts['uninstall'] is True:
407                 print 'Uninstallation complete.'
408             else:
409                 print 'Installation complete.'
410                 print 'Please restart HTTPD to enable the IdP instance.'
411     sys.exit(out)