From 59ff1d324966cd93d40104270ec6618c51e2e1ab Mon Sep 17 00:00:00 2001 From: Chris Lumens Date: Tue, 28 Jan 2025 13:35:14 -0500 Subject: [PATCH 1/4] Build: configure: Add python libraries and cflags. These are needed to build C-based extension modules for python. --- configure.ac | 3 +++ 1 file changed, 3 insertions(+) diff --git a/configure.ac b/configure.ac index 7e5ec6ee668..380ad667a28 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 From 805fb092d38c94d3dfe5a6a19bc9b3bb28aae5f6 Mon Sep 17 00:00:00 2001 From: Chris Lumens Date: Tue, 28 Jan 2025 13:45:27 -0500 Subject: [PATCH 2/4] Feature: python: Add a low level C-based python module wrapper. All the libpacemaker functions return an xmlNode **, which we need to convert into some sort of native python type if we are to provide a python module for people to use. There's various ways we could go about doing this, but what we probably want to be able to do is convert the C-based xmlNode ** type into something that libxml2's python module can work with. Then, we can write more of the python module in python itself instead of having to work with libxml2 in C. The way to do this is to write a small C-based wrapper function for each libpacemaker function we want to expose in the python module, using the PyCapsule type. These wrapper functions are likely to be pretty formulaic - we could probably autogenerate them from a script if we wanted. This introduces a single function for demonstration purposes, plus the rest of the boilerplate required to construct a python module in C. --- python/pacemaker/Makefile.am | 9 +++- python/pacemaker/pcmksupport.c | 82 ++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 python/pacemaker/pcmksupport.c diff --git a/python/pacemaker/Makefile.am b/python/pacemaker/Makefile.am index 27e6d6888fe..28e47cf2b90 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,6 +9,13 @@ 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 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; +} From 50a4f6e294ba64eb1892859b23f93e970f43cb79 Mon Sep 17 00:00:00 2001 From: Chris Lumens Date: Tue, 28 Jan 2025 14:08:01 -0500 Subject: [PATCH 3/4] Feature: python: Add a python module wrapping libpacemaker. This is a public, native python API that wraps libpacemaker. It aims to provide a python-based way of interacting with pacemaker, which means we need to be careful to use exceptions where appropriate, return python types, and in general write things in a pythonic style. --- mk/python.mk | 4 ++-- python/pacemaker/Makefile.am | 4 +++- python/pacemaker/__init__.py | 6 ++++-- python/pacemaker/exceptions.py | 9 +++++++++ python/pacemaker/resource.py | 22 ++++++++++++++++++++++ python/pylintrc | 2 +- 6 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 python/pacemaker/exceptions.py create mode 100644 python/pacemaker/resource.py 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/pacemaker/Makefile.am b/python/pacemaker/Makefile.am index 28e47cf2b90..8e9e82347dd 100644 --- a/python/pacemaker/Makefile.am +++ b/python/pacemaker/Makefile.am @@ -18,7 +18,9 @@ _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/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 From 9d692bd8199bdb7c2672118da9fd00697d9d5ff7 Mon Sep 17 00:00:00 2001 From: Chris Lumens Date: Tue, 28 Jan 2025 14:21:31 -0500 Subject: [PATCH 4/4] Build: Update build process for new python module. The presence of the new arch-specific compiled python module requires various build changes: * The python3-pacemaker package goes from noarch to arch-specific. * python3-libxml2 is now required as part of the build process to run tests. * PYTHONPATH needs to be updated in various places to run tests. --- python/Makefile.am | 5 +++-- rpm/pacemaker.spec.in | 8 +++++--- 2 files changed, 8 insertions(+), 5 deletions(-) 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/rpm/pacemaker.spec.in b/rpm/pacemaker.spec.in index 4998c1aed41..c64cf1243d1 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 \ +{ PYTHONPATH=python/pacemaker/.libs \ + cts/cts-scheduler --run load-stopped-loop \ + && PYTHONPATH=python/pacemaker/.libs cts/cts-cli \ && 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/