diff --git a/configure.ac b/configure.ac index ff4687b400c..5f82fcd284b 100644 --- a/configure.ac +++ b/configure.ac @@ -518,6 +518,9 @@ AS_IF([test x"${PYTHON}" != x""], [AC_PATH_PROG([PYTHON], [$PYTHON])]) dnl Require a minimum Python version AM_PATH_PYTHON([3.6]) +PKG_CHECK_MODULES([PYTHON], [python]) +AC_SUBST([PYTHON_LIBS]) +AC_SUBST([PYTHON_CFLAGS]) AC_PROG_LN_S AC_PROG_MKDIR_P diff --git a/mk/python.mk b/mk/python.mk index 9e7dd173fc5..6f543aeaf71 100644 --- a/mk/python.mk +++ b/mk/python.mk @@ -9,7 +9,7 @@ .PHONY: pylint pylint: $(PYCHECKFILES) - PYTHONPATH=$(abs_top_builddir)/python \ + PYTHONPATH=$(abs_top_builddir)/python:$(abs_top_builddir)/python/pacemaker/.libs \ pylint --rcfile $(top_srcdir)/python/pylintrc $(PYCHECKFILES) # Disabled warnings: @@ -27,5 +27,5 @@ pylint: $(PYCHECKFILES) # Disable docstrings warnings on unit tests. .PHONY: pyflake pyflake: $(PYCHECKFILES) - PYTHONPATH=$(abs_top_builddir)/python \ + PYTHONPATH=$(abs_top_builddir)/python:$(abs_top_builddir)/python/pacemaker/.libs \ flake8 --ignore=W503,E402,E501,F401 --per-file-ignores="tests/*:D100,D101,D102,D104" $(PYCHECKFILES) diff --git a/python/Makefile.am b/python/Makefile.am index 7d2508434fb..aa6976a73fe 100644 --- a/python/Makefile.am +++ b/python/Makefile.am @@ -1,5 +1,5 @@ # -# Copyright 2023-2024 the Pacemaker project contributors +# Copyright 2023-2025 the Pacemaker project contributors # # The version control history for this file may have further details. # @@ -22,4 +22,5 @@ check-local: if [ "x$(top_srcdir)" != "x$(top_builddir)" ]; then \ cp -r $(top_srcdir)/python/* $(abs_top_builddir)/python/; \ fi - PYTHONPATH=$(top_builddir)/python $(PYTHON) -m unittest discover -v -s $(top_builddir)/python/tests + PYTHONPATH=$(top_builddir)/python:$(top_builddir)/python/pacemaker/.libs \ + $(PYTHON) -m unittest discover -v -s $(top_builddir)/python/tests diff --git a/python/pacemaker/Makefile.am b/python/pacemaker/Makefile.am index 27e6d6888fe..8e9e82347dd 100644 --- a/python/pacemaker/Makefile.am +++ b/python/pacemaker/Makefile.am @@ -1,5 +1,5 @@ # -# Copyright 2023-2024 the Pacemaker project contributors +# Copyright 2023-2025 the Pacemaker project contributors # # The version control history for this file may have further details. # @@ -9,9 +9,18 @@ include $(top_srcdir)/mk/common.mk +pyexec_LTLIBRARIES = _pcmksupport.la + +_pcmksupport_la_SOURCES = pcmksupport.c +_pcmksupport_la_CPPFLAGS = $(PYTHON_CFLAGS) +_pcmksupport_la_LDFLAGS = $(AM_LDFLAGS) -module -avoid-version -export-symbols-regex PyInit__pcmksupport +_pcmksupport_la_LIBADD = $(top_builddir)/lib/pacemaker/libpacemaker.la + pkgpython_PYTHON = __init__.py \ _library.py \ - exitstatus.py + exceptions.py \ + exitstatus.py \ + resource.py nodist_pkgpython_PYTHON = buildoptions.py diff --git a/python/pacemaker/__init__.py b/python/pacemaker/__init__.py index e6b1b2a685f..a4afbc90a8c 100644 --- a/python/pacemaker/__init__.py +++ b/python/pacemaker/__init__.py @@ -3,5 +3,7 @@ __copyright__ = "Copyright 2023-2024 the Pacemaker project contributors" __license__ = "GNU Lesser General Public License version 2.1 or later (LGPLv2.1+)" -from pacemaker.buildoptions import BuildOptions -from pacemaker.exitstatus import ExitStatus +from .buildoptions import BuildOptions +from .exitstatus import ExitStatus +from . import exceptions +from . import resource diff --git a/python/pacemaker/exceptions.py b/python/pacemaker/exceptions.py new file mode 100644 index 00000000000..93e71918dd5 --- /dev/null +++ b/python/pacemaker/exceptions.py @@ -0,0 +1,9 @@ +"""A module providing exceptions that can be raised by the Pacemaker module.""" + +__all__ = ["PacemakerError"] +__copyright__ = "Copyright 2025 the Pacemaker project contributors" +__license__ = "GNU Lesser General Public License version 2.1 or later (LGPLv2.1+)" + + +class PacemakerError(Exception): + """Base exception class for all Pacemaker errors.""" diff --git a/python/pacemaker/pcmksupport.c b/python/pacemaker/pcmksupport.c new file mode 100644 index 00000000000..09c6ab2c4fa --- /dev/null +++ b/python/pacemaker/pcmksupport.c @@ -0,0 +1,82 @@ +/* + * Copyright 2025 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include +#include + +#include + +/* This file defines a c-based low level module that wraps libpacemaker + * functions and returns python objects. This is necessary because most + * libpacemaker functions return an xmlNode **, which needs to be coerced + * through the PyCapsule type into something that libxml2's python + * bindings can work with. + */ + +/* Base exception class for any errors in the _pcmksupport module */ +static PyObject *PacemakerError; + +PyMODINIT_FUNC PyInit__pcmksupport(void); + +static PyObject * +py_list_standards(PyObject *self, PyObject *args) +{ + int rc; + xmlNodePtr xml = NULL; + + if (!PyArg_ParseTuple(args, "")) { + return NULL; + } + + rc = pcmk_list_standards(&xml); + if (rc != pcmk_rc_ok) { + PyErr_SetString(PacemakerError, pcmk_rc_str(rc)); + return NULL; + } + + return PyCapsule_New(xml, "xmlNodePtr", NULL); +} + +static PyMethodDef pcmksupportMethods[] = { + { "list_standards", py_list_standards, METH_VARARGS, NULL }, + { NULL, NULL, 0, NULL } +}; + +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_pcmksupport", + NULL, + -1, + pcmksupportMethods, + NULL, + NULL, + NULL, + NULL +}; + +PyMODINIT_FUNC +PyInit__pcmksupport(void) +{ + PyObject *module = PyModule_Create(&moduledef); + + if (module == NULL) { + return NULL; + } + + /* Add the base exception to the module */ + PacemakerError = PyErr_NewException("_pcmksupport.PacemakerError", NULL, NULL); + + /* FIXME: When we can support Python >= 3.10, we can use PyModule_AddObjectRef */ + if (PyModule_AddObject(module, "PacemakerError", PacemakerError) < 0) { + Py_XDECREF(PacemakerError); + return NULL; + } + + return module; +} diff --git a/python/pacemaker/resource.py b/python/pacemaker/resource.py new file mode 100644 index 00000000000..1abf1c25311 --- /dev/null +++ b/python/pacemaker/resource.py @@ -0,0 +1,22 @@ +"""A module for managing cluster resources.""" + +__all__ = ["list_standards"] +__copyright__ = "Copyright 2025 the Pacemaker project contributors" +__license__ = "GNU Lesser General Public License version 2.1 or later (LGPLv2.1+)" + +import libxml2 + +import _pcmksupport +from pacemaker.exceptions import PacemakerError + + +def list_standards(): + """Return a list of supported resource standards.""" + try: + xml = _pcmksupport.list_standards() + except _pcmksupport.PacemakerError as e: + raise PacemakerError(*e.args) from None + + doc = libxml2.xmlDoc(xml) + + return [item.getContent() for item in doc.xpathEval("/pacemaker-result/standards/item")] diff --git a/python/pylintrc b/python/pylintrc index bb453f32a8c..0f9529e331e 100644 --- a/python/pylintrc +++ b/python/pylintrc @@ -41,7 +41,7 @@ unsafe-load-any-extension=no # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code -extension-pkg-allow-list= +extension-pkg-allow-list=_pcmksupport # Minimum supported python version # CHANGED diff --git a/rpm/pacemaker.spec.in b/rpm/pacemaker.spec.in index 9072783aa8b..9fcf19af860 100644 --- a/rpm/pacemaker.spec.in +++ b/rpm/pacemaker.spec.in @@ -248,6 +248,7 @@ Requires: %{python_name}-%{name} = %{version}-%{release} Requires: %{python_path} BuildRequires: %{python_name}-devel BuildRequires: %{python_name}-setuptools +BuildRequires: %{python_name}-libxml2 # Pacemaker requires a minimum libqb functionality Requires: libqb >= 1.0.1 @@ -373,7 +374,6 @@ License: LGPL-2.1-or-later Summary: Python libraries for Pacemaker Requires: %{python_path} Requires: %{pkgname_pcmk_libs} = %{version}-%{release} -BuildArch: noarch %description -n %{python_name}-%{name} Pacemaker is an advanced, scalable High-Availability cluster resource @@ -516,8 +516,9 @@ popd %check make %{_smp_mflags} check -{ cts/cts-scheduler --run load-stopped-loop \ - && cts/cts-cli -V \ +{ PYTHONPATH=python/pacemaker/.libs \ + cts/cts-scheduler --run load-stopped-loop \ + && PYTHONPATH=python/pacemaker/.libs cts/cts-cli -V \ && touch .CHECKED } 2>&1 | sed 's/[fF]ail/faiil/g' # prevent false positives in rpmlint [ -f .CHECKED ] && rm -f -- .CHECKED @@ -747,6 +748,7 @@ exit 0 %doc ChangeLog.md %files -n %{python_name}-%{name} +%{python3_sitearch}/_pcmksupport.so %{python3_sitelib}/pacemaker/ %{python3_sitelib}/pacemaker-*.egg-info %exclude %{python3_sitelib}/pacemaker/_cts/