diff --git a/novajoin/configure_ipa.py b/novajoin/configure_ipa.py new file mode 100644 index 0000000..d918243 --- /dev/null +++ b/novajoin/configure_ipa.py @@ -0,0 +1,208 @@ +#!/usr/bin/python + +import getpass +import logging +import os +import pwd +import socket +import sys +import tempfile +from ipalib import api +from ipalib import errors +from ipapython.ipautil import run, kinit_password, user_input +from novajoin.errors import ConfigurationError + +logger = logging.getLogger() + + +class NovajoinRole(object): + """ + One-stop shopping for creating the IPA permissions, privilege and role. + + Assumes that ipalib is imported and initialized and an RPC context + already exists. + """ + + def __init__(self, keytab='/etc/join/krb5.keytab', user='nova'): + self.keytab = keytab + self.user = user + self.service = u'nova/%s' % self._get_fqdn() + self.ccache_name = None + + def _get_fqdn(self): + """ + Try to determine the fully-qualfied domain name of this box + """ + fqdn = "" + try: + fqdn = socket.getfqdn() + except Exception: # pylint: disable=broad-except + try: + # assume it is in the IPA domain if it comes back + # not fully-qualified + fqdn = socket.gethostname() + fqdn = fqdn + '.' + api.env.domain + except Exception: # pylint: disable=broad-except + fqdn = "" + return fqdn + + def kinit(self, principal, password): + ccache_dir = tempfile.mkdtemp(prefix='krbcc') + self.ccache_name = os.path.join(ccache_dir, 'ccache') + + current_ccache = os.environ.get('KRB5CCNAME') + os.environ['KRB5CCNAME'] = self.ccache_name + + if principal.find('@') == -1: + principal = '%s@%s' % (principal, api.env.realm) + + try: + kinit_password(principal, password, self.ccache_name) + except RuntimeError as e: + raise ConfigurationError("Kerberos authentication failed: %s" % e) + finally: + if current_ccache: + os.environ['KRB5CCNAME'] = current_ccache + + def _call_ipa(self, command, args, kw): + """ + Call into the IPA API. + + Duplicates are ignored to be idempotent. Other errors are + ignored implitly because they are encapsulated in the result + for some calls. + """ + try: + api.Command[command](args, **kw) + except errors.DuplicateEntry: + pass + except Exception as e: # pylint: disable=broad-except + logger.error("Unhandled exception: %s", e) + + def _add_permissions(self): + self._call_ipa(u'permission_add', u'Modify host password', + {'ipapermright': u'write', + 'type': u'host', + 'attrs': u'userpassword'}) + self._call_ipa(u'permission_add', u'Write host certificate', + {'ipapermright': u'write', + 'type': u'host', + 'attrs': u'usercertificate'}) + self._call_ipa(u'permission_add', u'Modify host userclass', + {'ipapermright': u'write', + 'type': u'host', + 'attrs': u'userclass'}) + + def _add_privileges(self): + self._call_ipa(u'privilege_add', u'Nova Host Management', + {'description': u'Nova Host Management'}) + + self._call_ipa(u'privilege_add_permission', u'Nova Host Management', + {u'permission': [ + u'System: add hosts', + u'System: remove hosts', + u'modify host password', + u'modify host userclass', + u'modify hosts', + u'System: revoke certificate', + u'System: manage host keytab', + u'System: write host certificate', + u'System: retrieve certificates from the ca', + u'System: modify services', + u'System: manage service keytab', + u'System: read dns entries', + u'System: remove dns entries', + u'System: add dns entries', + u'System: update dns entries']}) + + def _add_role(self): + self._call_ipa(u'role_add', u'Nova Host Manager', + {'description': u'Nova Host Manager'}) + self._call_ipa(u'role_add_privilege', u'Nova Host Manager', + {'privilege': u'Nova Host Management'}) + self._call_ipa(u'role_add_member', u'Nova Host Manager', + {u'service': self.service}) + + def _add_service(self): + self._call_ipa(u'service_add', self.service, {'force': True}) + + def _get_keytab(self): + if self.ccache_name: + current_ccache = os.environ.get('KRB5CCNAME') + os.environ['KRB5CCNAME'] = self.ccache_name + + try: + if os.path.exists(self.keytab): + os.unlink(self.keytab) + except OSError as e: + sys.exit('Could not remove %s: %s' % (self.keytab, e)) + + try: + run(['ipa-getkeytab', + '-s', api.env.server, + '-p', self.service, + '-k', self.keytab]) + finally: + if current_ccache: + os.environ['KRB5CCNAME'] = current_ccache + + # s/b already validated + user = pwd.getpwnam(self.user) + + os.chown(self.keytab, user.pw_uid, user.pw_gid) + os.chmod(self.keytab, 0o600) + + def configure_ipa(self): + self._add_service() + self._get_keytab() + self._add_permissions() + self._add_privileges() + self._add_role() + + +def ipa_options(parser): + parser.add_argument('--no-kinit', + help='Assume the user has already done a kinit', + action="store_true", default=False) + parser.add_argument('--user', + help='User that nova services run as', + default='nova') + parser.add_argument('--principal', dest='principal', default='admin', + help='principal to use to setup IPA integration') + parser.add_argument('--password', dest='password', + help='password for the principal') + parser.add_argument('--password-file', dest='passwordfile', + help='path to file containing password for ' + 'the principal') + return parser + + +def validate_options(args): + if args.get('no_kinit', False): + return args + + if not args['principal']: + args['principal'] = user_input("IPA admin user", "admin", + allow_empty=False) + + if args['passwordfile']: + try: + with open(args['passwordfile']) as f: + args['password'] = f.read() + except IOError as e: + raise ConfigurationError('Unable to read password file: %s' + % e) + if not args['password']: + try: + args['password'] = getpass.getpass("Password for %s: " % + args['principal']) + except EOFError: + args['password'] = None + if not args['password']: + raise ConfigurationError('Password must be provided.') + + try: + pwd.getpwnam(args['user']) + except KeyError: + raise ConfigurationError('User: %s not found on the system' % + args['user']) diff --git a/novajoin/errors.py b/novajoin/errors.py new file mode 100644 index 0000000..2713511 --- /dev/null +++ b/novajoin/errors.py @@ -0,0 +1,19 @@ +# Copyright 2016 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +class ConfigurationError(StandardError): + + def __init__(self, message): + StandardError.__init__(self, message) diff --git a/scripts/novajoin-install b/scripts/novajoin-install index a85afcf..be15d27 100755 --- a/scripts/novajoin-install +++ b/scripts/novajoin-install @@ -24,27 +24,23 @@ import sys import time import copy import tempfile -from subprocess import CalledProcessError import getpass -from string import Template -from six.moves import input -from six.moves.configparser import ConfigParser from ipapython.ipautil import run, kinit_password, user_input from ipalib import api from ipalib import errors +from novajoin.errors import ConfigurationError +from novajoin import configure_ipa +from six.moves import input +from six.moves.configparser import ConfigParser +from subprocess import CalledProcessError +from string import Template DATADIR = '/usr/share/novajoin' NOVADIR = '/etc/nova' IPACONF = '/etc/ipa/default.conf' +NOVACONF = '/etc/nova/nova.conf' JOINCONF = '/etc/join/join.conf' -KEYTAB = '/etc/join/krb5.keytab' - - -class ConfigurationError(StandardError): - - def __init__(self, message): - StandardError.__init__(self, message) LOGFILE = '/var/log/novajoin-install.log' @@ -91,7 +87,7 @@ def write_from_template(destfile, template, opts): def install(args): logger.info('Installation initiated') - if not os.path.exists('/etc/ipa/default.conf'): + if not os.path.exists(IPACONF): raise ConfigurationError('Must be enrolled in IPA') try: @@ -105,48 +101,16 @@ def install(args): api.bootstrap(context='novajoin') api.finalize() - ccache_dir = tempfile.mkdtemp(prefix='krbcc') - - ccache_name = os.path.join(ccache_dir, 'ccache') - env = {"PATH": - "/bin:/sbin:/usr/kerberos/bin:/usr/kerberos/sbin:/usr/bin:/usr/sbin" - } - env['KRB5CCNAME'] = os.environ['KRB5CCNAME'] = ccache_name - - principal = args['principal'] - if principal.find('@') == -1: - principal = '%s@%s' % (principal, api.env.realm) + try: + api.Backend.rpcclient.connect() + except errors.CCacheError: + raise ConfigurationError("No Kerberos credentials") try: - kinit_password(principal, args['password'], ccache_name) - except RuntimeError as e: - raise ConfigurationError("Kerberos authentication failed: %s" % e) - - api.Backend.rpcclient.connect() - - try: - result = api.Command.service_add(u'nova/%s@%s' % - (args['hostname'], api.env.realm), - force=True) - except errors.DuplicateEntry: - pass - except Exception as e: - raise ConfigurationError( - 'Failed to add service: %s' % e) - - try: - if os.path.exists(KEYTAB): - os.unlink(KEYTAB) - except OSError as e: - raise ConfigurationError( - 'Could not remove %s: %s' % (KEYTAB, e) - ) - - run(['ipa-getkeytab', - '-s', api.env.server, - '-p', 'nova/%s@%s' % (args['hostname'], api.env.realm), - '-k', KEYTAB], - env=env) + user = pwd.getpwnam(args['user']) + except KeyError: + raise ConfigurationError('User: %s not found on the system' % + args['user']) logger.info('Installing default config files') @@ -169,7 +133,7 @@ def install(args): shutil.copyfile(source, dst) config = ConfigParser() - config.read('/etc/nova/nova.conf') + config.read(NOVACONF) config.set('DEFAULT', 'vendordata_jsonfile_path', '/etc/nova/cloud-config.json') @@ -203,7 +167,7 @@ def install(args): except ConfigParser.NoOptionError: transport_url = None - with open('/etc/nova/nova.conf', 'w') as f: + with open(NOVACONF, 'w') as f: config.write(f) if transport_url: @@ -213,15 +177,6 @@ def install(args): with open(JOINCONF, 'w') as f: join_config.write(f) - try: - user = pwd.getpwnam(args['user']) - except KeyError: - raise ConfigurationError('User: %s not found on the system' % - args['user']) - - os.chown(KEYTAB, user.pw_uid, user.pw_gid) - os.chmod(KEYTAB, 0o600) - logger.info('Importing IPA metadata') (stdout, stderr, returncode) = run( ['glance', @@ -234,52 +189,26 @@ def install(args): logger.error('Adding IPA metadata failed: %s' % stderr) logger.info('Creating IPA permissions') - (stdout, stderr, returncode) = run( - ['/usr/libexec/novajoin-ipa-setup.sh'], raiseonerr=False) - if returncode != 0: - logger.error('Creating IPA permissions failed') + + novajoin = configure_ipa.NovajoinRole(user=args.get('user')) + if not args.get('no_kinit', False): + novajoin.kinit(args.get('principal'), args.get('password')) + novajoin.configure_ipa() def parse_args(): parser = argparse.ArgumentParser(description='Nova join Install Options') parser.add_argument('--hostname', help='Machine\'s fully qualified host name') - parser.add_argument('--user', - help='User that nova services run as', - default='nova') - parser.add_argument('--principal', dest='principal', default='admin', - help='principal to use to setup IPA integration') - parser.add_argument('--password', dest='password', - help='password for the principal') - parser.add_argument('--password-file', dest='passwordfile', - help='path to file containing password for ' - 'the principal') parser.add_argument('--keystone-auth-url', dest='keystone_auth_url', help='Keystone auth URL') parser.add_argument('--nova-password', dest='nova_password', help='Nova service user password') + parser = configure_ipa.ipa_options(parser) args = vars(parser.parse_args()) - if not args['principal']: - args['principal'] = user_input("IPA admin user", "admin", - allow_empty=False) - - if args['passwordfile']: - try: - with open(args['passwordfile']) as f: - args['password'] = f.read() - except IOError as e: - raise ConfigurationError('Unable to read password file: %s' - % e) - if not args['password']: - try: - args['password'] = getpass.getpass("Password for %s: " % - args['principal']) - except EOFError: - args['password'] = None - if not args['password']: - raise ConfigurationError('Password must be provided.') + configure_ipa.validate_options(args) if not args['hostname']: args['hostname'] = socket.getfqdn() diff --git a/scripts/novajoin-ipa-setup b/scripts/novajoin-ipa-setup new file mode 100644 index 0000000..88577f1 --- /dev/null +++ b/scripts/novajoin-ipa-setup @@ -0,0 +1,49 @@ +#!/usr/bin/python +import argparse +import logging +import os +import sys +from ipalib import api, errors +from novajoin import configure_ipa +from novajoin.errors import ConfigurationError + + +IPACONF = '/etc/ipa/default.conf' +LOGFILE = '/var/log/novajoin-install.log' + +logging.basicConfig() +logger = logging.getLogger() + + +if __name__ == '__main__': + if not os.path.exists(IPACONF): + sys.exit('Must be enrolled in IPA') + + api.bootstrap(context='novajoin') + api.finalize() + + try: + parser = argparse.ArgumentParser( + description='Nova join Install Options' + ) + parser = configure_ipa.ipa_options(parser) + args = vars(parser.parse_args()) + configure_ipa.validate_options(args) + except ConfigurationError as e: # pylint: disable=broad-except + logger.info(str(e)) # emit message to console + logger.debug(e, exc_info=1) # add backtrace information to logfile + + logger.info('Installation aborted.') + logger.info('See log file %s for details' % LOGFILE) + sys.exit(1) + + novajoin = configure_ipa.NovajoinRole(user=args.get('user')) + if not args.get('no_kinit', False): + novajoin.kinit(args.get('principal'), args.get('password')) + + try: + api.Backend.rpcclient.connect() + except errors.CCacheError: + sys.exit("No Kerberos credentials") + + novajoin.configure_ipa() diff --git a/scripts/novajoin-ipa-setup.sh b/scripts/novajoin-ipa-setup.sh deleted file mode 100755 index 4ee5ff4..0000000 --- a/scripts/novajoin-ipa-setup.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash - -ipa privilege-add 'Nova Host Management' --desc='Nova Host Management' - -ipa permission-add 'modify host password' --permissions='write' --type='host' --attrs='userpassword' -ipa permission-add 'write host certificate' --permissions='write' --type='host' --attrs='usercertificate' -ipa permission-add 'modify host userclass' --permissions='write' --type='host' --attrs='userclass' - -ipa privilege-add-permission 'Nova Host Management' \ - --permissions='System: add hosts' \ - --permissions='System: remove hosts' \ - --permissions='modify host password' \ - --permissions='modify host userclass' \ - --permissions='modify hosts' \ - --permissions='System: revoke certificate' \ - --permissions='System: manage host keytab' \ - --permissions='System: write host certificate' \ - --permissions='System: retrieve certificates from the ca' \ - --permissions='System: modify services' \ - --permissions='System: manage service keytab' \ - --permissions='System: read dns entries' \ - --permissions='System: remove dns entries' \ - --permissions='System: add dns entries' \ - --permissions='System: update dns entries' - -ipa role-add 'Nova Host Manager' --desc='Nova host management' - -ipa role-add-privilege 'Nova Host Manager' --privilege='Nova Host Management' - -ipa role-add-member 'Nova Host Manager' --services=nova/`hostname`