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