Break IPA configuration into a separate module and installer

When installing via puppet the configuration changes aren't
necessary and in fact can cause problems. All that really needs
to happen is the IPA work to add the permissions, privilege and
role and create the nova service and fetch the keytab.

This is broken out into a separate class that can be called from
either the existing novajoin-install or the new
novajoin-ipa-setup. The bash script equivalent was removed.
This commit is contained in:
Rob Crittenden 2016-09-19 17:39:41 -04:00
parent 7fb07ad480
commit 5c0ad416f4
5 changed files with 301 additions and 126 deletions

208
novajoin/configure_ipa.py Normal file
View File

@ -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'])

19
novajoin/errors.py Normal file
View File

@ -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)

View File

@ -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()

View File

@ -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()

View File

@ -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`