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