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