Add python bindings via the --enable-python configure flag.

Signed-off-by: Tom Hromatka <tom.hroma...@oracle.com>
---
 .github/actions/setup-libcgroup/action.yml   |   6 +-
 .github/workflows/continuous-integration.yml |   9 +
 configure.ac                                 |  16 ++
 src/Makefile.am                              |   3 +
 src/python/.gitignore                        |   6 +
 src/python/Makefile.am                       |  56 ++++
 src/python/cgroup.pxd                        |  64 +++++
 src/python/libcgroup.pyx                     | 274 +++++++++++++++++++
 src/python/setup.py                          |  47 ++++
 9 files changed, 479 insertions(+), 2 deletions(-)
 create mode 100644 src/python/.gitignore
 create mode 100644 src/python/Makefile.am
 create mode 100644 src/python/cgroup.pxd
 create mode 100644 src/python/libcgroup.pyx
 create mode 100755 src/python/setup.py

diff --git a/.github/actions/setup-libcgroup/action.yml 
b/.github/actions/setup-libcgroup/action.yml
index fae416c8a410..01628b0a3886 100644
--- a/.github/actions/setup-libcgroup/action.yml
+++ b/.github/actions/setup-libcgroup/action.yml
@@ -26,13 +26,15 @@ runs:
   steps:
   - run: sudo apt-get update
     shell: bash
-  - run: sudo apt-get install libpam-dev lcov
+  - run: sudo apt-get install libpam-dev lcov python3-pip
+    shell: bash
+  - run: sudo pip install cython
     shell: bash
   - run: rm -fr tests/
     shell: bash
   - run: ./bootstrap.sh
     shell: bash
-  - run: CFLAGS="$CFLAGS -g -O0 -Werror" ./configure --sysconfdir=/etc 
--localstatedir=/var --enable-code-coverage 
--enable-opaque-hierarchy="name=systemd"
+  - run: CFLAGS="$CFLAGS -g -O0 -Werror" ./configure --sysconfdir=/etc 
--localstatedir=/var --enable-code-coverage 
--enable-opaque-hierarchy="name=systemd" --enable-python
     shell: bash
   - run: make
     shell: bash
diff --git a/.github/workflows/continuous-integration.yml 
b/.github/workflows/continuous-integration.yml
index 14fc04802f86..2ded6e9d3b2a 100644
--- a/.github/workflows/continuous-integration.yml
+++ b/.github/workflows/continuous-integration.yml
@@ -75,6 +75,9 @@ jobs:
       run: |
         # The cgroup v1 runner hosted by Github Actions doesn't allow
         # for exclusive cpusets.  Thus, skip the cpuset automated test
+        pushd src/python/build/lib.*
+        export PYTHONPATH=$PYTHONPATH:$(pwd)
+        popd
         ./tests/ftests/ftests.py -l 10 -L ftests.log
         ./tests/ftests/ftests.py -l 10 -L ftests-nocontainer.log --skip 36 
--no-container
     - name: Archive test logs
@@ -125,6 +128,9 @@ jobs:
       uses: ./.github/actions/setup-libcgroup
     - name: Run functional tests
       run: |
+        pushd src/python/build/lib.*
+        export PYTHONPATH=$PYTHONPATH:$(pwd)
+        popd
         pushd tests/ftests
         make check
         popd
@@ -169,6 +175,9 @@ jobs:
       uses: ./.github/actions/setup-libcgroup
     - name: Run functional tests
       run: |
+        pushd src/python/build/lib.*
+        export PYTHONPATH=$PYTHONPATH:$(pwd)
+        popd
         pushd tests/ftests
         make check
         popd
diff --git a/configure.ac b/configure.ac
index 2d10a527c1d4..84d7e688e4a7 100644
--- a/configure.ac
+++ b/configure.ac
@@ -75,6 +75,21 @@ AC_ARG_ENABLE([daemon],
        [with_daemon=true])
 AM_CONDITIONAL([WITH_DAEMON], [test x$with_daemon = xtrue])
 
