Source code for tcms_api

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
#   Python API for the Kiwi TCMS test case management system.
#
#   Copyright (c) 2012 Red Hat, Inc. All rights reserved.
#   Author: Petr Splichal <psplicha@redhat.com>
#
#   Copyright (c) 2018,2020-2024 Kiwi TCMS project. All rights reserved.
#   Author: Alexander Todorov <info@kiwitcms.org>
#
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#
#   This library is free software; you can redistribute it and/or
#   modify it under the terms of the GNU Lesser General Public
#   License as published by the Free Software Foundation; either
#   version 2.1 of the License, or (at your option) any later version.
#
#   This library is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#   Lesser General Public License for more details.
#
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

"""
This module provides a dictionary based Python interface for the
Kiwi TCMS test management system. It operates via the XML-RPC protocol.


Installation::

    pip install tcms-api


If you want to use Kerberos then::

    pip install tcms-api[gssapi]

**WARNING:** on Windows you need to install MIT Kerberos and make sure
``C:\\Program Files\\MIT\\Kerberos\\bin`` is included in ``%PATH%`` -
this is usually the case when you install and restart! It must be
a 64bit installation, see
`MIT Kerberos for Windows 4.1 <https://web.mit.edu/kerberos/dist/index.html#kfw-4.1>`_

**WARNING:** on Linux you will need gcc, Python and kerberos devel packages to
build ``gssapi`` because it doesn't provide binary packages via PyPI. Try
``dnf install gcc krb5-devel python3-devel`` (Red Hat/Fedora) or
``apt-get install gcc libkrb5-dev libpython3-dev`` (Debian/Ubuntu).


Minimal config file ``~/.tcms.conf``::

    [tcms]
    url = https://tcms.server/xml-rpc/
    username = your-username
    password = your-password

For Kerberos specify the ``use_kerberos = True`` key without username
and password! Also make sure that your ``/etc/krb5.conf`` contains::

    [libdefaults]
    default_realm = .EXAMPLE.COM

where ``EXAMPLE.COM`` matches the realm in your organization.


.. important::

    The filename ``~/.tcms.conf`` is expanded to something like
    ``/home/tcms-bot/.tcms.conf`` on Linux and
    ``C:\\Users\\tcms-bot\\.tcms.conf`` on Windows, where ``tcms-bot``
    is the username on the local computer.

    It's also possible to provide system-wide config in ``/etc/tcms.conf``
    on Linux and ``C:\\tcms.conf`` on Windows!

    Execute the following Python snippet to find the exact location on your
    system::

        import os
        print(os.path.expanduser('~/.tcms.conf'))

Connect to backend::

    from tcms_api import TCMS

    rpc = TCMS().exec

    for test_case in rpc.TestCase.filter({'pk': 46490}):
        print(test_case)


After tcms-api v13.2 you can pass connection configuration directly as
arguments when initializing the TCMS() class::

    TCMS("https://kiwitcms.example.com/xml-rpc/", "api-bot", "keep-me-secret").exec


.. important::

    For a list of available RPC methods see
    https://kiwitcms.readthedocs.io/en/latest/modules/tcms.rpc.api.html

    Example(s) and API scripts contributed by the Kiwi TCMS community
    can be found at https://github.com/kiwitcms/api-scripts. You are welcome
    to open a pull request with your own examples!

"""
import os
from configparser import ConfigParser
from datetime import datetime, timedelta
from distutils.util import strtobool  # pylint: disable=deprecated-module

from tcms_api.xmlrpc import TCMSXmlrpc, TCMSKerbXmlrpc


class _ConnectionProxy:
    def __init__(self, config):
        self.__connected_since = datetime(2024, 1, 1, 0, 0)
        self.__connection = None
        self.__config = config

    @staticmethod
    def server_url(config):
        """
        Returns the server URL and performs various sanity checks!
        """
        # Make sure the server URL is set
        try:
            config["tcms"]["url"] is not None
        except (KeyError, AttributeError) as err:
            raise RuntimeError(f"No url found in {config}") from err

        return config["tcms"]["url"].replace("json-rpc", "xml-rpc")

    def create_connection(self):
        # try authentication credentials from Python arguments first
        if self.__config["tcms"]["url"]:
            config = self.__config
        else:
            # if not provided then try reading from the filesystem
            path = os.path.expanduser("~/.tcms.conf")

            # Try system settings when the config does not exist in user directory
            if not os.path.exists(path):
                path = "/etc/tcms.conf"
            if not os.path.exists(path):
                path = "c:/tcms.conf"

            if not os.path.exists(path):
                raise RuntimeError(f"Config file '{path}' not found")

            config = ConfigParser()
            config.read(path)

        rpc_implementor = None
        server_url = self.server_url(config)
        if strtobool(config["tcms"].get("use_kerberos", "False")):
            # use Kerberos
            rpc_implementor = TCMSKerbXmlrpc(None, None, server_url)
        else:
            try:
                # use password authentication
                rpc_implementor = TCMSXmlrpc(
                    config["tcms"]["username"],
                    config["tcms"]["password"],
                    server_url,
                )
            except KeyError as err:
                raise RuntimeError(f"username/password required in '{path}'") from err

        self.__connected_since = datetime.now()
        return rpc_implementor.server

    def __getattr__(self, name):
        """
        refresh the connection every 4 minutes to avoid an
        `ssl.SSLEOFError: EOF occurred in violation of protocol` error with Python >= 3.10
        In practice I've discovered that 5 minutes works as well, 6 minutes fails so
        be more cautious and refresh the connection earlier!

        Side note: originally I thought this is related to calling
        context.set_alpn_protocols(['http/1.1']) inside http/client.py, introduced in
        https://github.com/python/cpython/commit/f97406be4c0a02c1501c7ab8bc8ef3850eddb962
        but that doesn't seem to be the case (or is much harder for me to debug)!
        """

        # NOTE: Method only called for attributes which don't exist, iow
        # XML-RPC methods, see
        # https://medium.com/@satishgoda/python-attribute-access-using-getattr-and-getattribute-6401f7425ce6
        if datetime.now() - self.__connected_since > timedelta(minutes=4):
            self.__connection = self.create_connection()
        elif self.__connection is None:
            self.__connection = self.create_connection()

        return self.__connection.__getattr__(name)


[docs] class TCMS: # pylint: disable=too-few-public-methods """ Takes care of initiating the connection to the TCMS server and parses user configuration using a utilities class! """ def __init__(self, url=None, username=None, password=None): self.config = { "tcms": { "url": url, "username": username, "password": password, } } @property def exec(self): """ Property that returns the underlying XML-RPC connection on which you can call various server-side functions. .. important:: Call this property once and assign it to a temporary variable as shown in the examples above. Then use the ``rpc`` variable to access the different RPC methods! Starting with tcms-api v12.9.1 this property is automatically refreshed every 4 minutes to avoid SSL connection timeout errors! """ return _ConnectionProxy(self.config)