Skip to content

Commit 3fce51f

Browse files
authored
Merge pull request #1459 from moreati/release-0.3.42
Release 0.3.42
2 parents 6bf20c8 + d2009e8 commit 3fce51f

File tree

17 files changed

+320
-124
lines changed

17 files changed

+320
-124
lines changed

ansible_mitogen/connection.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,22 @@ def _connect_lxd(spec):
256256
}
257257

258258

259+
def _connect_incus(spec):
260+
"""
261+
Return ContextService arguments for an Incus container connection.
262+
"""
263+
return {
264+
'method': 'incus',
265+
'kwargs': {
266+
'container': spec.remote_addr(),
267+
'python_path': spec.python_path(),
268+
'incus_path': spec.mitogen_incus_path(),
269+
'connect_timeout': spec.timeout(),
270+
'remote_name': get_remote_name(spec),
271+
}
272+
}
273+
274+
259275
def _connect_machinectl(spec):
260276
"""
261277
Return ContextService arguments for a machinectl connection.
@@ -415,6 +431,7 @@ def _connect_mitogen_doas(spec):
415431
'kubectl': _connect_kubectl,
416432
'jail': _connect_jail,
417433
'local': _connect_local,
434+
'incus': _connect_incus,
418435
'lxc': _connect_lxc,
419436
'lxd': _connect_lxd,
420437
'machinectl': _connect_machinectl,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# SPDX-FileCopyrightText: 2019 David Wilson
2+
# SPDX-FileCopyrightText: 2026 Mitogen authors <https://github.com/mitogen-hq>
3+
# SPDX-License-Identifier: BSD-3-Clause
4+
# !mitogen: minify_safe
5+
6+
from __future__ import absolute_import, division, print_function
7+
__metaclass__ = type
8+
9+
import os
10+
import sys
11+
12+
try:
13+
import ansible_mitogen
14+
except ImportError:
15+
sys.path.insert(0, os.path.abspath(os.path.join(__file__, '../../../..')))
16+
17+
import ansible_mitogen.connection
18+
19+
20+
class Connection(ansible_mitogen.connection.Connection):
21+
transport = 'incus'

ansible_mitogen/process.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ def _setup_responder(responder):
185185
certain packages, and to generate custom responses for certain modules.
186186
"""
187187
responder.whitelist_prefix('ansible')
188+
responder.whitelist_prefix('ansible_collections')
188189
responder.whitelist_prefix('ansible_mitogen')
189190

190191
# Ansible 2.3 is compatible with Python 2.4 targets, however

ansible_mitogen/transport_config.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,12 @@ def mitogen_kubectl_path(self):
349349
The path to the "kubectl" program for the 'docker' transport.
350350
"""
351351

352+
@abc.abstractmethod
353+
def mitogen_incus_path(self):
354+
"""
355+
The path to the "incus" program for the 'incus' transport.
356+
"""
357+
352358
@abc.abstractmethod
353359
def mitogen_lxc_path(self):
354360
"""
@@ -584,6 +590,9 @@ def mitogen_docker_path(self):
584590
def mitogen_kubectl_path(self):
585591
return self._connection.get_task_var('mitogen_kubectl_path')
586592

593+
def mitogen_incus_path(self):
594+
return self._connection.get_task_var('mitogen_incus_path')
595+
587596
def mitogen_lxc_path(self):
588597
return self._connection.get_task_var('mitogen_lxc_path')
589598

@@ -832,6 +841,9 @@ def mitogen_docker_path(self):
832841
def mitogen_kubectl_path(self):
833842
return self._host_vars.get('mitogen_kubectl_path')
834843

844+
def mitogen_incus_path(self):
845+
return self._host_vars.get('mitogen_incus_path')
846+
835847
def mitogen_lxc_path(self):
836848
return self._host_vars.get('mitogen_lxc_path')
837849

docs/changelog.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@ To avail of fixes in an unreleased version, please download a ZIP file
1818
`directly from GitHub <https://github.com/mitogen-hq/mitogen/>`_.
1919

2020

21+
v0.3.42 (2026-02-20)
22+
--------------------
23+
24+
* :gh:issue:`1451` :mod:`mitogen`: Refactor module whitelist & blacklist with
25+
module overrides and blocks. Improve error messages for denied modules.
26+
* :gh:issue:`1394` :mod:`ansible_mitogen`, :mod:`mitogen`: Add Incus connection
27+
support, allowing users who migrated from LXD to Incus to use ansible-mitogen
28+
* :gh:issue:`1456` :mod:`ansible_mitogen`: Fix Ansible collections
29+
:exc:`!mitogen.core.ModuleDeniedByOverridesError`
30+
31+
2132
v0.3.41 (2026-02-10)
2233
--------------------
2334

mitogen/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535

3636

3737
#: Library version as a tuple.
38-
__version__ = (0, 3, 41)
38+
__version__ = (0, 3, 42)
3939

4040

4141
#: This is :data:`False` in slave contexts. Previously it was used to prevent

mitogen/core.py

