Extend the novajoin-ipa-setup script to precreate IPA entries

For the case of Triple-O we don't want to pass IPA admin
credentials to the undercloud so instead pre-create the IPA
entries for undercloud and pass in an OTP that can be used to
enroll it using ipa-client-install.

Another feature of --precreate is that it doesn't require the
machine to be enrolled as an IPA client. The required options must
be provided on the command-line.

Change-Id: Ia69b5b4fbc275c04f5e07e9d2ef62e3547725ac8
This commit is contained in:
Rob Crittenden 2016-12-09 22:56:13 +00:00 committed by Ade Lee
parent 57b17b85df
commit 3de3e6b847
3 changed files with 311 additions and 38 deletions

70
man/novajoin-ipa-setup.1 Normal file
View File

@ -0,0 +1,70 @@
.TH "novajoin-ipa-setup" "1" "Dec 9 2016" "novajoin" "novajoin Manual Pages"
.SH "NAME"
novajoin\-ipa\-setup \- Configure the IPA services for novajoin
.SH "SYNOPSIS"
novajoin\-ipa\-setup [\fIOPTION\fR]...
.SH "DESCRIPTION"
Configures the objects in an IPA server to work with novajoin.
The machine does not necessarily have to be configured as an IPA client when the \-\-precreate option is used.
The installer creates the IPA permissions, privileges and role and service and optionally creates the IPA host entry for the novajoin service. It may also retrieve the service keytab.
.SH "OPTIONS"
novajoin IPA Install Options
\fB\-h\fR, \fB\-\-help\fR
show this help message and exit
.TP
\fB\-\-debug\fR
Additional logging output
.TP
\fB\-\-no\-kinit\fR
Assume the user has already done a kinit (principal and password not required)
.TP
\fB\-\-user\fR \fIUSER\fR
User that nova services run as
.TP
\fB\-\-principal\fR \fIPRINCIPAL\fR
Kerberos principal to use to setup IPA integration
.TP
\fB\-\-password\fR \fIPASSWORD\fR
password for the Kerberos principal
.TP
\fB\-\-password\-file\fR \fIPASSWORDFILE\fR
path to file containing password for the Kerberos principal
.TP
\fB\-\-precreate\fB
Pre-create entries for another host rather than for this one
.TP
\fB\-\-server\fB \fISERVER\fR
The IPA server to create entries in
.TP
\fB\-\-realm\fB \fIREALM\fR
The IPA realm
.TP
\fB\-\-domain\fB \fIDOMAIN\fR
The IPA domain
.TP
\fB\-\-hostname\fB \fIHOSTNAME\fR
The FQDN of the hostname that will run the novajoin service
.TP
\fB\-\-otp\-file\fB \fIFILENAME\fR
File to write the One-Time Password to rather than stdout
.SH "PRECREATE VS LOCAL"
There are two options for this installer:
1. Install using the current host as the server to run novajoin.
2. Create entries in IPA for another server. This is handy when the server hasn't been instantiated yet.
For the first the machine executing the installer must be enrolled as an IPA client and the values from /etc/ipa/default.conf are used for specifying domain, realm, etc.
The second case is the \-\-precreate case. For this case the server, realm, domain and hostname must all be passed into the script. This is useful if the target host that will run novajoin hasn't been created yet. The output of this script will the a One-Time Password (OTP) that can be passed onto this server and used to enroll. This is helpful for audomation so admin credentials don't need to be sent.
.SH "EXIT STATUS"
0 if the installation was successful
1 if an error occurred
.SH "SEEALSO"
.BR novajoin\-server(1),
.BR novajoin\-notify(1)

View File

