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