a4410fa1503c6a7570506f80066e33befcc7df2a
[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.providers.common import ProvidersInstall
22 from ipsilon.helpers.common import EnvHelpersInstall
23 from ipsilon.util.data import Store
24 from ipsilon.tools import files
25 import ConfigParser
26 import argparse
27 import cherrypy
28 import logging
29 import os
30 import pwd
31 import shutil
32 import socket
33 import subprocess
34 import sys
35 import time
36
37
38 TEMPLATES = '/usr/share/ipsilon/templates/install'
39 CONFDIR = '/etc/ipsilon'
40 DATADIR = '/var/lib/ipsilon'
41 HTTPDCONFD = '/etc/httpd/conf.d'
42 BINDIR = '/usr/sbin'
43 STATICDIR = '/usr/share/ipsilon'
44 WSGI_SOCKET_PREFIX = None
45
46
47 class ConfigurationError(Exception):
48
49     def __init__(self, message):
50         super(ConfigurationError, self).__init__(message)
51         self.message = message
52
53     def __str__(self):
54         return repr(self.message)
55
56
57 #Silence cherrypy logging to screen
58 cherrypy.log.screen = False
59
60 # Regular logging
61 LOGFILE = '/var/log/ipsilon-install.log'
62 logger = logging.getLogger()
63
64
65 def openlogs():
66     global logger  # pylint: disable=W0603
67     if os.path.isfile(LOGFILE):
68         try:
69             created = '%s' % time.ctime(os.path.getctime(LOGFILE))
70             shutil.move(LOGFILE, '%s.%s' % (LOGFILE, created))
71         except IOError:
72             pass
73     logger = logging.getLogger()
74     try:
75         lh = logging.FileHandler(LOGFILE)
76     except IOError, e:
77         print >> sys.stderr, 'Unable to open %s (%s)' % (LOGFILE, str(e))
78         lh = logging.StreamHandler(sys.stderr)
79     formatter = logging.Formatter('[%(asctime)s] %(message)s')
80     lh.setFormatter(formatter)
81     logger.addHandler(lh)
82
83
84 def install(plugins, args):
85     logger.info('Installation initiated')
86     now = time.strftime("%Y%m%d%H%M%S", time.gmtime())
87     instance_conf = os.path.join(CONFDIR, args['instance'])
88
89     logger.info('Installing default config files')
90     ipsilon_conf = os.path.join(instance_conf, 'ipsilon.conf')
91     idp_conf = os.path.join(instance_conf, 'idp.conf')
92     args['httpd_conf'] = os.path.join(HTTPDCONFD,
93                                       'ipsilon-%s.conf' % args['instance'])
94     args['data_dir'] = os.path.join(DATADIR, args['instance'])
95     if os.path.exists(ipsilon_conf):
96         shutil.move(ipsilon_conf, '%s.bakcup.%s' % (ipsilon_conf, now))
97     if os.path.exists(idp_conf):
98         shutil.move(idp_conf, '%s.backup.%s' % (idp_conf, now))
99     if not os.path.exists(instance_conf):
100         os.makedirs(instance_conf, 0700)
101     confopts = {'instance': args['instance'],
102                 'datadir': args['data_dir'],
103                 'sysuser': args['system_user'],
104                 'ipsilondir': BINDIR,
105                 'staticdir': STATICDIR,
106                 'secure': "False" if args['secure'] == "no" else "True",
107                 'debugging': "True" if args['server_debugging'] else "False"}
108     if args['secure'] == 'no':
109         confopts['secure'] = "False"
110         confopts['sslrequiressl'] = ""
111     else:
112         confopts['secure'] = "True"
113         confopts['sslrequiressl'] = "   SSLRequireSSL"
114     if WSGI_SOCKET_PREFIX:
115         confopts['wsgi_socket'] = 'WSGISocketPrefix %s' % WSGI_SOCKET_PREFIX
116     else:
117         confopts['wsgi_socket'] = ''
118     files.write_from_template(ipsilon_conf,
119                               os.path.join(TEMPLATES, 'ipsilon.conf'),
120                               confopts)
121     files.write_from_template(idp_conf,
122                               os.path.join(TEMPLATES, 'idp.conf'),
123                               confopts)
124     if not os.path.exists(args['httpd_conf']):
125         os.symlink(idp_conf, args['httpd_conf'])
126     sessdir = os.path.join(args['data_dir'], 'sessions')
127     if not os.path.exists(sessdir):
128         os.makedirs(sessdir, 0700)
129     data_conf = os.path.join(args['data_dir'], 'ipsilon.conf')
130     if not os.path.exists(data_conf):
131         os.symlink(ipsilon_conf, data_conf)
132     # Load the cherrypy config from the newly installed file so
133     # that db paths and all is properly set before configuring
134     # components
135     cherrypy.config.update(ipsilon_conf)
136
137     # Move pre-existing admin db away
138     admin_db = cherrypy.config['admin.config.db']
139     if os.path.exists(admin_db):
140         shutil.move(admin_db, '%s.backup.%s' % (admin_db, now))
141
142     # Rebuild user db
143     users_db = cherrypy.config['user.prefs.db']
144     if os.path.exists(users_db):
145         shutil.move(users_db, '%s.backup.%s' % (users_db, now))
146     db = Store()
147     db.save_user_preferences(args['admin_user'], {'is_admin': 1})
148
149     logger.info('Configuring environment helpers')
150     for plugin_name in plugins['Environment Helpers']:
151         plugin = plugins['Environment Helpers'][plugin_name]
152         plugin.configure_server(args)
153
154     logger.info('Configuring login managers')
155     for plugin_name in args['lm_order']:
156         plugin = plugins['Login Managers'][plugin_name]
157         plugin.configure(args)
158
159     logger.info('Configuring Authentication Providers')
160     for plugin_name in plugins['Auth Providers']:
161         plugin = plugins['Auth Providers'][plugin_name]
162         plugin.configure(args)
163
164     # Fixup permissions so only the ipsilon user can read these files
165     files.fix_user_dirs(instance_conf, opts['system_user'])
166     files.fix_user_dirs(args['data_dir'], opts['system_user'])
167     try:
168         subprocess.call(['/usr/sbin/restorecon', '-R', args['data_dir']])
169     except Exception:  # pylint: disable=broad-except
170         pass
171
172 def uninstall(plugins, args):
173     logger.info('Uninstallation initiated')
174     raise Exception('Not Implemented')
175
176
177 def find_plugins():
178     plugins = {
179         'Environment Helpers': EnvHelpersInstall().plugins,
180         'Login Managers': LoginMgrsInstall().plugins,
181         'Auth Providers': ProvidersInstall().plugins
182     }
183     return plugins
184
185
186 def parse_config_profile(args):
187     config = ConfigParser.ConfigParser()
188     files = config.read(args['config_profile'])
189     if len(files) == 0:
190         raise ConfigurationError('Config Profile file %s not found!' %
191                                  args['config_profile'])
192
193     if 'globals' in config.sections():
194         G = config.options('globals')
195         for g in G:
196             val = config.get('globals', g)
197             if g in globals():
198                 globals()[g] = val
199             else:
200                 for k in globals().keys():
201                     if k.lower() == g.lower():
202                         globals()[k] = val
203                         break
204
205     if 'arguments' in config.sections():
206         A = config.options('arguments')
207         for a in A:
208             args[a] = config.get('arguments', a)
209
210     return args
211
212
213 def parse_args(plugins):
214     parser = argparse.ArgumentParser(description='Ipsilon Install Options')
215     parser.add_argument('--version',
216                         action='version', version='%(prog)s 0.1')
217     parser.add_argument('-o', '--login-managers-order', dest='lm_order',
218                         help='Comma separated list of login managers')
219     parser.add_argument('--hostname',
220                         help="Machine's fully qualified host name")
221     parser.add_argument('--instance', default='idp',
222                         help="IdP instance name, each is a separate idp")
223     parser.add_argument('--system-user', default='ipsilon',
224                         help="User account used to run the server")
225     parser.add_argument('--admin-user', default='admin',
226                         help="User account that is assigned admin privileges")
227     parser.add_argument('--secure', choices=['yes', 'no'], default='yes',
228                         help="Turn on all security checks")
229     parser.add_argument('--config-profile', default=None,
230                         help="File containing install options")
231     parser.add_argument('--server-debugging', action='store_true',
232                         help="Uninstall the server and all data")
233     parser.add_argument('--uninstall', action='store_true',
234                         help="Uninstall the server and all data")
235
236     lms = []
237
238     for plugin_group in plugins:
239         group = parser.add_argument_group(plugin_group)
240         for plugin_name in plugins[plugin_group]:
241             plugin = plugins[plugin_group][plugin_name]
242             if plugin.ptype == 'login':
243                 lms.append(plugin.name)
244             plugin.install_args(group)
245
246     args = vars(parser.parse_args())
247
248     if args['config_profile']:
249         args = parse_config_profile(args)
250
251     if not args['hostname']:
252         args['hostname'] = socket.getfqdn()
253
254     if len(args['hostname'].split('.')) < 2:
255         raise ConfigurationError('Hostname: %s is not a FQDN')
256
257     try:
258         pwd.getpwnam(args['system_user'])
259     except KeyError:
260         raise ConfigurationError('User: %s not found on the system')
261
262     if args['lm_order'] is None:
263         args['lm_order'] = []
264         for name in lms:
265             if args[name] == 'yes':
266                 args['lm_order'].append(name)
267     else:
268         args['lm_order'] = args['lm_order'].split(',')
269
270     if len(args['lm_order']) == 0:
271         #force the basic pam provider if nothing else is selected
272         if 'pam' not in args:
273             parser.print_help()
274             sys.exit(-1)
275         args['lm_order'] = ['pam']
276         args['pam'] = 'yes'
277
278     #FIXME: check instance is only alphanums
279
280     return args
281
282 if __name__ == '__main__':
283     opts = []
284     out = 0
285     openlogs()
286     try:
287         fplugins = find_plugins()
288         opts = parse_args(fplugins)
289
290         logger.setLevel(logging.DEBUG)
291
292         logger.info('Intallation arguments:')
293         for k in sorted(opts.iterkeys()):
294             logger.info('%s: %s', k, opts[k])
295
296         if 'uninstall' in opts and opts['uninstall'] is True:
297             uninstall(fplugins, opts)
298
299         install(fplugins, opts)
300     except Exception, e:  # pylint: disable=broad-except
301         logger.exception(e)
302         if 'uninstall' in opts and opts['uninstall'] is True:
303             print 'Uninstallation aborted.'
304         else:
305             print 'Installation aborted.'
306         print 'See log file %s for details' % LOGFILE
307         out = 1
308     finally:
309         if out == 0:
310             if 'uninstall' in opts and opts['uninstall'] is True:
311                 print 'Uninstallation complete.'
312             else:
313                 print 'Installation complete.'
314                 print 'Please restart HTTPD to enable the IdP instance.'
315     sys.exit(out)