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:
parent
57b17b85df
commit
3de3e6b847
|
@ -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)
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue