sysrepo-python-plugind 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sysrepo_python_plugind-1.0.0/LICENSE +28 -0
- sysrepo_python_plugind-1.0.0/PKG-INFO +200 -0
- sysrepo_python_plugind-1.0.0/README.rst +186 -0
- sysrepo_python_plugind-1.0.0/pyproject.toml +32 -0
- sysrepo_python_plugind-1.0.0/setup.cfg +4 -0
- sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind/__init__.py +15 -0
- sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind/__main__.py +8 -0
- sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind/cli.py +158 -0
- sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind/daemon.py +306 -0
- sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind/install_yang.py +82 -0
- sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind/pid.py +92 -0
- sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind/plugin.py +56 -0
- sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind/sort.py +91 -0
- sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind/yang/sysrepo-python-plugind.yang +54 -0
- sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind.egg-info/PKG-INFO +200 -0
- sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind.egg-info/SOURCES.txt +21 -0
- sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind.egg-info/dependency_links.txt +1 -0
- sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind.egg-info/entry_points.txt +3 -0
- sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind.egg-info/requires.txt +4 -0
- sysrepo_python_plugind-1.0.0/src/sysrepo_python_plugind.egg-info/top_level.txt +1 -0
- sysrepo_python_plugind-1.0.0/tests/test_daemon.py +209 -0
- sysrepo_python_plugind-1.0.0/tests/test_pid.py +79 -0
- sysrepo_python_plugind-1.0.0/tests/test_sort.py +88 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 EntryPoint Communications, LLC
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice,
|
|
9
|
+
this list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its contributors
|
|
16
|
+
may be used to endorse or promote products derived from this software
|
|
17
|
+
without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sysrepo-python-plugind
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python plugin daemon for sysrepo (replacement for sysrepo-plugind)
|
|
5
|
+
License-Expression: BSD-3-Clause
|
|
6
|
+
Project-URL: Repository, https://github.com/entrypoint-communications/sysrepo-python-plugind
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/x-rst
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: sysrepo~=2.0
|
|
11
|
+
Provides-Extra: test
|
|
12
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
sysrepo-python-plugind
|
|
16
|
+
======================
|
|
17
|
+
|
|
18
|
+
A Python reimplementation of ``sysrepo-plugind`` — the sysrepo plugin daemon.
|
|
19
|
+
|
|
20
|
+
The C daemon loads compiled ``.so`` plugins via ``dlopen``. This daemon loads
|
|
21
|
+
**Python plugins** discovered via entry points (``importlib.metadata``), giving
|
|
22
|
+
plugin authors a pure-Python development experience against the existing
|
|
23
|
+
`sysrepo-python`_ CFFI bindings.
|
|
24
|
+
|
|
25
|
+
The two daemons can run **simultaneously**: the Python daemon manages its own
|
|
26
|
+
YANG subtree (``python-plugin-order``, ``python-loaded-plugins``) and never
|
|
27
|
+
touches the C daemon's nodes.
|
|
28
|
+
|
|
29
|
+
.. _sysrepo-python: https://github.com/sysrepo/sysrepo-python
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
Requirements
|
|
33
|
+
------------
|
|
34
|
+
|
|
35
|
+
- Python ≥ 3.10
|
|
36
|
+
- sysrepo-python_ (and the underlying ``libsysrepo.so``)
|
|
37
|
+
- sysrepo core 4.2.10 (ships ``sysrepo-plugind@2022-08-26.yang``)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
Installation
|
|
41
|
+
------------
|
|
42
|
+
|
|
43
|
+
.. code-block:: console
|
|
44
|
+
|
|
45
|
+
pip install sysrepo-python-plugind
|
|
46
|
+
|
|
47
|
+
Then register the YANG augmentation module with sysrepo (requires write
|
|
48
|
+
access to the sysrepo repository, typically run as root):
|
|
49
|
+
|
|
50
|
+
.. code-block:: console
|
|
51
|
+
|
|
52
|
+
sysrepo-python-plugind-setup
|
|
53
|
+
|
|
54
|
+
This is idempotent — safe to run on every deployment or system boot. Pass
|
|
55
|
+
``--force`` to upgrade the module after a package update.
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
Writing a plugin
|
|
59
|
+
----------------
|
|
60
|
+
|
|
61
|
+
Subclass ``SysrepoPlugin`` and implement ``init()``. Register the class via
|
|
62
|
+
an entry point in your package's ``pyproject.toml``:
|
|
63
|
+
|
|
64
|
+
.. code-block:: toml
|
|
65
|
+
|
|
66
|
+
[project.entry-points."sysrepo_python.plugins"]
|
|
67
|
+
my-plugin = "my_package.plugin:MyPlugin"
|
|
68
|
+
|
|
69
|
+
Installing your package makes the plugin visible to the daemon immediately —
|
|
70
|
+
no copying of files or environment variables required.
|
|
71
|
+
|
|
72
|
+
Minimal example:
|
|
73
|
+
|
|
74
|
+
.. code-block:: python
|
|
75
|
+
|
|
76
|
+
import logging
|
|
77
|
+
import sysrepo
|
|
78
|
+
from sysrepo_python_plugind import SysrepoPlugin
|
|
79
|
+
|
|
80
|
+
LOG = logging.getLogger(__name__)
|
|
81
|
+
|
|
82
|
+
class MyPlugin(SysrepoPlugin):
|
|
83
|
+
|
|
84
|
+
def init(self, session: sysrepo.SysrepoSession) -> None:
|
|
85
|
+
self._counter = 0
|
|
86
|
+
session.subscribe_module_change(
|
|
87
|
+
"my-module", None, self._on_change, done_only=True
|
|
88
|
+
)
|
|
89
|
+
session.subscribe_oper_data_request(
|
|
90
|
+
"my-module", "/my-module:stats", self._on_oper
|
|
91
|
+
)
|
|
92
|
+
LOG.info("MyPlugin ready")
|
|
93
|
+
|
|
94
|
+
def cleanup(self, session: sysrepo.SysrepoSession) -> None:
|
|
95
|
+
LOG.info("MyPlugin stopping (counter=%d)", self._counter)
|
|
96
|
+
|
|
97
|
+
def _on_change(self, event, req_id, changes, priv):
|
|
98
|
+
self._counter += 1
|
|
99
|
+
|
|
100
|
+
def _on_oper(self, xpath, priv):
|
|
101
|
+
return {"my-module": {"stats": {"change-count": self._counter}}}
|
|
102
|
+
|
|
103
|
+
State that the C daemon stored in ``void *private_data`` is stored as instance
|
|
104
|
+
attributes (``self._counter`` above).
|
|
105
|
+
|
|
106
|
+
Async callbacks
|
|
107
|
+
~~~~~~~~~~~~~~~
|
|
108
|
+
|
|
109
|
+
The daemon starts an asyncio event loop in a background thread before calling
|
|
110
|
+
any ``init()``, so plugins can use ``asyncio_register=True`` subscriptions
|
|
111
|
+
without managing a loop themselves:
|
|
112
|
+
|
|
113
|
+
.. code-block:: python
|
|
114
|
+
|
|
115
|
+
class AsyncMyPlugin(SysrepoPlugin):
|
|
116
|
+
|
|
117
|
+
def init(self, session: sysrepo.SysrepoSession) -> None:
|
|
118
|
+
session.subscribe_module_change(
|
|
119
|
+
"my-module", None, self._on_change,
|
|
120
|
+
done_only=True, asyncio_register=True,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
async def _on_change(self, event, req_id, changes, priv):
|
|
124
|
+
...
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
Running the daemon
|
|
128
|
+
------------------
|
|
129
|
+
|
|
130
|
+
.. code-block:: console
|
|
131
|
+
|
|
132
|
+
# Foreground, INFO-level logging to stderr:
|
|
133
|
+
sysrepo-python-plugind -d -v 2
|
|
134
|
+
|
|
135
|
+
# Background (daemonize), write PID file:
|
|
136
|
+
sysrepo-python-plugind -p /run/sysrepo-python-plugind.pid
|
|
137
|
+
|
|
138
|
+
CLI flags mirror those of the C ``sysrepo-plugind``:
|
|
139
|
+
|
|
140
|
+
============ ===================================================================
|
|
141
|
+
``-d`` Stay in foreground; log to stderr instead of syslog
|
|
142
|
+
``-v LEVEL`` Verbosity: 0=ERROR 1=WARNING 2=INFO 3=DEBUG (default: 2)
|
|
143
|
+
``-p PATH`` Write PID file at PATH
|
|
144
|
+
``-f`` Exit if any plugin fails to initialise (default: skip and continue)
|
|
145
|
+
============ ===================================================================
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
Plugin ordering
|
|
149
|
+
---------------
|
|
150
|
+
|
|
151
|
+
By default plugins are initialised in entry-point discovery order. To
|
|
152
|
+
override this, set the ``python-plugin-order`` leaf-list in sysrepo's running
|
|
153
|
+
datastore:
|
|
154
|
+
|
|
155
|
+
.. code-block:: console
|
|
156
|
+
|
|
157
|
+
sysrepocfg -d running --edit
|
|
158
|
+
|
|
159
|
+
Add under ``/sysrepo-plugind:sysrepo-plugind``:
|
|
160
|
+
|
|
161
|
+
.. code-block:: xml
|
|
162
|
+
|
|
163
|
+
<python-plugin-order xmlns="urn:sysrepo:python-plugind">
|
|
164
|
+
<plugin>my-plugin</plugin>
|
|
165
|
+
<plugin>other-plugin</plugin>
|
|
166
|
+
</python-plugin-order>
|
|
167
|
+
|
|
168
|
+
Plugins absent from the list are appended after the ordered set, preserving
|
|
169
|
+
their discovery order. A trailing file extension in a configured name (e.g.
|
|
170
|
+
``my-plugin.so``) is stripped before comparison, so operator configs written
|
|
171
|
+
for the C daemon can be reused unchanged.
|
|
172
|
+
|
|
173
|
+
The list of successfully initialised plugins is published to the operational
|
|
174
|
+
datastore at ``/sysrepo-plugind:sysrepo-plugind/sysrepo-python-plugind:python-loaded-plugins/plugin``.
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
Systemd deployment
|
|
178
|
+
------------------
|
|
179
|
+
|
|
180
|
+
A service file is provided in the ``systemd/`` directory of the source
|
|
181
|
+
distribution:
|
|
182
|
+
|
|
183
|
+
.. code-block:: console
|
|
184
|
+
|
|
185
|
+
cp systemd/sysrepo-python-plugind.service /etc/systemd/system/
|
|
186
|
+
systemctl daemon-reload
|
|
187
|
+
systemctl enable --now sysrepo-python-plugind
|
|
188
|
+
|
|
189
|
+
The service runs ``sysrepo-python-plugind-setup`` as ``ExecStartPre=``,
|
|
190
|
+
ensuring the YANG module is present before the daemon starts.
|
|
191
|
+
|
|
192
|
+
If the daemon binary is not at ``/usr/bin/sysrepo-python-plugind`` (e.g. when
|
|
193
|
+
installed into a virtual environment), update the ``ExecStartPre=`` and
|
|
194
|
+
``ExecStart=`` paths in the service file accordingly.
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
License
|
|
198
|
+
-------
|
|
199
|
+
|
|
200
|
+
BSD-3-Clause — same as sysrepo-python.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
sysrepo-python-plugind
|
|
2
|
+
======================
|
|
3
|
+
|
|
4
|
+
A Python reimplementation of ``sysrepo-plugind`` — the sysrepo plugin daemon.
|
|
5
|
+
|
|
6
|
+
The C daemon loads compiled ``.so`` plugins via ``dlopen``. This daemon loads
|
|
7
|
+
**Python plugins** discovered via entry points (``importlib.metadata``), giving
|
|
8
|
+
plugin authors a pure-Python development experience against the existing
|
|
9
|
+
`sysrepo-python`_ CFFI bindings.
|
|
10
|
+
|
|
11
|
+
The two daemons can run **simultaneously**: the Python daemon manages its own
|
|
12
|
+
YANG subtree (``python-plugin-order``, ``python-loaded-plugins``) and never
|
|
13
|
+
touches the C daemon's nodes.
|
|
14
|
+
|
|
15
|
+
.. _sysrepo-python: https://github.com/sysrepo/sysrepo-python
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
Requirements
|
|
19
|
+
------------
|
|
20
|
+
|
|
21
|
+
- Python ≥ 3.10
|
|
22
|
+
- sysrepo-python_ (and the underlying ``libsysrepo.so``)
|
|
23
|
+
- sysrepo core 4.2.10 (ships ``sysrepo-plugind@2022-08-26.yang``)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
Installation
|
|
27
|
+
------------
|
|
28
|
+
|
|
29
|
+
.. code-block:: console
|
|
30
|
+
|
|
31
|
+
pip install sysrepo-python-plugind
|
|
32
|
+
|
|
33
|
+
Then register the YANG augmentation module with sysrepo (requires write
|
|
34
|
+
access to the sysrepo repository, typically run as root):
|
|
35
|
+
|
|
36
|
+
.. code-block:: console
|
|
37
|
+
|
|
38
|
+
sysrepo-python-plugind-setup
|
|
39
|
+
|
|
40
|
+
This is idempotent — safe to run on every deployment or system boot. Pass
|
|
41
|
+
``--force`` to upgrade the module after a package update.
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
Writing a plugin
|
|
45
|
+
----------------
|
|
46
|
+
|
|
47
|
+
Subclass ``SysrepoPlugin`` and implement ``init()``. Register the class via
|
|
48
|
+
an entry point in your package's ``pyproject.toml``:
|
|
49
|
+
|
|
50
|
+
.. code-block:: toml
|
|
51
|
+
|
|
52
|
+
[project.entry-points."sysrepo_python.plugins"]
|
|
53
|
+
my-plugin = "my_package.plugin:MyPlugin"
|
|
54
|
+
|
|
55
|
+
Installing your package makes the plugin visible to the daemon immediately —
|
|
56
|
+
no copying of files or environment variables required.
|
|
57
|
+
|
|
58
|
+
Minimal example:
|
|
59
|
+
|
|
60
|
+
.. code-block:: python
|
|
61
|
+
|
|
62
|
+
import logging
|
|
63
|
+
import sysrepo
|
|
64
|
+
from sysrepo_python_plugind import SysrepoPlugin
|
|
65
|
+
|
|
66
|
+
LOG = logging.getLogger(__name__)
|
|
67
|
+
|
|
68
|
+
class MyPlugin(SysrepoPlugin):
|
|
69
|
+
|
|
70
|
+
def init(self, session: sysrepo.SysrepoSession) -> None:
|
|
71
|
+
self._counter = 0
|
|
72
|
+
session.subscribe_module_change(
|
|
73
|
+
"my-module", None, self._on_change, done_only=True
|
|
74
|
+
)
|
|
75
|
+
session.subscribe_oper_data_request(
|
|
76
|
+
"my-module", "/my-module:stats", self._on_oper
|
|
77
|
+
)
|
|
78
|
+
LOG.info("MyPlugin ready")
|
|
79
|
+
|
|
80
|
+
def cleanup(self, session: sysrepo.SysrepoSession) -> None:
|
|
81
|
+
LOG.info("MyPlugin stopping (counter=%d)", self._counter)
|
|
82
|
+
|
|
83
|
+
def _on_change(self, event, req_id, changes, priv):
|
|
84
|
+
self._counter += 1
|
|
85
|
+
|
|
86
|
+
def _on_oper(self, xpath, priv):
|
|
87
|
+
return {"my-module": {"stats": {"change-count": self._counter}}}
|
|
88
|
+
|
|
89
|
+
State that the C daemon stored in ``void *private_data`` is stored as instance
|
|
90
|
+
attributes (``self._counter`` above).
|
|
91
|
+
|
|
92
|
+
Async callbacks
|
|
93
|
+
~~~~~~~~~~~~~~~
|
|
94
|
+
|
|
95
|
+
The daemon starts an asyncio event loop in a background thread before calling
|
|
96
|
+
any ``init()``, so plugins can use ``asyncio_register=True`` subscriptions
|
|
97
|
+
without managing a loop themselves:
|
|
98
|
+
|
|
99
|
+
.. code-block:: python
|
|
100
|
+
|
|
101
|
+
class AsyncMyPlugin(SysrepoPlugin):
|
|
102
|
+
|
|
103
|
+
def init(self, session: sysrepo.SysrepoSession) -> None:
|
|
104
|
+
session.subscribe_module_change(
|
|
105
|
+
"my-module", None, self._on_change,
|
|
106
|
+
done_only=True, asyncio_register=True,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
async def _on_change(self, event, req_id, changes, priv):
|
|
110
|
+
...
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
Running the daemon
|
|
114
|
+
------------------
|
|
115
|
+
|
|
116
|
+
.. code-block:: console
|
|
117
|
+
|
|
118
|
+
# Foreground, INFO-level logging to stderr:
|
|
119
|
+
sysrepo-python-plugind -d -v 2
|
|
120
|
+
|
|
121
|
+
# Background (daemonize), write PID file:
|
|
122
|
+
sysrepo-python-plugind -p /run/sysrepo-python-plugind.pid
|
|
123
|
+
|
|
124
|
+
CLI flags mirror those of the C ``sysrepo-plugind``:
|
|
125
|
+
|
|
126
|
+
============ ===================================================================
|
|
127
|
+
``-d`` Stay in foreground; log to stderr instead of syslog
|
|
128
|
+
``-v LEVEL`` Verbosity: 0=ERROR 1=WARNING 2=INFO 3=DEBUG (default: 2)
|
|
129
|
+
``-p PATH`` Write PID file at PATH
|
|
130
|
+
``-f`` Exit if any plugin fails to initialise (default: skip and continue)
|
|
131
|
+
============ ===================================================================
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
Plugin ordering
|
|
135
|
+
---------------
|
|
136
|
+
|
|
137
|
+
By default plugins are initialised in entry-point discovery order. To
|
|
138
|
+
override this, set the ``python-plugin-order`` leaf-list in sysrepo's running
|
|
139
|
+
datastore:
|
|
140
|
+
|
|
141
|
+
.. code-block:: console
|
|
142
|
+
|
|
143
|
+
sysrepocfg -d running --edit
|
|
144
|
+
|
|
145
|
+
Add under ``/sysrepo-plugind:sysrepo-plugind``:
|
|
146
|
+
|
|
147
|
+
.. code-block:: xml
|
|
148
|
+
|
|
149
|
+
<python-plugin-order xmlns="urn:sysrepo:python-plugind">
|
|
150
|
+
<plugin>my-plugin</plugin>
|
|
151
|
+
<plugin>other-plugin</plugin>
|
|
152
|
+
</python-plugin-order>
|
|
153
|
+
|
|
154
|
+
Plugins absent from the list are appended after the ordered set, preserving
|
|
155
|
+
their discovery order. A trailing file extension in a configured name (e.g.
|
|
156
|
+
``my-plugin.so``) is stripped before comparison, so operator configs written
|
|
157
|
+
for the C daemon can be reused unchanged.
|
|
158
|
+
|
|
159
|
+
The list of successfully initialised plugins is published to the operational
|
|
160
|
+
datastore at ``/sysrepo-plugind:sysrepo-plugind/sysrepo-python-plugind:python-loaded-plugins/plugin``.
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
Systemd deployment
|
|
164
|
+
------------------
|
|
165
|
+
|
|
166
|
+
A service file is provided in the ``systemd/`` directory of the source
|
|
167
|
+
distribution:
|
|
168
|
+
|
|
169
|
+
.. code-block:: console
|
|
170
|
+
|
|
171
|
+
cp systemd/sysrepo-python-plugind.service /etc/systemd/system/
|
|
172
|
+
systemctl daemon-reload
|
|
173
|
+
systemctl enable --now sysrepo-python-plugind
|
|
174
|
+
|
|
175
|
+
The service runs ``sysrepo-python-plugind-setup`` as ``ExecStartPre=``,
|
|
176
|
+
ensuring the YANG module is present before the daemon starts.
|
|
177
|
+
|
|
178
|
+
If the daemon binary is not at ``/usr/bin/sysrepo-python-plugind`` (e.g. when
|
|
179
|
+
installed into a virtual environment), update the ``ExecStartPre=`` and
|
|
180
|
+
``ExecStart=`` paths in the service file accordingly.
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
License
|
|
184
|
+
-------
|
|
185
|
+
|
|
186
|
+
BSD-3-Clause — same as sysrepo-python.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sysrepo-python-plugind"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Python plugin daemon for sysrepo (replacement for sysrepo-plugind)"
|
|
9
|
+
readme = "README.rst"
|
|
10
|
+
license = "BSD-3-Clause"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = ["sysrepo~=2.0"]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
sysrepo-python-plugind = "sysrepo_python_plugind:main"
|
|
16
|
+
sysrepo-python-plugind-setup = "sysrepo_python_plugind.install_yang:main"
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Repository = "https://github.com/entrypoint-communications/sysrepo-python-plugind"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["src"]
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.package-data]
|
|
25
|
+
sysrepo_python_plugind = ["yang/*.yang"]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
test = ["pytest>=7"]
|
|
29
|
+
|
|
30
|
+
[tool.pytest.ini_options]
|
|
31
|
+
testpaths = ["tests"]
|
|
32
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright (c) 2026 EntryPoint Communications, LLC
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
|
|
4
|
+
"""sysrepo-python-plugind — Python plugin daemon for sysrepo.
|
|
5
|
+
|
|
6
|
+
Public API:
|
|
7
|
+
|
|
8
|
+
- :class:`SysrepoPlugin`: Abstract base class for Python plugins.
|
|
9
|
+
- :func:`main`: Entry point for the ``sysrepo-python-plugind`` console script.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .cli import main
|
|
13
|
+
from .plugin import SysrepoPlugin
|
|
14
|
+
|
|
15
|
+
__all__ = ["SysrepoPlugin", "main"]
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Copyright (c) 2026 EntryPoint Communications, LLC
|
|
2
|
+
# SPDX-License-Identifier: BSD-3-Clause
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import sysrepo
|
|
11
|
+
|
|
12
|
+
from .daemon import PluginDaemon
|
|
13
|
+
from .pid import PidFile
|
|
14
|
+
|
|
15
|
+
# Map sysrepo-plugind -v levels to Python logging levels, matching the C daemon.
|
|
16
|
+
_LOG_LEVELS = {
|
|
17
|
+
0: logging.ERROR,
|
|
18
|
+
1: logging.WARNING,
|
|
19
|
+
2: logging.INFO,
|
|
20
|
+
3: logging.DEBUG,
|
|
21
|
+
4: logging.DEBUG,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main() -> int:
|
|
26
|
+
"""Parse CLI arguments and run the sysrepo Python plugin daemon.
|
|
27
|
+
|
|
28
|
+
Parses ``-d``, ``-v``, ``-p``, and ``-f`` flags (mirroring the C
|
|
29
|
+
sysrepo-plugind), configures logging, creates a PluginDaemon, and
|
|
30
|
+
either daemonizes or runs in the foreground.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
int: Exit code; 0 on success, 1 on error.
|
|
34
|
+
"""
|
|
35
|
+
parser = argparse.ArgumentParser(
|
|
36
|
+
prog="sysrepo-python-plugind",
|
|
37
|
+
description="Sysrepo Python plugin daemon",
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"-d",
|
|
41
|
+
dest="debug",
|
|
42
|
+
action="store_true",
|
|
43
|
+
help="debug mode: stay in foreground, log to stderr",
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"-v",
|
|
47
|
+
dest="verbosity",
|
|
48
|
+
type=int,
|
|
49
|
+
default=2,
|
|
50
|
+
metavar="LEVEL",
|
|
51
|
+
choices=range(5),
|
|
52
|
+
help="log verbosity 0-4 (default: 2 = INFO)",
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"-p",
|
|
56
|
+
dest="pid_file",
|
|
57
|
+
metavar="PATH",
|
|
58
|
+
help="write PID file at PATH",
|
|
59
|
+
)
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"-f",
|
|
62
|
+
dest="fatal_fail",
|
|
63
|
+
action="store_true",
|
|
64
|
+
help="exit if any plugin fails to initialise",
|
|
65
|
+
)
|
|
66
|
+
args = parser.parse_args()
|
|
67
|
+
|
|
68
|
+
log_level = _LOG_LEVELS.get(args.verbosity, logging.DEBUG)
|
|
69
|
+
_configure_logging(debug=args.debug, level=log_level)
|
|
70
|
+
|
|
71
|
+
daemon = PluginDaemon(fatal_fail=args.fatal_fail)
|
|
72
|
+
|
|
73
|
+
if args.pid_file:
|
|
74
|
+
return _run_with_pid(daemon, args.pid_file, debug=args.debug)
|
|
75
|
+
|
|
76
|
+
if not args.debug:
|
|
77
|
+
_daemonize()
|
|
78
|
+
return daemon.run()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ------------------------------------------------------------------------------
|
|
82
|
+
def _run_with_pid(daemon: PluginDaemon, pid_path: str, debug: bool) -> int:
|
|
83
|
+
"""Run the daemon with a PID file, handling lock failure gracefully.
|
|
84
|
+
|
|
85
|
+
Opens the PID file lock before daemonizing so that a second invocation
|
|
86
|
+
fails immediately with a clear error message.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
daemon (PluginDaemon): Configured daemon instance to run.
|
|
90
|
+
pid_path (str): Filesystem path for the PID file.
|
|
91
|
+
debug (bool): If True, skip daemonization and stay in foreground.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
int: Exit code from daemon.run(), or 1 if the PID file is already
|
|
95
|
+
locked by another instance.
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
with PidFile(pid_path) as pid_file:
|
|
99
|
+
if not debug:
|
|
100
|
+
_daemonize()
|
|
101
|
+
return daemon.run(pid_file)
|
|
102
|
+
except RuntimeError as exc:
|
|
103
|
+
# PID file lock failure — another instance is running.
|
|
104
|
+
logging.getLogger(__name__).error("%s", exc)
|
|
105
|
+
return 1
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _configure_logging(debug: bool, level: int) -> None:
|
|
109
|
+
"""Configure Python and sysrepo logging.
|
|
110
|
+
|
|
111
|
+
In debug mode logs to stderr; otherwise routes to syslog under the
|
|
112
|
+
application name ``'sysrepo-python-plugind'``.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
debug (bool): If True, log to stderr; otherwise log to syslog.
|
|
116
|
+
level (int): Python logging level (e.g. ``logging.INFO``).
|
|
117
|
+
"""
|
|
118
|
+
handler = logging.StreamHandler()
|
|
119
|
+
handler.setLevel(level)
|
|
120
|
+
logging.getLogger().addHandler(handler)
|
|
121
|
+
logging.getLogger().setLevel(level)
|
|
122
|
+
|
|
123
|
+
if debug:
|
|
124
|
+
sysrepo.configure_logging(stderr_level=level, py_logging=True)
|
|
125
|
+
else:
|
|
126
|
+
sysrepo.configure_logging(
|
|
127
|
+
syslog_level=level,
|
|
128
|
+
syslog_app_name="sysrepo-python-plugind",
|
|
129
|
+
py_logging=True,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _daemonize() -> None:
|
|
134
|
+
"""Daemonize the current process using the standard double-fork technique.
|
|
135
|
+
|
|
136
|
+
After the second fork the process is in its own session, has no
|
|
137
|
+
controlling terminal, and has stdin/stdout/stderr redirected to
|
|
138
|
+
/dev/null. File descriptors beyond stderr are left open so that a
|
|
139
|
+
PidFile lock opened before this call survives in the child process.
|
|
140
|
+
"""
|
|
141
|
+
# First fork: become session leader's child.
|
|
142
|
+
pid = os.fork()
|
|
143
|
+
if pid > 0:
|
|
144
|
+
sys.exit(0)
|
|
145
|
+
|
|
146
|
+
os.setsid()
|
|
147
|
+
|
|
148
|
+
# Second fork: ensure we can never re-acquire a controlling terminal.
|
|
149
|
+
pid = os.fork()
|
|
150
|
+
if pid > 0:
|
|
151
|
+
sys.exit(0)
|
|
152
|
+
|
|
153
|
+
os.chdir("/")
|
|
154
|
+
|
|
155
|
+
devnull = os.open(os.devnull, os.O_RDWR)
|
|
156
|
+
for fd in (sys.stdin.fileno(), sys.stdout.fileno(), sys.stderr.fileno()):
|
|
157
|
+
os.dup2(devnull, fd)
|
|
158
|
+
os.close(devnull)
|