mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-30 12:11:56 +00:00
Compare commits
1 Commits
st/port-la
...
fix/volume
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e7d33b07f |
@@ -61,6 +61,24 @@ pub async fn unmount<P: AsRef<Path>>(mountpoint: P, lazy: bool) -> Result<(), Er
|
|||||||
Ok(())
|
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
|
/// Unmounts all mountpoints under (and including) the given path, in reverse
|
||||||
/// depth order so that nested mounts are unmounted before their parents.
|
/// depth order so that nested mounts are unmounted before their parents.
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ use clap::builder::ValueParserFactory;
|
|||||||
use exver::VersionRange;
|
use exver::VersionRange;
|
||||||
use rust_i18n::t;
|
use rust_i18n::t;
|
||||||
|
|
||||||
|
use tokio::process::Command;
|
||||||
|
|
||||||
use crate::db::model::package::{
|
use crate::db::model::package::{
|
||||||
CurrentDependencies, CurrentDependencyInfo, CurrentDependencyKind, ManifestPreference,
|
CurrentDependencies, CurrentDependencyInfo, CurrentDependencyKind, ManifestPreference,
|
||||||
TaskEntry,
|
TaskEntry,
|
||||||
@@ -19,7 +21,7 @@ use crate::service::effects::callbacks::CallbackHandler;
|
|||||||
use crate::service::effects::prelude::*;
|
use crate::service::effects::prelude::*;
|
||||||
use crate::service::rpc::CallbackId;
|
use crate::service::rpc::CallbackId;
|
||||||
use crate::status::health_check::NamedHealthCheckResult;
|
use crate::status::health_check::NamedHealthCheckResult;
|
||||||
use crate::util::{FromStrParser, VersionString};
|
use crate::util::{FromStrParser, Invoke, VersionString};
|
||||||
use crate::volume::data_dir;
|
use crate::volume::data_dir;
|
||||||
use crate::{DATA_DIR, HealthCheckId, PackageId, ReplayId, VolumeId};
|
use crate::{DATA_DIR, HealthCheckId, PackageId, ReplayId, VolumeId};
|
||||||
|
|
||||||
@@ -90,7 +92,7 @@ pub async fn mount(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.mount(
|
.mount(
|
||||||
mountpoint,
|
&mountpoint,
|
||||||
if readonly {
|
if readonly {
|
||||||
MountType::ReadOnly
|
MountType::ReadOnly
|
||||||
} else {
|
} else {
|
||||||
@@ -99,6 +101,15 @@ pub async fn mount(
|
|||||||
)
|
)
|
||||||
.await?;
|
.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(())
|
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::overlayfs::OverlayGuard;
|
||||||
use crate::disk::mount::filesystem::{MountType, ReadOnly};
|
use crate::disk::mount::filesystem::{MountType, ReadOnly};
|
||||||
use crate::disk::mount::guard::{GenericMountGuard, MountGuard};
|
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::lxc::{HOST_RPC_SERVER_SOCKET, LxcConfig, LxcContainer};
|
||||||
use crate::net::net_controller::NetService;
|
use crate::net::net_controller::NetService;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
@@ -76,6 +77,7 @@ pub struct PersistentContainer {
|
|||||||
pub(super) rpc_client: UnixRpcClient,
|
pub(super) rpc_client: UnixRpcClient,
|
||||||
pub(super) rpc_server: watch::Sender<Option<(NonDetachingJoinHandle<()>, ShutdownHandle)>>,
|
pub(super) rpc_server: watch::Sender<Option<(NonDetachingJoinHandle<()>, ShutdownHandle)>>,
|
||||||
js_mount: MountGuard,
|
js_mount: MountGuard,
|
||||||
|
host_volume_binds: BTreeMap<VolumeId, MountGuard>,
|
||||||
volumes: BTreeMap<VolumeId, MountGuard>,
|
volumes: BTreeMap<VolumeId, MountGuard>,
|
||||||
assets: Vec<MountGuard>,
|
assets: Vec<MountGuard>,
|
||||||
pub(super) images: BTreeMap<ImageId, Arc<MountGuard>>,
|
pub(super) images: BTreeMap<ImageId, Arc<MountGuard>>,
|
||||||
@@ -120,6 +122,7 @@ impl PersistentContainer {
|
|||||||
.is_ok();
|
.is_ok();
|
||||||
|
|
||||||
let mut volumes = BTreeMap::new();
|
let mut volumes = BTreeMap::new();
|
||||||
|
let mut host_volume_binds = BTreeMap::new();
|
||||||
|
|
||||||
// TODO: remove once packages are reconverted
|
// TODO: remove once packages are reconverted
|
||||||
let added = if is_compat {
|
let added = if is_compat {
|
||||||
@@ -128,13 +131,35 @@ impl PersistentContainer {
|
|||||||
BTreeSet::default()
|
BTreeSet::default()
|
||||||
};
|
};
|
||||||
for volume in s9pk.as_manifest().volumes.union(&added) {
|
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
|
let mountpoint = lxc_container
|
||||||
.rootfs_dir()
|
.rootfs_dir()
|
||||||
.join("media/startos/volumes")
|
.join("media/startos/volumes")
|
||||||
.join(volume);
|
.join(volume);
|
||||||
let mount = MountGuard::mount(
|
let mount = MountGuard::mount(
|
||||||
&IdMapped::new(
|
&IdMapped::new(
|
||||||
Bind::new(data_dir(DATA_DIR, &s9pk.as_manifest().id, volume)),
|
Bind::new(&host_volume_dir),
|
||||||
vec![IdMap {
|
vec![IdMap {
|
||||||
from_id: 0,
|
from_id: 0,
|
||||||
to_id: 100000,
|
to_id: 100000,
|
||||||
@@ -296,6 +321,7 @@ impl PersistentContainer {
|
|||||||
rpc_server: watch::channel(None).0,
|
rpc_server: watch::channel(None).0,
|
||||||
// procedures: Default::default(),
|
// procedures: Default::default(),
|
||||||
js_mount,
|
js_mount,
|
||||||
|
host_volume_binds,
|
||||||
volumes,
|
volumes,
|
||||||
assets,
|
assets,
|
||||||
images,
|
images,
|
||||||
@@ -439,6 +465,7 @@ impl PersistentContainer {
|
|||||||
let rpc_server = self.rpc_server.send_replace(None);
|
let rpc_server = self.rpc_server.send_replace(None);
|
||||||
let js_mount = self.js_mount.take();
|
let js_mount = self.js_mount.take();
|
||||||
let volumes = std::mem::take(&mut self.volumes);
|
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 assets = std::mem::take(&mut self.assets);
|
||||||
let images = std::mem::take(&mut self.images);
|
let images = std::mem::take(&mut self.images);
|
||||||
let subcontainers = self.subcontainers.clone();
|
let subcontainers = self.subcontainers.clone();
|
||||||
@@ -461,6 +488,11 @@ impl PersistentContainer {
|
|||||||
for (_, volume) in volumes {
|
for (_, volume) in volumes {
|
||||||
errs.handle(volume.unmount(true).await);
|
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 {
|
for assets in assets {
|
||||||
errs.handle(assets.unmount(true).await);
|
errs.handle(assets.unmount(true).await);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use imbl::vector;
|
|||||||
|
|
||||||
use crate::context::RpcContext;
|
use crate::context::RpcContext;
|
||||||
use crate::db::model::package::{InstalledState, InstallingInfo, InstallingState, PackageState};
|
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::prelude::*;
|
||||||
use crate::volume::PKG_VOLUME_DIR;
|
use crate::volume::PKG_VOLUME_DIR;
|
||||||
use crate::{DATA_DIR, PACKAGE_DATA, PackageId};
|
use crate::{DATA_DIR, PACKAGE_DATA, PackageId};
|
||||||
@@ -81,6 +82,22 @@ pub async fn cleanup(ctx: &RpcContext, id: &PackageId, soft: bool) -> Result<(),
|
|||||||
if !soft {
|
if !soft {
|
||||||
let path = Path::new(DATA_DIR).join(PKG_VOLUME_DIR).join(&manifest.id);
|
let path = Path::new(DATA_DIR).join(PKG_VOLUME_DIR).join(&manifest.id);
|
||||||
if tokio::fs::metadata(&path).await.is_ok() {
|
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?;
|
tokio::fs::remove_dir_all(&path).await?;
|
||||||
}
|
}
|
||||||
let logs_dir = Path::new(PACKAGE_DATA).join("logs").join(&manifest.id);
|
let logs_dir = Path::new(PACKAGE_DATA).join("logs").join(&manifest.id);
|
||||||
|
|||||||
Reference in New Issue
Block a user