Change to use IPA client instead of cmd line invocations

This will end up using the creds of the nova user - which is
probably what we want to end up using in any case.
This commit is contained in:
Ade Lee 2017-08-04 13:04:54 -04:00
parent 547b1ead85
commit 4df347c612
4 changed files with 242 additions and 29 deletions

View File

@ -41,3 +41,14 @@ class NovajoinTempestPlugin(plugins.TempestPlugin):
def get_opt_lists(self):
return [('service_available', [project_config.service_option]),
(project_config.ipa_group.name, project_config.IpaGroup)]
def get_service_clients(self):
params = {
'name': 'ipa_v4',
'service_version': 'ipa.v4',
'module_path': 'novajoin_tempest_plugin.services.ipa',
'client_names': [
'IPAClient',
],
}
return [params]

View File

@ -0,0 +1,19 @@
# Copyright (c) 2016 Red Hat
#
# 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.
from novajoin_tempest_plugin.services.ipa.ipa_client import IPAClient
__all__ = [
'IPAClient'
]

View File

@ -0,0 +1,198 @@
# Copyright 2017 Red Hat
# All Rights Reserved.
#
# 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.
import os
import time
import uuid
try:
from gssapi.exceptions import GSSError
from ipalib import api
from ipalib import errors
from ipapython.ipautil import kinit_keytab
ipalib_imported = True
except ImportError:
# ipalib/ipapython are not available in PyPy yet, don't make it
# a showstopper for the tests.
ipalib_imported = False
from oslo_config import cfg
from oslo_log import log as logging
from six.moves.configparser import SafeConfigParser
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
class IPABase(object):
def __init__(self, backoff=0):
try:
self.ntries = CONF.connect_retries
except cfg.NoSuchOptError:
self.ntries = 1
if not ipalib_imported:
return
self.ccache = "MEMORY:" + str(uuid.uuid4())
os.environ['KRB5CCNAME'] = self.ccache
if self._ipa_client_configured() and not api.isdone('finalize'):
(hostname, realm) = self.get_host_and_realm()
kinit_keytab(str('nova/%s@%s' % (hostname, realm)),
CONF.keytab, self.ccache)
api.bootstrap(context='novajoin')
api.finalize()
self.batch_args = list()
self.backoff = backoff
def get_host_and_realm(self):
"""Return the hostname and IPA realm name.
IPA 4.4 introduced the requirement that the schema be
fetched when calling finalize(). This is really only used by
the ipa command-line tool but for now it is baked in.
So we have to get a TGT first but need the hostname and
realm. For now directly read the IPA config file which is
in INI format and pull those two values out and return as
a tuple.
"""
config = SafeConfigParser()
config.read('/etc/ipa/default.conf')
hostname = config.get('global', 'host')
realm = config.get('global', 'realm')
return hostname, realm
def __backoff(self):
LOG.debug("Backing off %s seconds", self.backoff)
time.sleep(self.backoff)
if self.backoff < 1024:
self.backoff = self.backoff * 2
def __get_connection(self):
"""Make a connection to IPA or raise an error."""
tries = 0
while (tries <= self.ntries) or (self.backoff > 0):
if self.backoff == 0:
LOG.debug("Attempt %d of %d", tries, self.ntries)
if api.Backend.rpcclient.isconnected():
api.Backend.rpcclient.disconnect()
try:
api.Backend.rpcclient.connect()
# ping to force an actual connection in case there is only one
# IPA master
api.Command[u'ping']()
except (errors.CCacheError,
errors.TicketExpired,
errors.KerberosError) as e:
LOG.debug("kinit again: %s", e)
# pylint: disable=no-member
try:
kinit_keytab(str('nova/%s@%s' %
(api.env.host, api.env.realm)),
CONF.keytab,
self.ccache)
except GSSError as e:
LOG.debug("kinit failed: %s", e)
if tries > 0 and self.backoff:
self.__backoff()
tries += 1
except errors.NetworkError:
tries += 1
if self.backoff:
self.__backoff()
else:
return
def _call_ipa(self, command, *args, **kw):
"""Make an IPA call."""
if not api.Backend.rpcclient.isconnected():
self.__get_connection()
if 'version' not in kw:
kw['version'] = u'2.146' # IPA v4.2.0 for compatibility
while True:
try:
result = api.Command[command](*args, **kw)
LOG.debug(result)
return result
except (errors.CCacheError,
errors.TicketExpired,
errors.KerberosError):
LOG.debug("Refresh authentication")
self.__get_connection()
except errors.NetworkError:
if self.backoff:
self.__backoff()
else:
raise
def _ipa_client_configured(self):
"""Determine if the machine is an enrolled IPA client.
Return boolean indicating whether this machine is enrolled
in IPA. This is a rather weak detection method but better
than nothing.
"""
return os.path.exists('/etc/ipa/default.conf')
class IPAClient(IPABase):
def find_host(self, hostname):
params = [hostname]
return self._call_ipa('host_find', *params)
def show_host(self, hostname):
params = [hostname]
return self._call_ipa('host-show', *params)
def find_service(self, service_principal):
params = [service_principal]
service_args = {}
return self._call_ipa('service_find', *params, **service_args)
def show_service(self, service_principal):
params = [service_principal]
service_args = {}
return self._call_ipa('service_show', *params, **service_args)
def service_managed_by_host(self, service_principal, host):
"""Return True if service is managed by specified host"""
params = [service_principal]
service_args = {}
try:
result = self._call_ipa('service_show', *params, **service_args)
except errors.NotFound:
raise KeyError
serviceresult = result['result']
for candidate in serviceresult.get('managedby_host', []):
if candidate == host:
return True
return False
def host_has_services(self, service_host):
"""Return True if this host manages any services"""
LOG.debug('Checking if host ' + service_host + ' has services')
params = []
service_args = {'man_by_host': service_host}
result = self._call_ipa('service_find', *params, **service_args)
return result['count'] > 0
def show_cert(self, serial_number):
params = [serial_number]
return self._call_ipa('cert_show', *params)

