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