Source code for octavia_tempest_plugin.common.decorators
# Copyright 2020 Red Hat, Inc. 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.
from functools import wraps
import testtools
from oslo_utils import excutils
from tempest import config
from tempest.lib import exceptions
CONF = config.CONF
[docs]
def skip_if_not_implemented(f):
    """A decorator to raise a skip exception for not implemented features.
    This decorator raises a skipException if the method raises a
    NotImplemented exception. If "skip_if_not_implemented=False"
    argument was passed to the method, the NotImplemented exception will
    be raised.
    @param skip_if_not_implemented: If True (default), raise skipException.
    """
    @wraps(f)
    def wrapper(*func_args, **func_kwargs):
        skip = func_kwargs.pop('skip_if_not_implemented', True)
        if CONF.loadbalancer_feature_enabled.not_implemented_is_error:
            skip = False
        try:
            return f(*func_args, **func_kwargs)
        except exceptions.NotImplemented as e:
            with excutils.save_and_reraise_exception():
                if not skip:
                    raise
                message = ("The configured provider driver '{driver}' "
                           "does not support a feature required for this "
                           "test.".format(
                               driver=CONF.load_balancer.provider))
                if hasattr(e, 'resp_body'):
                    message = e.resp_body.get('faultstring', message)
                raise testtools.TestCase.skipException(message)
    return wrapper 
[docs]
def retry_on_port_in_use(start_port, max_retries=3):
    """Decorator to retry a test function if the specified port is in use.
    This handles cases where a test fails due to a port conflict, typically
    caused by another service binding the same port on the host. The decorator
    catches '[Errno 98] Address already in use' errors and retries the test
    using incrementally higher port numbers.
    The decorated function must accept `port` as its first parameter.
    :param start_port: Initial port to attempt.
    :param max_retries: Number of retries with incremented port values.
    """
    def decorator(func):
        @wraps(func)
        def wrapper(self, *args, **kwargs):
            port = start_port
            last_exception = None
            for _ in range(max_retries):
                try:
                    return func(self, port, *args, **kwargs)
                except exceptions.NotImplemented as e:
                    message = (
                        "The configured provider driver '{driver}' does not "
                        "support a feature required for this test.".format(
                            driver=CONF.load_balancer.provider
                        )
                    )
                    if hasattr(e, 'resp_body'):
                        message = e.resp_body.get('faultstring', message)
                    raise testtools.TestCase.skipException(message)
                except Exception as e:
                    if "Address already in use" in str(e):
                        last_exception = e
                        port += 1
                    else:
                        raise
            raise Exception(f"All port attempts failed after {max_retries} "
                            f"retries. Last error: {last_exception}")
        return wrapper
    return decorator