From 4df347c6127ee2ac97248d37dddc5cc3e276f72a Mon Sep 17 00:00:00 2001 From: Ade Lee Date: Fri, 4 Aug 2017 13:04:54 -0400 Subject: [PATCH] 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. --- novajoin_tempest_plugin/plugin.py | 11 + .../services/ipa/__init__.py | 19 ++ .../services/ipa/ipa_client.py | 198 ++++++++++++++++++ .../tests/scenario/novajoin_manager.py | 43 ++-- 4 files changed, 242 insertions(+), 29 deletions(-) create mode 100644 novajoin_tempest_plugin/services/ipa/__init__.py create mode 100644 novajoin_tempest_plugin/services/ipa/ipa_client.py diff --git a/novajoin_tempest_plugin/plugin.py b/novajoin_tempest_plugin/plugin.py index e5f4828..3e8eff5 100644 --- a/novajoin_tempest_plugin/plugin.py +++ b/novajoin_tempest_plugin/plugin.py @@ -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] diff --git a/novajoin_tempest_plugin/services/ipa/__init__.py b/novajoin_tempest_plugin/services/ipa/__init__.py new file mode 100644 index 0000000..ca5c6bb --- /dev/null +++ b/novajoin_tempest_plugin/services/ipa/__init__.py @@ -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' +] diff --git a/novajoin_tempest_plugin/services/ipa/ipa_client.py b/novajoin_tempest_plugin/services/ipa/ipa_client.py new file mode 100644 index 0000000..0240d0b --- /dev/null +++ b/novajoin_tempest_plugin/services/ipa/ipa_client.py @@ -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) diff --git a/novajoin_tempest_plugin/tests/scenario/novajoin_manager.py b/novajoin_tempest_plugin/tests/scenario/novajoin_manager.py index d613ac1..fe738fa 100644 --- a/novajoin_tempest_plugin/tests/scenario/novajoin_manager.py +++ b/novajoin_tempest_plugin/tests/scenario/novajoin_manager.py @@ -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