#!/usr/bin/python2 -E # # Authors: # Rob Crittenden # # Copyright (C) 2014 Red Hat # see file 'COPYING' for use and warranty information # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys import os import cherrypy import json import syslog import pwd from optparse import OptionParser from functools import wraps from cherrypy import response from cherrypy.process import plugins from ipalib import api from ipalib import errors from ipalib import util from ipaserver.rpcserver import json_encode_binary from ipapython.version import VERSION def jsonout(func): '''JSON output decorator''' @wraps(func) def wrapper(*args, **kw): value = func(*args, **kw) response.headers["Content-Type"] = "application/json;charset=utf-8" data = json_encode_binary(value) return json.dumps(data, sort_keys=True, indent=2) return wrapper def handle_error(status, message, traceback, version): """ Return basic messages to user and log backtrace in case of 500 error. """ if status.startswith('500'): cherrypy.log(msg=message + '\n', context='IPA', traceback=True) resp = cherrypy.response resp.headers['Content-Type'] = 'application/json' return json.dumps({'status': status, 'message': message}) def convert_unicode(value): """ IPA requires all incoming values to be unicode. Recursively convert the values. """ if not isinstance(value, basestring): return value if value is not None: return unicode(value) else: return None def Command(command, *args, **options): if (cherrypy.request.config.get('local_only', False) and cherrypy.request.remote.ip not in ['::1', '127.0.0.1']): raise IPAError( status=401, message="Not a local request" ) if not api.Backend.rpcclient.isconnected(): api.Backend.rpcclient.connect() try: if not api.Backend.rpcclient.isconnected(): api.Backend.rpcclient.connect() except errors.CCacheError, e: raise IPAError( status=401, message=e ) # IPA wants all its strings as unicode args = map(lambda v: convert_unicode(v), args) options = dict(zip(options, map(convert_unicode, options.values()))) params = api.Command[command].args_options_2_params(*args, **options) cherrypy.log(context='IPA', msg='%s(%s)' % (command, ', '.join(api.Command[command]._repr_iter(**params)))) try: return api.Command[command](*args, **options)['result'] except (errors.DuplicateEntry, errors.DNSNotARecordError, errors.ValidationError, errors.ConversionError,) as e: raise IPAError( status=400, message=e ) except errors.ACIError, e: raise IPAError( status=401, message=e ) except errors.NotFound, e: raise IPAError( status=404, message=e ) except Exception, e: raise IPAError( status=500, message=e ) @jsonout def GET(command, *args, **options): return Command(command, *args, **options) @jsonout def POST(command, *args, **options): cherrypy.response.status = 201 return Command(command, *args, **options) @jsonout def DELETE(command, *args, **options): return Command(command, *args, **options) class IPAError(cherrypy.HTTPError): """ Return errors in IPA-style json. Local errors are treated as strings so do not include the code and name attributes within the error dict. This is not padded for IE. """ def set_response(self): response = cherrypy.serving.response cherrypy._cperror.clean_headers(self.code) # In all cases, finalize will be called after this method, # so don't bother cleaning up response values here. response.status = self.status if isinstance(self._message, Exception): error = {'code': self._message.errno, 'message': self._message.message, 'name': self._message.__class__.__name__} elif isinstance(self._message, basestring): error = {'message': self._message} else: error = {'message': 'Unable to handle error message type %s' % type(self._message)} response.body = json.dumps({'error': error, 'id': 0, 'principal': util.get_current_principal(), 'result': None, 'version': VERSION}, sort_keys=True, indent=2) class Host(object): exposed = True def GET(self, fqdn=None): if fqdn is None: command = 'host_find' else: command = 'host_show' return GET(command, fqdn) def POST(self, hostname, description=None, random=False, macaddress=None, userclass=None, ip_address=None, password=None): return POST('host_add', hostname, description=description, random=random, force=True, macaddress=macaddress, userclass=userclass, ip_address=ip_address, userpassword=password) def DELETE(self, fqdn): return DELETE('host_del', fqdn) class Hostgroup(object): exposed = True def GET(self, name=None): if name is None: command = 'hostgroup_find' else: command = 'hostgroup_show' return GET(command, name) def POST(self, name=None, description=None): cherrypy.response.status = 201 return POST('hostgroup_add', name, description=description,) def DELETE(self, name): return DELETE('hostgroup_del', name) class Features(object): exposed = True def GET(self): return '["realm"]' def start(config=None, daemonize=False, pidfile=None): # Set the umask so only the owner can read the log files old_umask = os.umask(077) cherrypy.tree.mount( Features(), '/features', {'/': {'request.dispatch': cherrypy.dispatch.MethodDispatcher()} } ) cherrypy.tree.mount( Host(), '/ipa/smartproxy/host', {'/': {'request.dispatch': cherrypy.dispatch.MethodDispatcher()} } ) cherrypy.tree.mount( Hostgroup(), '/ipa/smartproxy/hostgroup', {'/': {'request.dispatch': cherrypy.dispatch.MethodDispatcher()} } ) api.bootstrap(context='ipasmartproxy') api.finalize() # Register the domain for requests from Foreman cherrypy.tree.mount( Host(), '/realm/%s' % api.env.domain, {'/': {'request.dispatch': cherrypy.dispatch.MethodDispatcher()} } ) for c in config or []: try: cherrypy.config.update(c) except (IOError, OSError), e: cherrypy.log(msg="Exception trying to load %s: %s" % (c, e), context='IPA', traceback=False) return 1 # Log files are created, reset umask os.umask(old_umask) user = cherrypy.config.get('user', None) if user is None: cherrypy.log(msg="User is required", context='IPA', traceback=False) return 1 pent = pwd.getpwnam(user) if daemonize: cherrypy.config.update({'log.screen': False}) plugins.Daemonizer(cherrypy.engine).subscribe() if pidfile: plugins.PIDFile(cherrypy.engine, pidfile).subscribe() cherrypy.engine.signal_handler.subscribe() cherrypy.config.update({'error_page.500': handle_error}) # If you don't use GSS-Proxy you're on your own in ensuring that # there is always a valid ticket for the smartproxy to use. if cherrypy.config.get('use_gssproxy', False): cherrypy.log(msg="Enabling GSS-Proxy", context='IPA') os.environ['GSS_USE_PROXY'] = '1' if os.geteuid() == 0: cherrypy.log(msg="Dropping root privileges to %s" % user, context='IPA') plugins.DropPrivileges(cherrypy.engine, uid=pent.pw_uid, gid=pent.pw_gid).subscribe() try: cherrypy.engine.start() except Exception: return 1 else: cherrypy.engine.block() return 0 if __name__ == '__main__': p = OptionParser() p.add_option('-c', '--config', action="append", dest='config', help="specify config file(s)") p.add_option('-d', action="store_true", dest='daemonize', help="run the server as a daemon") p.add_option('-p', '--pidfile', dest='pidfile', default=None, help="store the process id in the given file") options, args = p.parse_args() try: sys.exit(start(options.config, options.daemonize, options.pidfile)) except Exception, e: cherrypy.log(msg="Exception trying to start: %s" % e, context='IPA', traceback=True) syslog.syslog(syslog.LOG_ERR, "Exception trying to start: %s" % e)