1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
|
#
# Ouroboros - Copyright (C) 2016 - 2026
#
# Python API for Ouroboros - CLI equivalents
#
# Dimitri Staessens <dimitri@ouroboros.rocks>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public License
# version 2.1 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, write to the Free Software
# Foundation, Inc., http://www.fsf.org/about/contact/.
#
"""
Higher-level wrappers that mirror CLI tool behaviour.
The ``irm`` CLI tools perform extra steps that the raw C library API
does not, such as resolving program names via ``realpath``, looking up
IPCP pids, and the ``autobind`` flag for bootstrapping/enrolling.
This module exposes those same patterns as a Python API so that
callers do not need to re-implement them.
Each wrapper corresponds to a specific ``irm`` sub-command:
========================= ====================================
Python CLI equivalent
========================= ====================================
``create_ipcp`` ``irm ipcp create``
``destroy_ipcp`` ``irm ipcp destroy``
``bootstrap_ipcp`` ``irm ipcp bootstrap [autobind]``
``enroll_ipcp`` ``irm ipcp enroll [autobind]``
``connect_ipcp`` ``irm ipcp connect``
``disconnect_ipcp`` ``irm ipcp disconnect``
``list_ipcps`` ``irm ipcp list``
``bind_program`` ``irm bind program``
``bind_process`` ``irm bind process``
``bind_ipcp`` ``irm bind ipcp``
``unbind_program`` ``irm unbind program``
``unbind_process`` ``irm unbind process``
``unbind_ipcp`` ``irm unbind ipcp``
``create_name`` ``irm name create``
``destroy_name`` ``irm name destroy``
``reg_name`` ``irm name register``
``unreg_name`` ``irm name unregister``
``list_names`` ``irm name list``
``autoboot`` ``irm ipcp bootstrap autobind``
(with implicit ``create``)
========================= ====================================
Usage::
from ouroboros.cli import create_ipcp, bootstrap_ipcp, enroll_ipcp
from ouroboros.cli import bind_program, autoboot
"""
import shutil
from typing import List, Optional
from ouroboros.irm import (
DT_COMP,
MGMT_COMP,
IpcpType,
IpcpConfig,
IpcpInfo,
NameInfo,
BindError,
IrmError,
UnicastConfig,
DtConfig,
RoutingConfig,
LinkStateConfig,
LinkStatePolicy,
EthConfig,
Udp4Config,
Udp6Config,
bind_program as _irm_bind_program,
bind_process as _irm_bind_process,
bootstrap_ipcp as _irm_bootstrap_ipcp,
connect_ipcp as _irm_connect_ipcp,
create_ipcp,
create_name as _irm_create_name,
destroy_ipcp as _irm_destroy_ipcp,
destroy_name,
disconnect_ipcp as _irm_disconnect_ipcp,
enroll_ipcp as _irm_enroll_ipcp,
list_ipcps as _irm_list_ipcps,
list_names as _irm_list_names,
reg_name as _irm_reg_name,
unbind_process as _irm_unbind_process,
unbind_program as _irm_unbind_program,
unreg_name as _irm_unreg_name,
)
from ouroboros.qos import QoSSpec
def _pid_of(ipcp_name: str) -> int:
"""Look up the pid of a running IPCP by its name."""
for info in _irm_list_ipcps():
if info.name == ipcp_name:
return info.pid
raise ValueError(f"No IPCP named {ipcp_name!r}")
def destroy_ipcp(name: str) -> None:
"""
Destroy an IPCP by name.
Mirrors ``irm ipcp destroy name <name>``.
Resolves the IPCP name to a pid, then destroys the IPCP.
:param name: Name of the IPCP to destroy.
:raises ValueError: If no IPCP with *name* exists.
"""
_irm_destroy_ipcp(_pid_of(name))
def list_ipcps(name: Optional[str] = None,
layer: Optional[str] = None,
ipcp_type: Optional[IpcpType] = None) -> List[IpcpInfo]:
"""
List running IPCPs, optionally filtered.
Mirrors ``irm ipcp list [name <n>] [type <t>] [layer <l>]``.
:param name: Filter by IPCP name (exact match).
:param layer: Filter by layer name (exact match).
:param ipcp_type: Filter by IPCP type.
:return: List of matching :class:`IpcpInfo` objects.
"""
result = _irm_list_ipcps()
if name is not None:
result = [i for i in result if i.name == name]
if layer is not None:
result = [i for i in result if i.layer == layer]
if ipcp_type is not None:
result = [i for i in result if i.type == ipcp_type]
return result
def reg_name(name: str,
ipcp: Optional[str] = None,
ipcps: Optional[List[str]] = None,
layer: Optional[str] = None,
layers: Optional[List[str]] = None) -> None:
"""
Register a name with IPCP(s), creating it first if needed.
Mirrors ``irm name register <name> ipcp <ipcp> [ipcp ...]
layer <layer> [layer ...]``.
The C CLI tool resolves IPCP names and layer names to pids,
checks whether the name already exists and calls
``irm_create_name`` before ``irm_reg_name`` for each IPCP.
The function accepts flexible input:
- A single *ipcp* name or list of *ipcps* names.
- A single *layer* name or list of *layers* names (registers
with every IPCP in each of those layers).
- Any combination of the above.
:param name: The name to register.
:param ipcp: Single IPCP name to register with.
:param ipcps: List of IPCP names to register with.
:param layer: Single layer name to register with.
:param layers: List of layer names to register with.
"""
existing = {n.name for n in _irm_list_names()}
if name not in existing:
_irm_create_name(NameInfo(name=name))
pids = set()
# Collect IPCP names into a single list
ipcp_names = []
if ipcp is not None:
ipcp_names.append(ipcp)
if ipcps is not None:
ipcp_names.extend(ipcps)
# Collect layer names into a single list
layer_names = []
if layer is not None:
layer_names.append(layer)
if layers is not None:
layer_names.extend(layers)
if ipcp_names or layer_names:
all_ipcps = _irm_list_ipcps()
for ipcp_name in ipcp_names:
for i in all_ipcps:
if i.name == ipcp_name:
pids.add(i.pid)
break
for lyr in layer_names:
for i in all_ipcps:
if i.layer == lyr:
pids.add(i.pid)
for p in pids:
_irm_reg_name(name, p)
def bind_program(prog: str,
name: str,
opts: int = 0,
argv: Optional[List[str]] = None) -> None:
"""
Bind a program to a name, resolving bare names to full paths.
Mirrors ``irm bind program <prog> name <name>``.
The ``irm bind program`` CLI tool calls ``realpath()`` on *prog*
before passing it to the library. The raw C function
``irm_bind_program`` contains a ``check_prog_path`` helper that
corrupts the ``PATH`` environment variable (writes NUL over ``:``
separators) when given a bare program name. Only the first such
call would succeed in a long-running process.
This wrapper resolves *prog* via ``shutil.which()`` before calling
the library, avoiding the bug entirely.
:param prog: Program name or path. Bare names (without ``/``)
are resolved on ``PATH`` via ``shutil.which()``.
:param name: Name to bind to.
:param opts: Bind options (e.g. ``BIND_AUTO``).
:param argv: Arguments to pass when the program is auto-started.
:raises BindError: If the program cannot be found or the bind
call fails.
"""
if '/' not in prog:
resolved = shutil.which(prog)
if resolved is None:
raise BindError(f"Program {prog!r} not found on PATH")
prog = resolved
_irm_bind_program(prog, name, opts=opts, argv=argv)
def unbind_program(prog: str, name: str) -> None:
"""
Unbind a program from a name.
Mirrors ``irm unbind program <prog> name <name>``.
:param prog: Path to the program.
:param name: Name to unbind from.
"""
_irm_unbind_program(prog, name)
def bind_process(pid: int, name: str) -> None:
"""
Bind a running process to a name.
Mirrors ``irm bind process <pid> name <name>``.
:param pid: PID of the process.
:param name: Name to bind to.
"""
_irm_bind_process(pid, name)
def unbind_process(pid: int, name: str) -> None:
"""
Unbind a process from a name.
Mirrors ``irm unbind process <pid> name <name>``.
:param pid: PID of the process.
:param name: Name to unbind from.
"""
_irm_unbind_process(pid, name)
def bind_ipcp(ipcp: str, name: str) -> None:
"""
Bind an IPCP to a name.
Mirrors ``irm bind ipcp <ipcp> name <name>``.
Resolves the IPCP name to a pid, then calls ``bind_process``.
:param ipcp: IPCP instance name.
:param name: Name to bind to.
:raises ValueError: If no IPCP with *ipcp* exists.
"""
_irm_bind_process(_pid_of(ipcp), name)
def unbind_ipcp(ipcp: str, name: str) -> None:
"""
Unbind an IPCP from a name.
Mirrors ``irm unbind ipcp <ipcp> name <name>``.
Resolves the IPCP name to a pid, then calls ``unbind_process``.
:param ipcp: IPCP instance name.
:param name: Name to unbind from.
:raises ValueError: If no IPCP with *ipcp* exists.
"""
_irm_unbind_process(_pid_of(ipcp), name)
def create_name(name: str,
pol_lb: Optional[int] = None,
info: Optional[NameInfo] = None) -> None:
"""
Create a registered name.
Mirrors ``irm name create <name> [lb <policy>]``.
:param name: The name to create.
:param pol_lb: Load-balance policy (optional).
:param info: Full :class:`NameInfo` (overrides *name*/*pol_lb*
if given).
"""
if info is not None:
_irm_create_name(info)
else:
ni = NameInfo(name=name)
if pol_lb is not None:
ni.pol_lb = pol_lb
_irm_create_name(ni)
def list_names(name: Optional[str] = None) -> List[NameInfo]:
"""
List all registered names, optionally filtered.
Mirrors ``irm name list [<name>]``.
:param name: Filter by name (exact match).
:return: List of :class:`NameInfo` objects.
"""
result = _irm_list_names()
if name is not None:
result = [n for n in result if n.name == name]
return result
def unreg_name(name: str,
ipcp: Optional[str] = None,
ipcps: Optional[List[str]] = None,
layer: Optional[str] = None,
layers: Optional[List[str]] = None) -> None:
"""
Unregister a name from IPCP(s).
Mirrors ``irm name unregister <name> ipcp <ipcp> [ipcp ...]
layer <layer> [layer ...]``.
Accepts the same flexible input as :func:`reg_name`.
:param name: The name to unregister.
:param ipcp: Single IPCP name to unregister from.
:param ipcps: List of IPCP names to unregister from.
:param layer: Single layer name to unregister from.
:param layers: List of layer names to unregister from.
"""
pids = set()
ipcp_names = []
if ipcp is not None:
ipcp_names.append(ipcp)
if ipcps is not None:
ipcp_names.extend(ipcps)
layer_names = []
if layer is not None:
layer_names.append(layer)
if layers is not None:
layer_names.extend(layers)
if ipcp_names or layer_names:
all_ipcps = _irm_list_ipcps()
for ipcp_name in ipcp_names:
for i in all_ipcps:
if i.name == ipcp_name:
pids.add(i.pid)
break
for lyr in layer_names:
for i in all_ipcps:
if i.layer == lyr:
pids.add(i.pid)
for p in pids:
_irm_unreg_name(name, p)
def bootstrap_ipcp(name: str,
conf: IpcpConfig,
autobind: bool = False) -> None:
"""
Bootstrap an IPCP, optionally binding it to its name and layer.
Mirrors ``irm ipcp bootstrap name <n> layer <l> [autobind]``.
When *autobind* is ``True`` and the IPCP type is ``UNICAST`` or
``BROADCAST``, the sequence is::
bind_process(pid, ipcp_name) # accept flows for ipcp name
bind_process(pid, layer_name) # accept flows for layer name
bootstrap_ipcp(pid, conf) # bootstrap into the layer
This matches the C ``irm ipcp bootstrap`` tool exactly. If
bootstrap fails after autobind, the bindings are rolled back.
:param name: Name of the IPCP.
:param conf: IPCP configuration (includes layer name & type).
:param autobind: Bind the IPCP process to its name and layer.
"""
pid = _pid_of(name)
layer_name = conf.layer_name
if autobind and conf.ipcp_type in (IpcpType.UNICAST,
IpcpType.BROADCAST):
_irm_bind_process(pid, name)
_irm_bind_process(pid, layer_name)
try:
_irm_bootstrap_ipcp(pid, conf)
except Exception:
if autobind and conf.ipcp_type in (IpcpType.UNICAST,
IpcpType.BROADCAST):
_irm_unbind_process(pid, name)
_irm_unbind_process(pid, layer_name)
raise
def enroll_ipcp(name: str, dst: str,
autobind: bool = False) -> None:
"""
Enroll an IPCP, optionally binding it to its name and layer.
Mirrors ``irm ipcp enroll name <n> layer <dst> [autobind]``.
When *autobind* is ``True``, the sequence is::
enroll_ipcp(pid, dst)
bind_process(pid, ipcp_name)
bind_process(pid, layer_name) # layer learned from enrollment
This matches the C ``irm ipcp enroll`` tool exactly.
:param name: Name of the IPCP.
:param dst: Destination name or layer to enroll with.
:param autobind: Bind the IPCP process to its name and layer
after successful enrollment.
"""
pid = _pid_of(name)
_irm_enroll_ipcp(pid, dst)
if autobind:
# Look up enrolled layer from the IPCP list
for info in _irm_list_ipcps():
if info.pid == pid:
_irm_bind_process(pid, info.name)
_irm_bind_process(pid, info.layer)
break
def connect_ipcp(name: str, dst: str, comp: str = "*",
qos: Optional[QoSSpec] = None) -> None:
"""
Connect IPCP components to a destination.
Mirrors ``irm ipcp connect name <n> dst <dst> [component <c>]
[qos <qos>]``.
When *comp* is ``"*"`` (default), both ``dt`` and ``mgmt``
components are connected, matching the CLI default.
:param name: Name of the IPCP.
:param dst: Destination IPCP name.
:param comp: Component to connect: ``"dt"``, ``"mgmt"``, or
``"*"`` for both (default).
:param qos: QoS specification for the dt component.
"""
pid = _pid_of(name)
if comp in ("*", "mgmt"):
_irm_connect_ipcp(pid, MGMT_COMP, dst)
if comp in ("*", "dt"):
_irm_connect_ipcp(pid, DT_COMP, dst, qos=qos)
def disconnect_ipcp(name: str, dst: str, comp: str = "*") -> None:
"""
Disconnect IPCP components from a destination.
Mirrors ``irm ipcp disconnect name <n> dst <dst>
[component <c>]``.
When *comp* is ``"*"`` (default), both ``dt`` and ``mgmt``
components are disconnected, matching the CLI default.
:param name: Name of the IPCP.
:param dst: Destination IPCP name.
:param comp: Component to disconnect: ``"dt"``, ``"mgmt"``, or
``"*"`` for both (default).
"""
pid = _pid_of(name)
if comp in ("*", "mgmt"):
_irm_disconnect_ipcp(pid, MGMT_COMP, dst)
if comp in ("*", "dt"):
_irm_disconnect_ipcp(pid, DT_COMP, dst)
def autoboot(name: str,
ipcp_type: IpcpType,
layer: str,
conf: Optional[IpcpConfig] = None) -> None:
"""
Create, autobind and bootstrap an IPCP in one step.
Convenience wrapper equivalent to::
irm ipcp bootstrap name <name> type <type> layer <layer> autobind
(with an implicit ``create`` if the IPCP does not yet exist).
:param name: Name for the IPCP.
:param ipcp_type: Type of IPCP to create.
:param layer: Layer name to bootstrap into.
:param conf: Optional IPCP configuration. If *None*, a
default ``IpcpConfig`` is created for the given
*ipcp_type* and *layer*.
"""
create_ipcp(name, ipcp_type)
if conf is None:
conf = IpcpConfig(ipcp_type=ipcp_type, layer_name=layer)
else:
conf.layer_name = layer
bootstrap_ipcp(name, conf, autobind=True)
|