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