View File

@ -12,13 +12,13 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
from oslo_log import log as logging
from tempest import config
from tempest import clients
from tempest.scenario import manager as mgr
from tempest.lib.common import ssh
CONF = config.CONF
LOG = logging.getLogger(__name__)
@ -30,55 +30,42 @@ class NovajoinScenarioTest(mgr.ScenarioTest):
def setUp(self):
super(NovajoinScenarioTest, self).setUp()
ssh_host = CONF.tripleo.undercloud_hostname
ssh_user = CONF.stress.target_ssh_user
ssh_key = CONF.stress.target_private_key_path
self.ssh_client = ssh.Client(ssh_host, ssh_user, key_filename=ssh_key)
@classmethod
def skip_checks(cls):
super(NovajoinScenarioTest, cls).skip_checks()
cmd = 'source ~/stackrc;openstack service list | grep novajoin'
novajoin_enabled = ssh_client.exec_command(cmd)
if not novajoin_enabled:
if not CONF.service_available.novajoin:
raise cls.skipException("Novajoin is not enabled")
@classmethod
def setup_clients(cls):
super(NovajoinScenarioTest, cls).setup_clients()
# os = getattr(cls, 'os_%s' % cls.credentials[0])
# os_adm = getattr(cls, 'os_%s' % cls.credentials[1])
# set up ipa client
cls.ipa_client = os.ipa_v4.IPAClient
def verify_host_registered_with_ipa(self, host):
# check if specified host is registered with ipa
# basically doing a host-show
cmd = 'ipa host-show {hostname}'.format(hostname=host)
result = self.ssh_client.exec_command(cmd)
if host in result:
result = self.ipa_client.find_host(host)
if result['count'] > 0:
return True
return False
def verify_host_has_keytab(self, host):
# check if specified host entry has a keytab
cmd = 'ipa host-show {hostname} | grep Keytab'.format(hostname=host)
result = self.ssh_client.exec_command(cmd)
if 'True' in result:
result = self.ipa_client.show_host(host)['result']
keytab_present = result['Keytab']
if 'True' in keytab_present:
return True
return False
def verify_service_exists(self, service, host):
# verify service exists for host on ipa server
# needed for the triple-O tests
cmd = 'ipa service-show {servicename}/{hostname}'.format(
service_principal = '{servicename}/{hostname}'.format(
servicename=service, hostname=host
)
result = self.ssh_client.exec_command(cmd)
if service in result:
result = self.ipa_client.find_service(service_principal)
if result['count'] > 0:
return True
return False
@ -110,10 +97,8 @@ class NovajoinScenarioTest(mgr.ScenarioTest):
def verify_cert_revoked(self, serial):
# verify that the given certificate has been revoked
cmd = 'ipa cert-show {serial} |grep Revoked'.format(
serial=serial
)
result = self.ssh_client.exec_command(cmd)
if 'True' in result:
result = self.ipa_client.show_cert(serial)['result']
is_revoked = result['Revoked']
if 'True' in is_revoked:
return True
return False