dfd2ffe1a9bbef084d89294b37e591d4429c5a1d
[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 def uninstall(plugins, args):
210     logger.info('Uninstallation initiated')
211     raise Exception('Not Implemented')
212
213
214 def find_plugins():
215     plugins = {
216         'Environment Helpers': EnvHelpersInstall().plugins,
217         'Login Managers': LoginMgrsInstall().plugins,
218         'Info Provider': InfoProviderInstall().plugins,
219         'Auth Providers': ProvidersInstall().plugins
220     }
221     return plugins
222
223
224 def parse_config_profile(args):
225     config = ConfigParser.RawConfigParser()
226     files = config.read(args['config_profile'])
227     if len(files) == 0:
228         raise ConfigurationError('Config Profile file %s not found!' %
229                                  args['config_profile'])
230
231     if 'globals' in config.sections():
232         G = config.options('globals')
233         for g in G:
234             val = config.get('globals', g)
235             if g in globals():
236                 globals()[g] = val
237             else:
238                 for k in globals().keys():
239                     if k.lower() == g.lower():
240                         globals()[k] = val
241                         break
242
243     if 'arguments' in config.sections():
244         A = config.options('arguments')
245         for a in A:
246             args[a] = config.get('arguments', a)
247
248     return args
249
250
251 def parse_args(plugins):
252     parser = argparse.ArgumentParser(description='Ipsilon Install Options')
253     parser.add_argument('--version',
254                         action='version', version='%(prog)s 0.1')
255     parser.add_argument('-o', '--login-managers-order', dest='lm_order',
256                         help='Comma separated list of login managers')
257     parser.add_argument('--hostname',
258                         help="Machine's fully qualified host name")
259     parser.add_argument('--instance', default='idp',
260                         help="IdP instance name, each is a separate idp")
261     parser.add_argument('--system-user', default='ipsilon',
262                         help="User account used to run the server")
263     parser.add_argument('--admin-user', default='admin',
264                         help="User account that is assigned admin privileges")
265     parser.add_argument('--database-url',
266                         default='sqlite:///%(datadir)s/%(dbname)s.sqlite',
267                         help="The (templatized) database URL to use")
268     parser.add_argument('--secure', choices=['yes', 'no'], default='yes',
269                         help="Turn on all security checks")
270     parser.add_argument('--config-profile', default=None,
271                         help="File containing install options")
272     parser.add_argument('--server-debugging', action='store_true',
273                         help="Enable debugging")
274     parser.add_argument('--uninstall', action='store_true',
275                         help="Uninstall the server and all data")
276
277     lms = []
278
279     for plugin_group in plugins:
280         group = parser.add_argument_group(plugin_group)
281         for plugin_name in plugins[plugin_group]:
282             plugin = plugins[plugin_group][plugin_name]
283             if plugin.ptype == 'login':
284                 lms.append(plugin.name)
285             plugin.install_args(group)
286
287     args = vars(parser.parse_args())
288
289     if args['config_profile']:
290         args = parse_config_profile(args)
291
292     if not args['hostname']:
293         args['hostname'] = socket.getfqdn()
294
295     if len(args['hostname'].split('.')) < 2:
296         raise ConfigurationError('Hostname: %s is not a FQDN')
297
298     try:
299         pwd.getpwnam(args['system_user'])
300     except KeyError:
301         raise ConfigurationError('User: %s not found on the system')
302
303     if args['lm_order'] is None:
304         args['lm_order'] = []
305         for name in lms:
306             if args[name] == 'yes':
307                 args['lm_order'].append(name)
308     else:
309         args['lm_order'] = args['lm_order'].split(',')
310
311     if len(args['lm_order']) == 0:
312         #force the basic pam provider if nothing else is selected
313         if 'pam' not in args:
314             parser.print_help()
315             sys.exit(-1)
316         args['lm_order'] = ['pam']
317         args['pam'] = 'yes'
318
319     #FIXME: check instance is only alphanums
320
321     return args
322
323 if __name__ == '__main__':
324     opts = []
325     out = 0
326     openlogs()
327     try:
328         fplugins = find_plugins()
329         opts = parse_args(fplugins)
330
331         logger.setLevel(logging.DEBUG)
332
333         logger.info('Intallation arguments:')
334         for k in sorted(opts.iterkeys()):
335             logger.info('%s: %s', k, opts[k])
336
337         if 'uninstall' in opts and opts['uninstall'] is True:
338             uninstall(fplugins, opts)
339
340         install(fplugins, opts)
341     except Exception, e:  # pylint: disable=broad-except
342         logger.exception(e)
343         if 'uninstall' in opts and opts['uninstall'] is True:
344             print 'Uninstallation aborted.'
345         else:
346             print 'Installation aborted.'
347         print 'See log file %s for details' % LOGFILE
348         out = 1
349     finally:
350         if out == 0:
351             if 'uninstall' in opts and opts['uninstall'] is True:
352                 print 'Uninstallation complete.'
353             else:
354                 print 'Installation complete.'
355                 print 'Please restart HTTPD to enable the IdP instance.'
356     sys.exit(out)