Add simple SqlSession implementation
[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     if os.path.exists(ipsilon_conf):
97         shutil.move(ipsilon_conf, '%s.bakcup.%s' % (ipsilon_conf, now))
98     if os.path.exists(idp_conf):
99         shutil.move(idp_conf, '%s.backup.%s' % (idp_conf, now))
100     if not os.path.exists(instance_conf):
101         os.makedirs(instance_conf, 0700)
102     confopts = {'instance': args['instance'],
103                 'datadir': args['data_dir'],
104                 'sysuser': args['system_user'],
105                 'ipsilondir': BINDIR,
106                 'staticdir': STATICDIR,
107                 'admindb': args['database_url'] % {
108                     'datadir': args['data_dir'], 'dbname': 'adminconfig'},
109                 'usersdb': args['database_url'] % {
110                     'datadir': args['data_dir'], 'dbname': 'userprefs'},
111                 'transdb': args['database_url'] % {
112                     'datadir': args['data_dir'], 'dbname': 'transactions'},
113                 'secure': "False" if args['secure'] == "no" else "True",
114                 'debugging': "True" if args['server_debugging'] else "False"}
115     # Testing database sessions
116     if 'session_type' in args:
117         confopts['sesstype'] = args['session_type']
118     else:
119         confopts['sesstype'] = 'file'
120     if 'session_dburi' in args:
121         confopts['sessopt'] = 'dburi'
122         confopts['sessval'] = args['session_dburi']
123     else:
124         confopts['sessopt'] = 'path'
125         confopts['sessval'] = os.path.join(args['data_dir'], 'sessions')
126     # Whetehr to disable security (for testing)
127     if args['secure'] == 'no':
128         confopts['secure'] = "False"
129         confopts['sslrequiressl'] = ""
130     else:
131         confopts['secure'] = "True"
132         confopts['sslrequiressl'] = "   SSLRequireSSL"
133     if WSGI_SOCKET_PREFIX:
134         confopts['wsgi_socket'] = 'WSGISocketPrefix %s' % WSGI_SOCKET_PREFIX
135     else:
136         confopts['wsgi_socket'] = ''
137     files.write_from_template(ipsilon_conf,
138                               os.path.join(TEMPLATES, 'ipsilon.conf'),
139                               confopts)
140     files.write_from_template(idp_conf,
141                               os.path.join(TEMPLATES, 'idp.conf'),
142                               confopts)
143     if not os.path.exists(args['httpd_conf']):
144         os.symlink(idp_conf, args['httpd_conf'])
145     sessdir = os.path.join(args['data_dir'], 'sessions')
146     if not os.path.exists(sessdir):
147         os.makedirs(sessdir, 0700)
148     data_conf = os.path.join(args['data_dir'], 'ipsilon.conf')
149     if not os.path.exists(data_conf):
150         os.symlink(ipsilon_conf, data_conf)
151     # Load the cherrypy config from the newly installed file so
152     # that db paths and all is properly set before configuring
153     # components
154     cherrypy.config.update(ipsilon_conf)
155
156     # Move pre-existing admin db away
157     admin_db = cherrypy.config['admin.config.db']
158     if os.path.exists(admin_db):
159         shutil.move(admin_db, '%s.backup.%s' % (admin_db, now))
160
161     # Rebuild user db
162     users_db = cherrypy.config['user.prefs.db']
163     if os.path.exists(users_db):
164         shutil.move(users_db, '%s.backup.%s' % (users_db, now))
165     db = UserStore()
166     db.save_user_preferences(args['admin_user'], {'is_admin': 1})
167
168     logger.info('Configuring environment helpers')
169     for plugin_name in plugins['Environment Helpers']:
170         plugin = plugins['Environment Helpers'][plugin_name]
171         plugin.configure_server(args)
172
173     logger.info('Configuring login managers')
174     for plugin_name in args['lm_order']:
175         plugin = plugins['Login Managers'][plugin_name]
176         plugin.configure(args)
177
178     logger.info('Configuring Info provider')
179     for plugin_name in plugins['Info Provider']:
180         plugin = plugins['Info Provider'][plugin_name]
181         plugin.configure(args)
182
183     logger.info('Configuring Authentication Providers')
184     for plugin_name in plugins['Auth Providers']:
185         plugin = plugins['Auth Providers'][plugin_name]
186         plugin.configure(args)
187
188     # Fixup permissions so only the ipsilon user can read these files
189     files.fix_user_dirs(instance_conf, opts['system_user'])
190     files.fix_user_dirs(args['data_dir'], opts['system_user'])
191     try:
192         subprocess.call(['/usr/sbin/restorecon', '-R', args['data_dir']])
193     except Exception:  # pylint: disable=broad-except
194         pass
195
196 def uninstall(plugins, args):
197     logger.info('Uninstallation initiated')
198     raise Exception('Not Implemented')
199
200
201 def find_plugins():
202     plugins = {
203         'Environment Helpers': EnvHelpersInstall().plugins,
204         'Login Managers': LoginMgrsInstall().plugins,
205         'Info Provider': InfoProviderInstall().plugins,
206         'Auth Providers': ProvidersInstall().plugins
207     }
208     return plugins
209
210
211 def parse_config_profile(args):
212     config = ConfigParser.RawConfigParser()
213     files = config.read(args['config_profile'])
214     if len(files) == 0:
215         raise ConfigurationError('Config Profile file %s not found!' %
216                                  args['config_profile'])
217
218     if 'globals' in config.sections():
219         G = config.options('globals')
220         for g in G:
221             val = config.get('globals', g)
222             if g in globals():
223                 globals()[g] = val
224             else:
225                 for k in globals().keys():
226                     if k.lower() == g.lower():
227                         globals()[k] = val
228                         break
229
230     if 'arguments' in config.sections():
231         A = config.options('arguments')
232         for a in A:
233             args[a] = config.get('arguments', a)
234
235     return args
236
237
238 def parse_args(plugins):
239     parser = argparse.ArgumentParser(description='Ipsilon Install Options')
240     parser.add_argument('--version',
241                         action='version', version='%(prog)s 0.1')
242     parser.add_argument('-o', '--login-managers-order', dest='lm_order',
243                         help='Comma separated list of login managers')
244     parser.add_argument('--hostname',
245                         help="Machine's fully qualified host name")
246     parser.add_argument('--instance', default='idp',
247                         help="IdP instance name, each is a separate idp")
248     parser.add_argument('--system-user', default='ipsilon',
249                         help="User account used to run the server")
250     parser.add_argument('--admin-user', default='admin',
251                         help="User account that is assigned admin privileges")
252     parser.add_argument('--database-url',
253                         default='sqlite:///%(datadir)s/%(dbname)s.sqlite',
254                         help="The (templatized) database URL to use")
255     parser.add_argument('--secure', choices=['yes', 'no'], default='yes',
256                         help="Turn on all security checks")
257     parser.add_argument('--config-profile', default=None,
258                         help="File containing install options")
259     parser.add_argument('--server-debugging', action='store_true',
260                         help="Uninstall the server and all data")
261     parser.add_argument('--uninstall', action='store_true',
262                         help="Uninstall the server and all data")
263
264     lms = []
265
266     for plugin_group in plugins:
267         group = parser.add_argument_group(plugin_group)
268         for plugin_name in plugins[plugin_group]:
269             plugin = plugins[plugin_group][plugin_name]
270             if plugin.ptype == 'login':
271                 lms.append(plugin.name)
272             plugin.install_args(group)
273
274     args = vars(parser.parse_args())
275
276     if args['config_profile']:
277         args = parse_config_profile(args)
278
279     if not args['hostname']:
280         args['hostname'] = socket.getfqdn()
281
282     if len(args['hostname'].split('.')) < 2:
283         raise ConfigurationError('Hostname: %s is not a FQDN')
284
285     try:
286         pwd.getpwnam(args['system_user'])
287     except KeyError:
288         raise ConfigurationError('User: %s not found on the system')
289
290     if args['lm_order'] is None:
291         args['lm_order'] = []
292         for name in lms:
293             if args[name] == 'yes':
294                 args['lm_order'].append(name)
295     else:
296         args['lm_order'] = args['lm_order'].split(',')
297
298     if len(args['lm_order']) == 0:
299         #force the basic pam provider if nothing else is selected
300         if 'pam' not in args:
301             parser.print_help()
302             sys.exit(-1)
303         args['lm_order'] = ['pam']
304         args['pam'] = 'yes'
305
306     #FIXME: check instance is only alphanums
307
308     return args
309
310 if __name__ == '__main__':
311     opts = []
312     out = 0
313     openlogs()
314     try:
315         fplugins = find_plugins()
316         opts = parse_args(fplugins)
317
318         logger.setLevel(logging.DEBUG)
319
320         logger.info('Intallation arguments:')
321         for k in sorted(opts.iterkeys()):
322             logger.info('%s: %s', k, opts[k])
323
324         if 'uninstall' in opts and opts['uninstall'] is True:
325             uninstall(fplugins, opts)
326
327         install(fplugins, opts)
328     except Exception, e:  # pylint: disable=broad-except
329         logger.exception(e)
330         if 'uninstall' in opts and opts['uninstall'] is True:
331             print 'Uninstallation aborted.'
332         else:
333             print 'Installation aborted.'
334         print 'See log file %s for details' % LOGFILE
335         out = 1
336     finally:
337         if out == 0:
338             if 'uninstall' in opts and opts['uninstall'] is True:
339                 print 'Uninstallation complete.'
340             else:
341                 print 'Installation complete.'
342                 print 'Please restart HTTPD to enable the IdP instance.'
343     sys.exit(out)