Lines changed: 71 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def _path_importer_cache(cls, path):
7272
import os
7373
import pstats
7474
import pty
75+
import re
7576
import signal
7677
import socket
7778
import struct
@@ -360,6 +361,18 @@ class LatchError(Error):
360361
pass
361362

362363

364+
class ModuleDeniedByOverridesError(ModuleNotFoundError):
365+
fmt = "Mitogen won't serve %s, it's not in the overrides list"
366+
367+
368+
class ModuleDeniedByBlocksError(ModuleNotFoundError):
369+
fmt = "Mitogen won't serve %s, it's in the blocks list"
370+
371+
372+
class ModuleUnsuitableError(ModuleNotFoundError):
373+
fmt = "Mitogen won't serve %s, it's e.g. binary, legacy, part of stdlib"
374+
375+
363376
class Blob(BytesType):
364377
"""
365378
A serializable bytes subclass whose content is summarized in repr() output,
@@ -499,6 +512,11 @@ def has_parent_authority(msg, _stream=None):
499512
return _has_parent_authority(msg.auth_id)
500513

501514

515+
def module_lineage(fullname):
516+
"Return an iterator of a module's parent fullnames and its own"
517+
return (fullname[:m.start()] for m in re.finditer(r'\.|\Z', fullname))
518+
519+
502520
def _signals(obj, signal):
503521
return (
504522
obj.__dict__
@@ -564,21 +582,6 @@ def takes_router(func):
564582
return func
565583

566584

567-
def is_blacklisted_import(importer, fullname):
568-
"""
569-
Return :data:`True` if `fullname` is part of a blacklisted package, or if
570-
any packages have been whitelisted and `fullname` is not part of one.
571-
572-
NB:
573-
- The default whitelist is `['']` which matches any module name.
574-
- If a package is on both lists, then it is treated as blacklisted.
575-
- If any package is whitelisted, then all non-whitelisted packages are
576-
treated as blacklisted.
577-
"""
578-
return ((not any(fullname.startswith(s) for s in importer.whitelist)) or
579-
(any(fullname.startswith(s) for s in importer.blacklist)))
580-
581-
582585
def set_cloexec(fd):
583586
"""
584587
Set the file descriptor `fd` to automatically close on :func:`os.execve`.
@@ -1302,6 +1305,46 @@ def __repr__(self):
13021305
)
13031306

13041307

