mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-02 10:52:22 +00:00
fix(oci): a shared host directory is recognised by its filesystem, not its device number
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
"""Read-only identity checks for directory bind mounts; never manage their data."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path, PurePosixPath
|
||||
import re
|
||||
import stat
|
||||
import subprocess
|
||||
|
||||
from oci_installation_state import parse_config
|
||||
from oci_ui import translate
|
||||
@@ -18,6 +20,74 @@ def valid_path(value):
|
||||
return value
|
||||
|
||||
|
||||
MOUNTINFO = Path('/proc/self/mountinfo')
|
||||
BY_UUID = Path('/dev/disk/by-uuid')
|
||||
# Filesystems that number their folders again every time they are mounted.
|
||||
RENUMBERED = ('vfat', 'exfat', 'msdos')
|
||||
|
||||
|
||||
def _field(value):
|
||||
return re.sub(r'\\([0-7]{3})', lambda match: chr(int(match.group(1), 8)), value)
|
||||
|
||||
|
||||
def _mounted_at(resolved):
|
||||
"""Type and source of the filesystem a path is on: the mount with the
|
||||
longest mount point above it, the last one when several share it."""
|
||||
found = None
|
||||
try:
|
||||
lines = MOUNTINFO.read_text().splitlines()
|
||||
except OSError:
|
||||
return None
|
||||
for line in lines:
|
||||
before, _, after = line.partition(' - ')
|
||||
fields, origin = before.split(), after.split()
|
||||
if len(fields) < 5 or len(origin) < 2:
|
||||
continue
|
||||
point = _field(fields[4])
|
||||
if resolved == point or resolved.startswith(point.rstrip('/') + '/'):
|
||||
if found is None or len(point) >= len(found[0]):
|
||||
found = (point, origin[0], _field(origin[1]))
|
||||
return found and found[1:]
|
||||
|
||||
|
||||
def _uuid(device):
|
||||
"""UUID of the filesystem on a block device, as the host publishes it."""
|
||||
try:
|
||||
number = os.stat(device).st_rdev
|
||||
for link in BY_UUID.iterdir():
|
||||
if os.stat(link).st_rdev == number:
|
||||
return link.name
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
result = subprocess.run(['blkid', '-o', 'value', '-s', 'UUID', device],
|
||||
capture_output=True, text=True, timeout=10, check=False)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return None
|
||||
return result.stdout.strip() or None
|
||||
|
||||
|
||||
def filesystem(resolved):
|
||||
"""What tells the filesystem of a directory apart across restarts, and its
|
||||
type. The device number does not: the host gives it again on every boot,
|
||||
and to a disk every time it is connected. A filesystem on a block device
|
||||
is told by its UUID; one without, as a ZFS dataset or a network share, by
|
||||
what is mounted. The identity is None when the host does not tell."""
|
||||
mounted = _mounted_at(resolved)
|
||||
if mounted is None:
|
||||
return None, None
|
||||
kind, origin = mounted
|
||||
if origin.startswith('/'):
|
||||
try:
|
||||
block = stat.S_ISBLK(os.stat(origin).st_mode)
|
||||
except OSError:
|
||||
block = False
|
||||
if block:
|
||||
uuid = _uuid(origin)
|
||||
return (f'uuid:{uuid}' if uuid else None), kind
|
||||
return f'{kind}:{origin}', kind
|
||||
|
||||
|
||||
def snapshot(source, allow_missing=False):
|
||||
path = Path(source)
|
||||
resolved = str(path.resolve())
|
||||
@@ -29,9 +99,10 @@ def snapshot(source, allow_missing=False):
|
||||
return {'resolved_path': resolved, 'exists': False}
|
||||
if not stat.S_ISDIR(info.st_mode):
|
||||
raise ValueError(translate('This profile only supports directory bind mounts'))
|
||||
identity, kind = filesystem(resolved)
|
||||
return {'resolved_path': resolved, 'exists': True, 'device': info.st_dev,
|
||||
'inode': info.st_ino, 'uid': info.st_uid, 'gid': info.st_gid,
|
||||
'mode': stat.S_IMODE(info.st_mode)}
|
||||
'mode': stat.S_IMODE(info.st_mode), 'filesystem': identity, 'fstype': kind}
|
||||
|
||||
|
||||
def validate_source(source, allow_missing=False, declared=False):
|
||||
@@ -51,7 +122,20 @@ def validate_source(source, allow_missing=False, declared=False):
|
||||
|
||||
def same_source(a, b):
|
||||
# Native application init may legitimately change permissions, not identity.
|
||||
return all(a.get(k) == b.get(k) for k in ('resolved_path', 'exists', 'device', 'inode'))
|
||||
if any(a.get(k) != b.get(k) for k in ('resolved_path', 'exists')):
|
||||
return False
|
||||
known = bool(a.get('filesystem') and b.get('filesystem'))
|
||||
if known and a['filesystem'] != b['filesystem']:
|
||||
return False
|
||||
if a.get('inode') != b.get('inode') and not (known and a.get('fstype') in RENUMBERED):
|
||||
return False
|
||||
if known:
|
||||
return True
|
||||
# A record written before the filesystem was looked at has only the device
|
||||
# number, which does not survive a restart: there is nothing to compare.
|
||||
if 'filesystem' not in a or 'filesystem' not in b:
|
||||
return True
|
||||
return a.get('device') == b.get('device')
|
||||
|
||||
|
||||
def verify_sources(expected):
|
||||
@@ -69,6 +153,15 @@ def verify_observation(expected, observed):
|
||||
raise ValueError(translate('The mount evidence does not match the verified directories'))
|
||||
|
||||
|
||||
def missing_sources(config):
|
||||
"""Host directories a container mounts that are not there, as when their
|
||||
disk is not connected. Proxmox cannot mount the container without them,
|
||||
and leaves it locked when it tries."""
|
||||
return [value.split(',', 1)[0] for key, value in parse_config(config).items()
|
||||
if re.fullmatch(r'mp[0-9]+', key) and value.startswith('/')
|
||||
and not Path(value.split(',', 1)[0]).exists()]
|
||||
|
||||
|
||||
def capture_sources(config):
|
||||
result = {}
|
||||
for key, value in parse_config(config).items():
|
||||
|
||||
Reference in New Issue
Block a user