LOOP_CONFIGURE allows to atomically configure the decive when opening it. This avoid the possibility of a race condition where between set_fd and set_status some operations are already accepted by the loopback device. See https://lwn.net/Articles/820408/ This feature was included in the linux kernel 5.8 however it is safe to not include any kind of fallback to the previous method as @obudai points out that: LOOP_CONFIGURE was backported into RHEL 8 kernel in RHEL 8.4 as a part of https://bugzilla.redhat.com/show_bug.cgi?id=1881760 (block layer: update to upstream v5.8). Since RHEL 8.4 is currently the oldest supported release that we support running osbuild on, it might be just fine implementing this without the fallback. From a centos stream 8 container: kernel-4.18.0-448.el8.x86_64 - loop: Fix missing discard support when using LOOP_CONFIGURE (Ming Lei) [1997338] - [block] loop: Set correct device size when using LOOP_CONFIGURE (Ming Lei) [1881760] - [block] loop: unset GENHD_FL_NO_PART_SCAN on LOOP_CONFIGURE (Ming Lei) [1881760] - [block] loop: Add LOOP_CONFIGURE ioctl (Ming Lei) [1881760]
123 lines
3.5 KiB
Python
123 lines
3.5 KiB
Python
import contextlib
|
|
import errno
|
|
import os
|
|
|
|
from . import api, loop
|
|
from .util import jsoncomm
|
|
|
|
__all__ = [
|
|
"LoopClient",
|
|
"LoopServer"
|
|
]
|
|
|
|
|
|
class LoopServer(api.BaseAPI):
|
|
"""Server for creating loopback devices
|
|
|
|
The server listens for requests on a AF_UNIX/SOCK_DRGAM sockets.
|
|
|
|
A request should contain SCM_RIGHTS of two filedescriptors, one
|
|
that sholud be the backing file for the new loopdevice, and a
|
|
second that should be a directory file descriptor where the new
|
|
device node will be created.
|
|
|
|
The payload should be a JSON object with the mandatory arguments
|
|
@fd which is the offset in the SCM_RIGHTS array for the backing
|
|
file descriptor and @dir_fd which is the offset for the output
|
|
directory. Optionally, @offset and @sizelimit in bytes may also
|
|
be specified.
|
|
|
|
The server respods with a JSON object containing the device name
|
|
of the new device node created in the output directory.
|
|
|
|
The created loopback device is guaranteed to be bound to the
|
|
given backing file descriptor for the lifetime of the LoopServer
|
|
object.
|
|
"""
|
|
|
|
endpoint = "remoteloop"
|
|
|
|
def __init__(self, *, socket_address=None):
|
|
super().__init__(socket_address)
|
|
self.devs = []
|
|
self.ctl = loop.LoopControl()
|
|
|
|
def _create_device(self, fd, dir_fd, offset=None, sizelimit=None):
|
|
while True:
|
|
# Getting an unbound loopback device and attaching a backing
|
|
# file descriptor to it is racy, so we must use a retry loop
|
|
lo = loop.Loop(self.ctl.get_unbound())
|
|
try:
|
|
lo.configure(fd, offset=offset, sizelimit=sizelimit, autoclear=True)
|
|
except BlockingIOError:
|
|
lo.clear_fd()
|
|
lo.close()
|
|
continue
|
|
except OSError as e:
|
|
lo.close()
|
|
# `configure` returns EBUSY when the pages from the previously
|
|
# bound file have not been fully cleared yet.
|
|
if e.errno == errno.EBUSY:
|
|
continue
|
|
raise e
|
|
break
|
|
|
|
lo.mknod(dir_fd)
|
|
# Pin the Loop objects so they are only released when the LoopServer
|
|
# is destroyed.
|
|
self.devs.append(lo)
|
|
return lo.devname
|
|
|
|
def _message(self, msg, fds, sock):
|
|
fd = fds[msg["fd"]]
|
|
dir_fd = fds[msg["dir_fd"]]
|
|
offset = msg.get("offset")
|
|
sizelimit = msg.get("sizelimit")
|
|
|
|
devname = self._create_device(fd, dir_fd, offset, sizelimit)
|
|
sock.send({"devname": devname})
|
|
|
|
def _cleanup(self):
|
|
for lo in self.devs:
|
|
lo.close()
|
|
self.ctl.close()
|
|
|
|
|
|
class LoopClient:
|
|
client = None
|
|
|
|
def __init__(self, connect_to):
|
|
self.client = jsoncomm.Socket.new_client(connect_to)
|
|
|
|
def __del__(self):
|
|
if self.client is not None:
|
|
self.client.close()
|
|
|
|
@contextlib.contextmanager
|
|
def device(self, filename, offset=None, sizelimit=None):
|
|
req = {}
|
|
fds = []
|
|
|
|
fd = os.open(filename, os.O_RDWR)
|
|
dir_fd = os.open("/dev", os.O_DIRECTORY)
|
|
|
|
fds.append(fd)
|
|
req["fd"] = 0
|
|
fds.append(dir_fd)
|
|
req["dir_fd"] = 1
|
|
|
|
if offset:
|
|
req["offset"] = offset
|
|
if sizelimit:
|
|
req["sizelimit"] = sizelimit
|
|
|
|
self.client.send(req, fds=fds)
|
|
os.close(dir_fd)
|
|
os.close(fd)
|
|
|
|
payload, _, _ = self.client.recv()
|
|
path = os.path.join("/dev", payload["devname"])
|
|
try:
|
|
yield path
|
|
finally:
|
|
os.unlink(path)
|