@ -18,18 +18,57 @@ import logging
import os
import pwd
import socket
import string
import sys
import tempfile
from ipalib import api
from ipalib import certstore
from ipalib import errors
from ipalib import x509
from ipapython import certdb
from ipapython import ipaldap
from ipapython.ipautil import CalledProcessError
from ipapython.ipautil import ipa_generate_password
from ipapython.ipautil import kinit_password
from ipapython.ipautil import realm_to_suffix
from ipapython.ipautil import run
from ipapython.ipautil import user_input
from ipapython.ipautil import write_tmp_file
from novajoin.errors import ConfigurationError
import nss.nss as nss
logger = logging.getLogger()
allowed_chars = string.letters + string.digits
KRB5_CONF_TMPL = """
includedir /var/lib/sss/pubconf/krb5.include.d/
[libdefaults]
default_realm = $REALM
dns_lookup_realm = false
dns_lookup_kdc = false
rdns = false
ticket_lifetime = 24h
forwardable = yes
udp_preference_limit = 0
default_ccache_name = KEYRING:persistent:%{uid}
[realms]
$REALM = {
kdc = $MASTER:88
master_kdc = $MASTER:88
admin_server = $MASTER:749
default_domain = $DOMAIN
}
[domain_realm]
.$DOMAIN = $REALM
$DOMAIN = $REALM
"""
class NovajoinRole(object):
"""One-stop shopping for creating the IPA permissions, privilege and role.
@ -38,10 +77,15 @@ class NovajoinRole(object):
already exists.
"""
def __init__(self, keytab='/etc/nova/krb5.keytab', user='nova'):
def __init__(self, keytab='/etc/nova/krb5.keytab', user='nova',
hostname=None):
self.keytab = keytab
self.user = user
self.service = u'nova/%s' % self._get_fqdn()
if not hostname:
self.hostname = self._get_fqdn()
else:
self.hostname = hostname
self.service = u'nova/%s' % self.hostname
self.ccache_name = None
def _get_fqdn(self):
@ -60,7 +104,58 @@ class NovajoinRole(object):
fqdn = ""
return fqdn
def kinit(self, principal, password):
def write_tmp_krb5_conf(self, opts, filename):
options = {'MASTER': opts.server,
'DOMAIN': opts.domain,
'REALM': opts.realm}
template = string.Template(KRB5_CONF_TMPL)
text = template.substitute(options)
with open(filename, 'w+') as f:
f.write(text)
def create_krb5_conf(self, opts):
(krb_fd, krb_name) = tempfile.mkstemp()
os.close(krb_fd)
self.write_tmp_krb5_conf(opts, krb_name)
return krb_name
def _get_ca_certs(self, server, realm):
basedn = realm_to_suffix(realm)
try:
conn = ipaldap.IPAdmin(server, sasl_nocanon=True)
conn.do_sasl_gssapi_bind()
certs = certstore.get_ca_certs(conn, basedn, realm, False)
except Exception as e:
raise ConfigurationError("get_ca_certs_from_ldap() error: %s", e)
certs = [x509.load_certificate(c[0], x509.DER) for c in certs
if c[2] is not False]
return certs
def create_nssdb(self, server, realm):
nss.nss_init_nodb()
nss_db = certdb.NSSDatabase()
ca_certs = self._get_ca_certs(server, realm)
ca_certs = [cert.der_data for cert in ca_certs]
# Add CA certs to a temporary NSS database
try:
pwd_file = write_tmp_file(ipa_generate_password())
nss_db.create_db(pwd_file.name)
for i, cert in enumerate(ca_certs):
nss_db.add_cert(cert, 'CA certificate %d' % (i + 1), 'C,,')
except CalledProcessError:
raise ConfigurationError(
'Failed to add CA to temporary NSS database.')
return nss_db
def kinit(self, principal, password, config=None):
ccache_dir = tempfile.mkdtemp(prefix='krbcc')
self.ccache_name = os.path.join(ccache_dir, 'ccache')
@ -72,13 +167,16 @@ class NovajoinRole(object):
principal = '%s@%s' % (principal, api.env.realm)
try:
kinit_password(principal, password, self.ccache_name)
kinit_password(principal, password, self.ccache_name,
config=config)
except RuntimeError as e:
raise ConfigurationError("Kerberos authentication failed: %s" % e)
finally:
if current_ccache:
os.environ['KRB5CCNAME'] = current_ccache
return ccache_dir
def _call_ipa(self, command, args, kw):
"""Call into the IPA API.
@ -94,6 +192,7 @@ class NovajoinRole(object):
logger.error("Unhandled exception: %s", e)
def _add_permissions(self):
logging.debug('Add permissions')
self._call_ipa(u'permission_add', u'Modify host password',
{'ipapermright': u'write',
'type': u'host',
@ -108,6 +207,7 @@ class NovajoinRole(object):
'attrs': u'userclass'})
def _add_privileges(self):
logging.debug('Add privileges')
self._call_ipa(u'privilege_add', u'Nova Host Management',
{'description': u'Nova Host Management'})
@ -130,6 +230,7 @@ class NovajoinRole(object):
u'System: update dns entries']})
def _add_role(self):
logging.debug('Add role')
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',
@ -137,10 +238,25 @@ class NovajoinRole(object):
self._call_ipa(u'role_add_member', u'Nova Host Manager',
{u'service': self.service})
def _add_host(self, filename):
logging.debug('Add host %s', self.hostname)
otp = ipa_generate_password(allowed_chars)
if filename:
with open(filename, "w") as fd:
fd.write("%s\n" % otp)
else:
return otp
self._call_ipa(u'host_add', unicode(self.hostname),
{'description': u'Undercloud host',
'userpassword': unicode(otp),
'force': True})
def _add_service(self):
logging.debug('Add service %s', self.service)
self._call_ipa(u'service_add', self.service, {'force': True})
def _get_keytab(self):
logging.debug('Getting keytab %s for %s', self.keytab, self.service)
if self.ccache_name:
current_ccache = os.environ.get('KRB5CCNAME')
os.environ['KRB5CCNAME'] = self.ccache_name
@ -166,15 +282,24 @@ class NovajoinRole(object):
os.chown(self.keytab, user.pw_uid, user.pw_gid)
os.chmod(self.keytab, 0o600)
def configure_ipa(self):
def configure_ipa(self, precreate, otp_filename=None):
otp = None
if precreate:
otp = self._add_host(otp_filename)
self._add_service()
self._get_keytab()
if not precreate:
self._get_keytab()
self._add_permissions()
self._add_privileges()
self._add_role()
if otp:
print(otp)
def ipa_options(parser):
parser.add_argument('--debug',
help='Additional logging output',
action="store_true", default=False)
parser.add_argument('--no-kinit',
help='Assume the user has already done a kinit',
action="store_true", default=False)
@ -188,35 +313,62 @@ def ipa_options(parser):
parser.add_argument('--password-file', dest='passwordfile',
help='path to file containing password for '
'the principal')
parser.add_argument('--precreate', default=False,
help='Pre-create the IPA host with an OTP',
action="store_true")
noconfig = parser.add_argument_group('Pre-create options')
noconfig.add_argument('--server', dest='server',
help='IPA server')
noconfig.add_argument('--realm', dest='realm',
help='IPA realm name')
noconfig.add_argument('--domain', dest='domain',
help='IPA domain name')
noconfig.add_argument('--hostname', dest='hostname',
help='Hostname of IPA host to create')
noconfig.add_argument('--otp-file', dest='otp_filename',
help='File to write OTP to instead of stdout')
return parser
def validate_options(args):
if args.get('no_kinit', False):
return args
def validate_options(opts):
if opts.precreate and not os.path.exists('/etc/ipa/default.conf'):
if not opts.hostname:
raise ConfigurationError('hostname is required')
if not args['principal']:
args['principal'] = user_input("IPA admin user", "admin",
allow_empty=False)
if not opts.domain:
raise ConfigurationError('IPA domain is required')
if args['passwordfile']:
if not opts.realm:
raise ConfigurationError('IPA realm is required')
if not opts.server:
raise ConfigurationError('IPA server is required')
if opts.no_kinit:
return
if not opts.principal:
opts.principal = user_input("IPA admin user", "admin",
allow_empty=False)
if opts.passwordfile:
try:
with open(args['passwordfile']) as f:
args['password'] = f.read()
with open(opts.passwordfile) as f:
opts.password = f.read()
except IOError as e:
raise ConfigurationError('Unable to read password file: %s'
% e)
if not args['password']:
if not opts.password:
try:
args['password'] = getpass.getpass("Password for %s: " %
args['principal'])
opts.password = getpass.getpass("Password for %s: " %
opts.principal)
except EOFError:
args['password'] = None
if not args['password']:
opts.password = None
if not opts.password:
raise ConfigurationError('Password must be provided.')
try:
pwd.getpwnam(args['user'])
pwd.getpwnam(opts.user)
except KeyError:
raise ConfigurationError('User: %s not found on the system' %
args['user'])
opts.user)

View File

@ -16,48 +16,99 @@
import argparse
import logging
import os
import shutil
import sys
from ipalib import api, errors
from ipapython.ipa_log_manager import log_mgr
from novajoin import configure_ipa
from novajoin.errors import ConfigurationError
IPACONF = '/etc/ipa/default.conf'
LOGFILE = '/var/log/novajoin-install.log'
logging.basicConfig()
logging.basicConfig(format='%(message)s')
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()
logger.setLevel(logging.INFO)
try:
parser = argparse.ArgumentParser(
description='Nova join Install Options'
description='novajoin Install Options'
)
parser = configure_ipa.ipa_options(parser)
args = vars(parser.parse_args())
configure_ipa.validate_options(args)
opts = parser.parse_args()
configure_ipa.validate_options(opts)
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'))
if not opts.precreate and not os.path.exists('/etc/ipa/default.conf'):
sys.exit('Must be enrolled in IPA or see precreate')
if opts.debug:
logger.setLevel(logging.DEBUG)
args = {'context': 'novajoin'}
if opts.precreate:
if opts.domain:
args['domain'] = opts.domain
if opts.realm:
args['realm'] = opts.realm
if opts.server:
args['xmlrpc_uri'] = u'https://%s/ipa/json' % opts.server
precreate_opts_specified = (opts.precreate and opts.domain
and opts.realm and opts.server)
api.bootstrap(**args)
api.finalize()
# suppress the Forwarding messages from ipa
console = log_mgr.get_handler('console')
console.setLevel(logging.WARN)
novajoin = configure_ipa.NovajoinRole(user=opts.user,
hostname=opts.hostname)
ccache_dir = None
if not opts.no_kinit:
if precreate_opts_specified:
krb5_conf = novajoin.create_krb5_conf(opts)
else:
krb5_conf = None
try:
ccache_dir = novajoin.kinit(opts.principal, opts.password,
krb5_conf)
except ConfigurationError:
if krb5_conf:
os.remove(krb5_conf)
nss_db = None
if precreate_opts_specified:
nss_db = novajoin.create_nssdb(opts.server, opts.realm)
logging.debug('Connect to IPA backend')
try:
api.Backend.rpcclient.connect()
if nss_db:
api.Backend.rpcclient.connect(nss_dir=nss_db.secdir)
else:
api.Backend.rpcclient.connect()
except errors.CCacheError:
if nss_db:
nss_db.close()
sys.exit("No Kerberos credentials")
novajoin.configure_ipa()
novajoin.configure_ipa(opts.precreate, opts.otp_filename)
if nss_db:
nss_db.close()
if krb5_conf:
os.remove(krb5_conf)
if ccache_dir:
shutil.rmtree(ccache_dir)