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