mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
Compare commits
1 Commits
v0.4.0-alp
...
fix/volume
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e7d33b07f |
@@ -61,6 +61,24 @@ pub async fn unmount<P: AsRef<Path>>(mountpoint: P, lazy: bool) -> Result<(), Er
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns true if any mountpoints exist under (or at) the given path.
|
||||
pub async fn has_mounts_under<P: AsRef<Path>>(path: P) -> Result<bool, Error> {
|
||||
let path = path.as_ref();
|
||||
let canonical_path = tokio::fs::canonicalize(path)
|
||||
.await
|
||||
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("canonicalize {path:?}")))?;
|
||||
|
||||
let mounts_content = tokio::fs::read_to_string("/proc/mounts")
|
||||
.await
|
||||
.with_ctx(|_| (ErrorKind::Filesystem, "read /proc/mounts"))?;
|
||||
|
||||
Ok(mounts_content.lines().any(|line| {
|
||||
line.split_whitespace()
|
||||
.nth(1)
|
||||
.map_or(false, |mp| Path::new(mp).starts_with(&canonical_path))
|
||||
}))
|
||||
}
|
||||
|
||||
/// Unmounts all mountpoints under (and including) the given path, in reverse
|
||||
/// depth order so that nested mounts are unmounted before their parents.
|
||||
#[instrument(skip_all)]
|
||||
|
||||
@@ -6,6 +6,8 @@ use clap::builder::ValueParserFactory;
|
||||
use exver::VersionRange;
|
||||
use rust_i18n::t;
|
||||
|
||||
use tokio::process::Command;
|
||||
|
||||
use crate::db::model::package::{
|
||||
CurrentDependencies, CurrentDependencyInfo, CurrentDependencyKind, ManifestPreference,
|
||||
TaskEntry,
|
||||
@@ -19,7 +21,7 @@ use crate::service::effects::callbacks::CallbackHandler;
|
||||
use crate::service::effects::prelude::*;
|
||||
use crate::service::rpc::CallbackId;
|
||||
use crate::status::health_check::NamedHealthCheckResult;
|
||||
use crate::util::{FromStrParser, VersionString};
|
||||
use crate::util::{FromStrParser, Invoke, VersionString};
|
||||
use crate::volume::data_dir;
|
||||
use crate::{DATA_DIR, HealthCheckId, PackageId, ReplayId, VolumeId};
|
||||
|
||||
@@ -90,7 +92,7 @@ pub async fn mount(
|
||||
),
|
||||
)
|
||||
.mount(
|
||||
mountpoint,
|
||||
&mountpoint,
|
||||
if readonly {
|
||||
MountType::ReadOnly
|
||||
} else {
|
||||
@@ -99,6 +101,15 @@ pub async fn mount(
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Make the dependency mount a slave so it receives propagated mounts
|
||||
// (e.g. NAS mounts from the source service) but cannot propagate
|
||||
// mounts back to the source service's volume.
|
||||
Command::new("mount")
|
||||
.arg("--make-rslave")
|
||||
.arg(&mountpoint)
|
||||
.invoke(ErrorKind::Filesystem)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ use crate::disk::mount::filesystem::loop_dev::LoopDev;
|
||||
use crate::disk::mount::filesystem::overlayfs::OverlayGuard;
|
||||
use crate::disk::mount::filesystem::{MountType, ReadOnly};
|
||||
use crate::disk::mount::guard::{GenericMountGuard, MountGuard};
|
||||
use crate::disk::mount::util::{is_mountpoint, unmount};
|
||||
use crate::lxc::{HOST_RPC_SERVER_SOCKET, LxcConfig, LxcContainer};
|
||||
use crate::net::net_controller::NetService;
|
||||
use crate::prelude::*;
|
||||
@@ -76,6 +77,7 @@ pub struct PersistentContainer {
|
||||
pub(super) rpc_client: UnixRpcClient,
|
||||
pub(super) rpc_server: watch::Sender<Option<(NonDetachingJoinHandle<()>, ShutdownHandle)>>,
|
||||
js_mount: MountGuard,
|
||||
host_volume_binds: BTreeMap<VolumeId, MountGuard>,
|
||||
volumes: BTreeMap<VolumeId, MountGuard>,
|
||||
assets: Vec<MountGuard>,
|
||||
pub(super) images: BTreeMap<ImageId, Arc<MountGuard>>,
|
||||
@@ -120,6 +122,7 @@ impl PersistentContainer {
|
||||
.is_ok();
|
||||
|
||||
let mut volumes = BTreeMap::new();
|
||||
let mut host_volume_binds = BTreeMap::new();
|
||||
|
||||
// TODO: remove once packages are reconverted
|
||||
let added = if is_compat {
|
||||
@@ -128,13 +131,35 @@ impl PersistentContainer {
|
||||
BTreeSet::default()
|
||||
};
|
||||
for volume in s9pk.as_manifest().volumes.union(&added) {
|
||||
let host_volume_dir = data_dir(DATA_DIR, &s9pk.as_manifest().id, volume);
|
||||
|
||||
// Self-bind the host volume directory and mark it rshared so that
|
||||
// mounts created inside the container (e.g. NAS mounts from
|
||||
// postinit.sh) propagate back to the host path and are visible to
|
||||
// dependent services that bind-mount the same volume.
|
||||
if is_mountpoint(&host_volume_dir).await? {
|
||||
unmount(&host_volume_dir, true).await?;
|
||||
}
|
||||
let host_bind = MountGuard::mount(
|
||||
&Bind::new(&host_volume_dir),
|
||||
&host_volume_dir,
|
||||
MountType::ReadWrite,
|
||||
)
|
||||
.await?;
|
||||
Command::new("mount")
|
||||
.arg("--make-rshared")
|
||||
.arg(&host_volume_dir)
|
||||
.invoke(ErrorKind::Filesystem)
|
||||
.await?;
|
||||
host_volume_binds.insert(volume.clone(), host_bind);
|
||||
|
||||
let mountpoint = lxc_container
|
||||
.rootfs_dir()
|
||||
.join("media/startos/volumes")
|
||||
.join(volume);
|
||||
let mount = MountGuard::mount(
|
||||
&IdMapped::new(
|
||||
Bind::new(data_dir(DATA_DIR, &s9pk.as_manifest().id, volume)),
|
||||
Bind::new(&host_volume_dir),
|
||||
vec![IdMap {
|
||||
from_id: 0,
|
||||
to_id: 100000,
|
||||
@@ -296,6 +321,7 @@ impl PersistentContainer {
|
||||
rpc_server: watch::channel(None).0,
|
||||
// procedures: Default::default(),
|
||||
js_mount,
|
||||
host_volume_binds,
|
||||
volumes,
|
||||
assets,
|
||||
images,
|
||||
@@ -439,6 +465,7 @@ impl PersistentContainer {
|
||||
let rpc_server = self.rpc_server.send_replace(None);
|
||||
let js_mount = self.js_mount.take();
|
||||
let volumes = std::mem::take(&mut self.volumes);
|
||||
let host_volume_binds = std::mem::take(&mut self.host_volume_binds);
|
||||
let assets = std::mem::take(&mut self.assets);
|
||||
let images = std::mem::take(&mut self.images);
|
||||
let subcontainers = self.subcontainers.clone();
|
||||
@@ -461,6 +488,11 @@ impl PersistentContainer {
|
||||
for (_, volume) in volumes {
|
||||
errs.handle(volume.unmount(true).await);
|
||||
}
|
||||
// Unmount host-side shared binds after the rootfs-side volume
|
||||
// mounts. Use delete_mountpoint=false to preserve the data dirs.
|
||||
for (_, host_bind) in host_volume_binds {
|
||||
errs.handle(host_bind.unmount(false).await);
|
||||
}
|
||||
for assets in assets {
|
||||
errs.handle(assets.unmount(true).await);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use imbl::vector;
|
||||
|
||||
use crate::context::RpcContext;
|
||||
use crate::db::model::package::{InstalledState, InstallingInfo, InstallingState, PackageState};
|
||||
use crate::disk::mount::util::{has_mounts_under, unmount_all_under};
|
||||
use crate::prelude::*;
|
||||
use crate::volume::PKG_VOLUME_DIR;
|
||||
use crate::{DATA_DIR, PACKAGE_DATA, PackageId};
|
||||
@@ -81,6 +82,22 @@ pub async fn cleanup(ctx: &RpcContext, id: &PackageId, soft: bool) -> Result<(),
|
||||
if !soft {
|
||||
let path = Path::new(DATA_DIR).join(PKG_VOLUME_DIR).join(&manifest.id);
|
||||
if tokio::fs::metadata(&path).await.is_ok() {
|
||||
// Best-effort cleanup of any propagated mounts (e.g. NAS)
|
||||
// that survived container destroy or were never cleaned up
|
||||
// (force-uninstall skips destroy entirely).
|
||||
unmount_all_under(&path, true).await.log_err();
|
||||
// Hard check: refuse to delete if mounts are still active,
|
||||
// to avoid traversing into a live NFS/NAS mount.
|
||||
if has_mounts_under(&path).await? {
|
||||
return Err(Error::new(
|
||||
eyre!(
|
||||
"Refusing to remove {}: active mounts remain under this path. \
|
||||
Unmount them manually and retry.",
|
||||
path.display()
|
||||
),
|
||||
ErrorKind::Filesystem,
|
||||
));
|
||||
}
|
||||
tokio::fs::remove_dir_all(&path).await?;
|
||||
}
|
||||
let logs_dir = Path::new(PACKAGE_DATA).join("logs").join(&manifest.id);
|
||||
|
||||
Reference in New Issue
Block a user