+AC_ARG_ENABLE([python],
+       [AS_HELP_STRING([--enable-python],
+       [build the python bindings, requires cython])])
+AS_IF([test "$enable_python" = yes], [
+       # cython version check
+       AS_IF([test "$CYTHON_VER_MAJ" -eq 0 -a "$CYTHON_VER_MIN" -lt 29], [
+               AC_MSG_ERROR([python bindings require cython 0.29 or higher])
+       ])
+       AM_PATH_PYTHON([3])
+])
+AM_CONDITIONAL([ENABLE_PYTHON], [test "$enable_python" = yes])
+AC_DEFINE_UNQUOTED([ENABLE_PYTHON],
+       [$(test "$enable_python" = yes && echo 1 || echo 0)],
+       [Python bindings build flag.])
+
 AC_ARG_ENABLE([initscript-install],
        [AS_HELP_STRING([--enable-initscript-install],[install init scripts 
[default=no]])],
        [
@@ -196,6 +211,7 @@ AC_CONFIG_FILES([Makefile
        src/daemon/Makefile
        src/tools/Makefile
        src/pam/Makefile
+       src/python/Makefile
        scripts/Makefile
        scripts/init.d/cgconfig
        scripts/init.d/cgred
diff --git a/src/Makefile.am b/src/Makefile.am
index 5227a0366072..176cf13c3333 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -3,6 +3,9 @@
 BUILT_SOURCES = parse.c parse.h
 
 SUBDIRS = . daemon pam tools
+if ENABLE_PYTHON
+SUBDIRS += python
+endif
 
 # generate parse.h  from parse.y
 AM_YFLAGS = -d
diff --git a/src/python/.gitignore b/src/python/.gitignore
new file mode 100644
index 000000000000..8862a1395e13
--- /dev/null
+++ b/src/python/.gitignore
@@ -0,0 +1,6 @@
+build/
+Makefile
+Makefile.in
+__init__.py
+*.c
+*.swp
diff --git a/src/python/Makefile.am b/src/python/Makefile.am
new file mode 100644
index 000000000000..5c77add326bf
--- /dev/null
+++ b/src/python/Makefile.am
@@ -0,0 +1,56 @@
+#
+# Libcgroup Python Bindings
+#
+# Copyright (c) 2021 Oracle and/or its affiliates.
+# Author: Tom Hromatka <tom.hroma...@oracle.com>
+#
+
+#
+# This library is free software; you can redistribute it and/or modify it
+# under the terms of version 2.1 of the GNU Lesser General Public License as
+# published by the Free Software Foundation.
+#
+# 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.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library; if not, see <http://www.gnu.org/licenses>.
+#
+@CODE_COVERAGE_RULES@
+
+PY_DISTUTILS = \
+       VERSION_RELEASE="@PACKAGE_VERSION@" \
+       CPPFLAGS="-I\${top_srcdir}/include ${AM_CPPFLAGS} ${CPPFLAGS}" \
+       CFLAGS="$(CODE_COVERAGE_CFLAGS) ${AM_CFLAGS} ${CFLAGS}" \
+       LDFLAGS="$(CODE_COVERAGE_LIBS) ${AM_LDFLAGS} ${LDFLAGS}" \
+       ${PYTHON} ${srcdir}/setup.py
+
+# support silent builds
+PY_BUILD_0 = @echo "  PYTHON  " $@; ${PY_DISTUTILS} -q build
+PY_BUILD_1 = ${PY_DISTUTILS} build
+PY_BUILD_ = ${PY_BUILD_0}
+PY_BUILD = ${PY_BUILD_@AM_V@}
+
+PY_INSTALL = ${PY_DISTUTILS} install
+
+EXTRA_DIST = cgroup.pxd libcgroup.pyx setup.py
+
+all-local: build
+
+build: ../libcgroup.la cgroup.pxd libcgroup.pyx setup.py
+       [ ${srcdir} == ${builddir} ] || cp ${srcdir}/libcgroup.pyx ${builddir}
+       ${PY_BUILD} && touch build
+
+install-exec-local: build
+       ${PY_INSTALL} --install-lib=${DESTDIR}/${pyexecdir} \
+               --record=${DESTDIR}/${pyexecdir}/install_files.txt
+
+uninstall-local:
+       cat ${DESTDIR}/${pyexecdir}/install_files.txt | xargs ${RM} -f
+       ${RM} -f ${DESTDIR}/${pyexecdir}/install_files.txt
+
+clean-local:
+       [ ${srcdir} == ${builddir} ] || ${RM} -f ${builddir}/libcgroup.pyx
+       ${RM} -rf libcgroup.c build
diff --git a/src/python/cgroup.pxd b/src/python/cgroup.pxd
new file mode 100644
index 000000000000..82fa2af91a2d
--- /dev/null
+++ b/src/python/cgroup.pxd
@@ -0,0 +1,64 @@
+#
+# Libcgroup Python Bindings
+#
+# Copyright (c) 2021 Oracle and/or its affiliates.
+# Author: Tom Hromatka <tom.hroma...@oracle.com>
+#
+
+#
+# This library is free software; you can redistribute it and/or modify it
+# under the terms of version 2.1 of the GNU Lesser General Public License as
+# published by the Free Software Foundation.
+#
+# 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.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library; if not, see <http://www.gnu.org/licenses>.
+#
+
+# cython: language_level = 3str
+
+cdef extern from "libcgroup.h":
+    cdef struct cgroup:
+        pass
+
+    cdef struct cgroup_controller:
+        pass
+
+    cdef enum cg_version_t:
+        CGROUP_UNK
+        CGROUP_V1
+        CGROUP_V2
+        CGROUP_DISK
+
+    cdef struct cgroup_library_version:
+        unsigned int major
+        unsigned int minor
+        unsigned int release
+
+    int cgroup_init()
+    const cgroup_library_version * cgroup_version()
+
+    cgroup *cgroup_new_cgroup(const char *name)
+    int cgroup_convert_cgroup(cgroup *out_cg, cg_version_t out_version,
+                              cgroup  *in_cg, cg_version_t in_version)
+    void cgroup_free(cgroup **cg)
+
+    cgroup_controller *cgroup_add_controller(cgroup *cg, const char *name)
+    cgroup_controller *cgroup_get_controller(cgroup *cg, const char *name)
+
+    int cgroup_add_value_string(cgroup_controller *cgc, const char *name,
+                                const char *value)
+    int cgroup_get_value_string(cgroup_controller *cgc, const char *name,
+                                char **value)
+    char *cgroup_get_value_name(cgroup_controller *cgc, int index)
+    int cgroup_get_value_name_count(cgroup_controller *cgc)
+
+    int cgroup_cgxget(cgroup ** cg, cg_version_t version,
+                      bint ignore_unmappable)
+
+    int cgroup_cgxset(const cgroup * const cg, cg_version_t version,
+                      bint ignore_unmappable)
diff --git a/src/python/libcgroup.pyx b/src/python/libcgroup.pyx
new file mode 100644
index 000000000000..cf43a5ad905d
--- /dev/null
+++ b/src/python/libcgroup.pyx
@@ -0,0 +1,274 @@
+#
+# Libcgroup Python Bindings
+#
+# Copyright (c) 2021 Oracle and/or its affiliates.
+# Author: Tom Hromatka <tom.hroma...@oracle.com>
+#
+
+#
+# This library is free software; you can redistribute it and/or modify it
+# under the terms of version 2.1 of the GNU Lesser General Public License as
+# published by the Free Software Foundation.
+#
+# 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.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library; if not, see <http://www.gnu.org/licenses>.
+#
+
+# cython: language_level = 3str
+
+""" Python bindings for the libcgroup library
+"""
+
+__author__ =  'Tom Hromatka <tom.hroma...@oracle.com>'
+__date__ = "25 October 2021"
+
+cimport cgroup
+
+cdef class Version:
+    CGROUP_UNK = cgroup.CGROUP_UNK
+    CGROUP_V1 = cgroup.CGROUP_V1
+    CGROUP_V2 = cgroup.CGROUP_V2
+    CGROUP_DISK = cgroup.CGROUP_DISK
+
+def c_str(string):
+    return bytes(string, "ascii")
+
+def indent(in_str, cnt):
+    leading_indent = cnt * ' '
+    return ''.join(leading_indent + line for line in in_str.splitlines(True))
+
+class Controller:
+    def __init__(self, name):
+        self.name = name
+        # self.settings maps to
+        # struct control_value *values[CG_NV_MAX];
+        self.settings = dict()
+
+    def __str__(self):
+        out_str = "Controller {}\n".format(self.name)
+
+        for setting_key in self.settings:
+            out_str += indent("{} = {}\n".format(setting_key,
+                              self.settings[setting_key]), 4)
+
+        return out_str
+
+cdef class Cgroup:
+    """ Python object representing a libcgroup cgroup """
+    cdef cgroup.cgroup * _cgp
+    cdef public:
+        object name, controllers, version
+
+    def __cinit__(self, name, version):
+        ret = cgroup.cgroup_init()
+        if ret != 0:
+            raise RuntimeError("Failed to initialize libcgroup: 
{}".format(ret))
+
+        self._cgp = cgroup.cgroup_new_cgroup(c_str(name))
+        if self._cgp == NULL:
+            raise RuntimeError("Failed to create cgroup {}".format(name))
+
+    def __init__(self, name, version):
+        """Initialize this cgroup instance
+
+        Arguments:
+        name - Name of this cgroup
+        version - Version of this cgroup
+
+        Note:
+        Does not modify the cgroup sysfs.  Does not read from the cgroup sysfs
+        """
+        self.name = name
+        self.controllers = dict()
+        self.version = version
+
+    def __str__(self):
+        out_str = "Cgroup {}\n".format(self.name)
+        for ctrl_key in self.controllers:
+            out_str += indent(str(self.controllers[ctrl_key]), 4)
+
+        return out_str
+
+    @staticmethod
+    def library_version():
+        cdef const cgroup.cgroup_library_version * version
+
+        version = cgroup.cgroup_version()
+        return [version.major, version.minor, version.release]
+
+    def add_controller(self, ctrl_name):
+        """Add a controller to the Cgroup instance
+
+        Arguments:
+        ctrl_name - name of the controller
+
+        Description:
+        Adds a controller to the Cgroup instance
+
+        Note:
+        Does not modify the cgroup sysfs
+        """
+        cdef cgroup.cgroup_controller * cgcp
+        cdef cgroup.cgroup * cgp
+
+        cgcp = cgroup.cgroup_add_controller(self._cgp,
+                                            c_str(ctrl_name))
+        if cgcp == NULL:
+            raise RuntimeError("Failed to add controller {} to cgroup".format(
+                               ctrl_name))
+
+        self.controllers[ctrl_name] = Controller(ctrl_name)
+
+    def add_setting(self, setting_name, setting_value=None):
+        """Add a setting to the Cgroup/Controller instance
+
+        Arguments:
+        setting_name - name of the cgroup setting, e.g. 'cpu.shares'
+        setting_value (optional) - value
+
+        Description:
+        Adds a setting/value pair to the Cgroup/Controller instance
+
+        Note:
+        Does not modify the cgroup sysfs
+        """
+        cdef cgroup.cgroup_controller *cgcp
+        cdef char * value
+
+        ctrl_name = setting_name.split('.')[0]
+
+        cgcp = cgroup.cgroup_get_controller(self._cgp,
+                                            c_str(ctrl_name))
+        if cgcp == NULL:
+            self.add_controller(ctrl_name)
+
+        if setting_value == None:
+            ret = cgroup.cgroup_add_value_string(cgcp,
+                      c_str(setting_name), NULL)
+        else:
+            ret = cgroup.cgroup_add_value_string(cgcp,
+                      c_str(setting_name), c_str(setting_value))
+        if ret != 0:
+            raise RuntimeError("Failed to add setting {}: {}".format(
+                               setting_name, ret))
+
+    def _pythonize_cgroup(self):
+        """
+        Given a populated self._cgp, populate the equivalent Python fields
+        """
+        cdef char *setting_name
+        cdef char *setting_value
+
+        for ctrlr_key in self.controllers:
+            cgcp = cgroup.cgroup_get_controller(self._cgp,
+                       c_str(self.controllers[ctrlr_key].name))
+            if cgcp == NULL:
+                raise RuntimeError("Failed to get controller {}".format(
+                                   ctrlr_key))
+
+            self.controllers[ctrlr_key] = Controller(ctrlr_key)
+            setting_cnt = cgroup.cgroup_get_value_name_count(cgcp)
+
+            for i in range(0, setting_cnt):
+                setting_name = cgroup.cgroup_get_value_name(cgcp, i)
+
+                ret = cgroup.cgroup_get_value_string(cgcp,
+                          setting_name, &setting_value)
+                if ret != 0:
+                    raise RuntimeError("Failed to get value {}: {}".format(
+                                       setting_name, ret))
+
+                name = setting_name.decode("ascii")
+                value = setting_value.decode("ascii").strip()
+                self.controllers[ctrlr_key].settings[name] = value
+
+    def convert(self, out_version):
+        """Convert this cgroup to another cgroup version
+
+        Arguments:
+        out_version - Version to convert to
+
+        Return:
+        Returns the converted cgroup instance
+
+        Description:
+        Convert this cgroup instance to a cgroup instance of a different
+        cgroup version
+
+        Note:
+        Does not modify the cgroup sysfs.  Does not read from the cgroup sysfs
+        """
+        out_cgp = Cgroup(self.name, out_version)
+        ret = cgroup.cgroup_convert_cgroup(out_cgp._cgp,
+                  out_version, self._cgp, self.version)
+        if ret != 0:
+            raise RuntimeError("Failed to convert cgroup: {}".format(ret))
+
+        for ctrlr_key in self.controllers:
+            out_cgp.controllers[ctrlr_key] = Controller(ctrlr_key)
+
+        out_cgp._pythonize_cgroup()
+
+        return out_cgp
+
+    def cgxget(self, ignore_unmappable=False):
+        """Read the requested settings from the cgroup sysfs
+
+        Arguments:
+        ignore_unmappable - Ignore cgroup settings that can't be converted
+                            from one version to another
+
+        Return:
+        Returns the cgroup instance that represents the settings read from
+        sysfs
+
+        Description:
+        Given this cgroup instance, read the settings/values from the
+        cgroup sysfs.  If the read was successful, the settings are
+        returned in the return value
+
+        Note:
+        Reads from the cgroup sysfs
+        """
+        cdef bint ignore
+
+        if ignore_unmappable:
+            ignore = 1
+        else:
+            ignore = 0
+
+        ret = cgroup.cgroup_cgxget(&self._cgp, self.version, ignore)
+        if ret != 0:
+            raise RuntimeError("cgxget failed: {}".format(ret))
+
+        self._pythonize_cgroup()
+
+    def cgxset(self, ignore_unmappable=False):
+        """Write this cgroup to the cgroup sysfs
+
+        Arguments:
+        ignore_unmappable - Ignore cgroup settings that can't be converted
+                            from one version to another
+
+        Description:
+        Write the settings/values in this cgroup instance to the cgroup sysfs
+
+        Note:
+        Writes to the cgroup sysfs
+        """
+        if ignore_unmappable:
+            ignore = 1
+        else:
+            ignore = 0
+
+        ret = cgroup.cgroup_cgxset(self._cgp, self.version, ignore)
+        if ret != 0:
+            raise RuntimeError("cgxset failed: {}".format(ret))
+
+    def __dealloc__(self):
+        cgroup.cgroup_free(&self._cgp);
diff --git a/src/python/setup.py b/src/python/setup.py
new file mode 100755
index 000000000000..f4b95d4a1a80
--- /dev/null
+++ b/src/python/setup.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+
+#
+# Libcgroup Python Module Build Script
+#
+
+#
+# Copyright (c) 2021 Oracle and/or its affiliates.
+# Author: Tom Hromatka <tom.hroma...@oracle.com>
+#
+
+#
+# This library is free software; you can redistribute it and/or modify it
+# under the terms of version 2.1 of the GNU Lesser General Public License as
+# published by the Free Software Foundation.
+#
+# 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.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library; if not, see <http://www.gnu.org/licenses>.
+#
+
+import os
+
+from distutils.core import setup
+from distutils.extension import Extension
+from Cython.Distutils import build_ext
+
+setup(
+    name = "libcgroup",
+    version = os.environ["VERSION_RELEASE"],
+    description = "Python bindings for libcgroup",
+    url = "https://github.com/libcgroup/libcgroup";,
+    maintainer = "Tom Hromatka",
+    maintainer_email = "tom.hroma...@oracle.com",
+    license = "LGPLv2.1",
+    platforms = "Linux",
+    cmdclass = {'build_ext': build_ext},
+    ext_modules = [
+        Extension("libcgroup", ["libcgroup.pyx"],
+            # unable to handle libtool libraries directly
+            extra_objects=["../.libs/libcgroup.a"])
+    ]
+)
-- 
2.31.1



_______________________________________________
Libcg-devel mailing list
Libcg-devel@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/libcg-devel

Reply via email to