1308+
class ImportPolicy(object):
1309+
"""
1310+
Policy deciding which module prefixes :class:`Importer` will request from
1311+
:class:`mitogen.master.ModuleResponder` and which requests will be served
1312+
or denied.
1313+
1314+
:param overrides:
1315+
Prefixes always requested, ignoring local versions. If ``overrides``
1316+
has entries, then it's also used as an allow list by the responder -
1317+
any request for a prefix that's not overriden will be denied.
1318+
1319+
:param blocks:
1320+
Prefixes always denied by the responder, only local versions can be
1321+
used.
1322+
"""
1323+
def __init__(self, overrides=(), blocks=()):
1324+
self.overrides = set(overrides)
1325+
self.blocks = set(blocks)
1326+
self._always = set(Importer.ALWAYS_BLACKLIST)
1327+
1328+
def denied(self, fullname):
1329+
fullnames = frozenset(module_lineage(fullname))
1330+
if self.overrides and not self.overrides.intersection(fullnames):
1331+
return ModuleDeniedByOverridesError
1332+
if self.blocks.intersection(fullnames): return ModuleDeniedByBlocksError
1333+
if self._always.intersection(fullnames): return ModuleUnsuitableError
1334+
return False
1335+
1336+
def denied_raise(self, fullname):
1337+
denial = self.denied(fullname)
1338+
if denial: raise denial(denial.fmt % (fullname,))
1339+
1340+
def overriden(self, fullname):
1341+
return bool(self.overrides.intersection(module_lineage(fullname)))
1342+
1343+
def __repr__(self):
1344+
args = (type(self).__name__, self.overrides, self.blocks)
1345+
return '%s(overrides=%r, blocks=%r)' % args
1346+
1347+
13051348
class Importer(object):
13061349
"""
13071350
Import protocol implementation that fetches modules from the parent
@@ -1361,18 +1404,12 @@ class Importer(object):
13611404
if sys.version_info >= (3, 0):
13621405
ALWAYS_BLACKLIST += ['cStringIO']
13631406

1364-
def __init__(self, router, context, core_src, whitelist=(), blacklist=()):
1407+
def __init__(self, router, context, core_src, policy):
13651408
self._log = logging.getLogger('mitogen.importer')
13661409
self._context = context
13671410
self._present = {'mitogen': self.MITOGEN_PKG_CONTENT}
13681411
self._lock = threading.Lock()
1369-
self.whitelist = list(whitelist) or ['']
1370-
self.blacklist = list(blacklist) + self.ALWAYS_BLACKLIST
1371-
1372-
# Preserve copies of the original server-supplied whitelist/blacklist
1373-
# for later use by children.
1374-
self.master_whitelist = self.whitelist[:]
1375-
self.master_blacklist = self.blacklist[:]
1412+
self.policy = policy
13761413

13771414
# Presence of an entry in this map indicates in-flight GET_MODULE.
13781415
self._callbacks = {}
@@ -1465,11 +1502,8 @@ def find_module(self, fullname, path=None):
14651502
self._log.debug('%s has no submodule %s', pkgname, suffix)
14661503
return None
14671504

1468-
# #114: explicitly whitelisted prefixes override any
1469-
# system-installed package.
1470-
if self.whitelist != ['']:
1471-
if any(fullname.startswith(s) for s in self.whitelist):
1472-
return self
1505+
if self.policy.overriden(fullname):
1506+
return self
14731507

14741508
try:
14751509
self.builtin_find_module(fullname)
@@ -1515,11 +1549,9 @@ def find_spec(self, fullname, path, target=None):
15151549
fullname, pkgname, pkg_loader)
15161550
return None
15171551

1518-
# #114: whitelisted prefixes override any system-installed package.
1519-
if self.whitelist != ['']:
1520-
if any(s and fullname.startswith(s) for s in self.whitelist):
1521-
log.debug('Handling %s. It is whitelisted', fullname)
1522-
return importlib.machinery.ModuleSpec(fullname, loader=self)
1552+
if self.policy.overriden(fullname):
1553+
log.debug('Handling %s. It is overriden', fullname)
1554+
return importlib.machinery.ModuleSpec(fullname, loader=self)
15231555

15241556
if fullname == '__main__':
15251557
log.debug('Handling %s. A special case', fullname)
@@ -1540,10 +1572,6 @@ def find_spec(self, fullname, path, target=None):
15401572
log.debug('Handling %s. Unavailable locally', fullname)
15411573
return importlib.machinery.ModuleSpec(fullname, loader=self)
15421574

1543-
blacklisted_msg = (
1544-
'A %r request would be refused by the Mitogen master. The module is '
1545-
'on the deny list (blacklist) or not on the allow list (whitelist).'
1546-
)
15471575
pkg_resources_msg = (
15481576
'pkg_resources is prohibited from importing __main__, as it causes '
15491577
'problems in applications whose main module is not designed to be '
@@ -1556,8 +1584,7 @@ def find_spec(self, fullname, path, target=None):
15561584
)
15571585

15581586
def _refuse_imports(self, fullname):
1559-
if is_blacklisted_import(self, fullname):
1560-
raise ModuleNotFoundError(self.blacklisted_msg % (fullname,))
1587+
self.policy.denied_raise(fullname)
15611588

15621589
f = sys._getframe(2)
15631590
requestee = f.f_globals['__name__']
@@ -4175,12 +4202,15 @@ def _setup_importer(self):
41754202
else:
41764203
core_src = None
41774204

4205+
policy = ImportPolicy(
4206+
self.config['import_overrides'],
4207+
self.config['import_blocks'],
4208+
)
41784209
importer = Importer(
41794210
self.router,
41804211
self.parent,
41814212
core_src,
4182-
self.config.get('whitelist', ()),
4183-
self.config.get('blacklist', ()),
4213+
policy,
41844214
)
41854215

41864216
self.importer = importer

mitogen/incus.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# SPDX-FileCopyrightText: 2019 David Wilson
2+
# SPDX-FileCopyrightText: 2026 Mitogen authors <https://github.com/mitogen-hq>
3+
# SPDX-License-Identifier: BSD-3-Clause
4+
# !mitogen: minify_safe
5+
6+
import mitogen.parent
7+
8+
9+
class Options(mitogen.parent.Options):
10+
container = None
11+
incus_path = 'incus'
12+
python_path = 'python'
13+
14+
def __init__(self, container, incus_path=None, **kwargs):
15+
super(Options, self).__init__(**kwargs)
16+
self.container = container
17+
if incus_path:
18+
self.incus_path = incus_path
19+
20+
21+
class Connection(mitogen.parent.Connection):
22+
options_class = Options
23+
24+
child_is_immediate_subprocess = False
25+
create_child_args = {
26+
# If incus finds any of stdin, stdout, stderr connected to a TTY, to
27+
# prevent input injection it creates a proxy pty, forcing all IO to be
28+
# buffered in <4KiB chunks. So ensure stderr is also routed to the
29+
# socketpair.
30+
'merge_stdio': True
31+
}
32+
33+
eof_error_hint = (
34+
'Note: many versions of Incus do not report program execution failure '
35+
'meaningfully. Please check the host logs (/var/log) for more '
36+
'information.'
37+
)
38+
39+
def _get_name(self):
40+
return u'incus.' + self.options.container
41+
42+
def get_boot_command(self):
43+
bits = [
44+
self.options.incus_path,
45+
'exec',
46+
'--mode=non-interactive',
47+
self.options.container,
48+
'--',
49+
]
50+
return bits + super(Connection, self).get_boot_command()

0 commit comments

Comments
 (0)