Confine image preparation beneath retained storage authority
This commit is contained in:
parent
fbd332c31f
commit
c0b139256e
1 changed files with 694 additions and 103 deletions
|
|
@ -3,20 +3,27 @@
|
|||
use std::fmt;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io;
|
||||
use std::os::fd::AsFd;
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use lazaret_core::codec::{self, CodecError};
|
||||
use lazaret_core::identity::{PrepareOperationId, PreparedArtifactId};
|
||||
use lazaret_core::configuration::ConfinedPathPlan;
|
||||
use lazaret_core::identity::{MountIdentity, PrepareOperationId, PreparedArtifactId};
|
||||
use lazaret_core::records::{
|
||||
ArtifactLease, ArtifactState, PrepareOperation, PrepareStageState, PreparedArtifact,
|
||||
StorageCatalog, StorageCatalogMetadata,
|
||||
};
|
||||
use lazaret_linux::filesystem::{ExclusiveFileLock, try_exclusive_flock};
|
||||
use lazaret_linux::filesystem::{
|
||||
ExclusiveFileLock, mount_identity, require_mount_identity, try_exclusive_flock,
|
||||
};
|
||||
use redb::{
|
||||
Database, ReadTransaction, ReadableDatabase, ReadableTable, TableDefinition, WriteTransaction,
|
||||
};
|
||||
use rustix::fs::{Mode, OFlags, ResolveFlags, openat2};
|
||||
|
||||
const METADATA: TableDefinition<u8, &[u8]> = TableDefinition::new("catalog_metadata");
|
||||
const ARTIFACTS: TableDefinition<&[u8], &[u8]> = TableDefinition::new("prepared_artifacts");
|
||||
|
|
@ -26,23 +33,77 @@ const METADATA_KEY: u8 = 0;
|
|||
const DEFAULT_LOCK_WAIT: Duration = Duration::from_secs(5);
|
||||
const LOCK_RETRY_INTERVAL: Duration = Duration::from_millis(2);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone)]
|
||||
pub struct Catalog {
|
||||
database_path: PathBuf,
|
||||
lock_path: PathBuf,
|
||||
location: CatalogLocation,
|
||||
lock_wait: Duration,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum CatalogLocation {
|
||||
Ambient {
|
||||
database_path: PathBuf,
|
||||
lock_path: PathBuf,
|
||||
},
|
||||
StorageRoot(Arc<StorageRootCatalog>),
|
||||
}
|
||||
|
||||
struct StorageRootCatalog {
|
||||
root: File,
|
||||
root_label: PathBuf,
|
||||
identity: MountIdentity,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Catalog {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let location = match &self.location {
|
||||
CatalogLocation::Ambient { .. } => "ambient",
|
||||
CatalogLocation::StorageRoot(_) => "storage-root",
|
||||
};
|
||||
formatter
|
||||
.debug_struct("Catalog")
|
||||
.field("location", &location)
|
||||
.field("lock_wait", &self.lock_wait)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Catalog {
|
||||
#[must_use]
|
||||
pub fn new(database_path: impl Into<PathBuf>, lock_path: impl Into<PathBuf>) -> Self {
|
||||
Self {
|
||||
database_path: database_path.into(),
|
||||
lock_path: lock_path.into(),
|
||||
location: CatalogLocation::Ambient {
|
||||
database_path: database_path.into(),
|
||||
lock_path: lock_path.into(),
|
||||
},
|
||||
lock_wait: DEFAULT_LOCK_WAIT,
|
||||
}
|
||||
}
|
||||
|
||||
/// Binds the fixed catalog children to the validated Storage Root authority.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error when the exact Storage Root cannot be retained and identified.
|
||||
pub fn under_storage_root(storage_root: &ConfinedPathPlan) -> Result<Self, CatalogError> {
|
||||
let root = File::from(
|
||||
storage_root
|
||||
.open_authority()
|
||||
.map_err(|error| CatalogError::Io(io::Error::other(error.to_string())))?,
|
||||
);
|
||||
let identity = mount_identity(&root).map_err(CatalogError::Io)?;
|
||||
let root_label = PathBuf::from(std::ffi::OsString::from_vec(
|
||||
storage_root.canonical_label().as_bytes().to_vec(),
|
||||
));
|
||||
Ok(Self {
|
||||
location: CatalogLocation::StorageRoot(Arc::new(StorageRootCatalog {
|
||||
root,
|
||||
root_label,
|
||||
identity,
|
||||
})),
|
||||
lock_wait: DEFAULT_LOCK_WAIT,
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_lock_wait(mut self, lock_wait: Duration) -> Self {
|
||||
self.lock_wait = lock_wait;
|
||||
|
|
@ -56,10 +117,11 @@ impl Catalog {
|
|||
/// Returns an error if locking, creation, or the initialization transaction fails.
|
||||
pub fn initialize(&self) -> Result<(), CatalogError> {
|
||||
self.with_lock(|| {
|
||||
if self.database_path.exists() {
|
||||
if self.database_exists()? {
|
||||
return Err(CatalogError::AlreadyInitialized);
|
||||
}
|
||||
let database = Database::create(&self.database_path).map_err(CatalogError::redb)?;
|
||||
let database_path = self.database_path_for_handoff()?;
|
||||
let database = Database::create(database_path).map_err(CatalogError::redb)?;
|
||||
let transaction = database.begin_write().map_err(CatalogError::redb)?;
|
||||
{
|
||||
let mut metadata = transaction
|
||||
|
|
@ -538,7 +600,8 @@ impl Catalog {
|
|||
operation: impl FnOnce(&ReadTransaction) -> Result<T, CatalogError>,
|
||||
) -> Result<T, CatalogError> {
|
||||
self.with_lock(|| {
|
||||
let database = Database::open(&self.database_path).map_err(CatalogError::redb)?;
|
||||
let database_path = self.database_path_for_handoff()?;
|
||||
let database = Database::open(database_path).map_err(CatalogError::redb)?;
|
||||
let transaction = database.begin_read().map_err(CatalogError::redb)?;
|
||||
validate_read_metadata(&transaction)?;
|
||||
operation(&transaction)
|
||||
|
|
@ -550,7 +613,8 @@ impl Catalog {
|
|||
operation: impl FnOnce(&WriteTransaction) -> Result<T, CatalogError>,
|
||||
) -> Result<T, CatalogError> {
|
||||
self.with_lock(|| {
|
||||
let database = Database::open(&self.database_path).map_err(CatalogError::redb)?;
|
||||
let database_path = self.database_path_for_handoff()?;
|
||||
let database = Database::open(database_path).map_err(CatalogError::redb)?;
|
||||
let transaction = database.begin_write().map_err(CatalogError::redb)?;
|
||||
validate_write_metadata(&transaction)?;
|
||||
let result = operation(&transaction)?;
|
||||
|
|
@ -563,9 +627,43 @@ impl Catalog {
|
|||
&self,
|
||||
operation: impl FnOnce() -> Result<T, CatalogError>,
|
||||
) -> Result<T, CatalogError> {
|
||||
let _lock = acquire_lock(&self.lock_path, self.lock_wait)?;
|
||||
let _lock = match &self.location {
|
||||
CatalogLocation::Ambient { lock_path, .. } => acquire_lock(lock_path, self.lock_wait)?,
|
||||
CatalogLocation::StorageRoot(location) => {
|
||||
acquire_storage_root_lock(location, self.lock_wait)?
|
||||
}
|
||||
};
|
||||
operation()
|
||||
}
|
||||
|
||||
fn database_exists(&self) -> Result<bool, CatalogError> {
|
||||
match &self.location {
|
||||
CatalogLocation::Ambient { database_path, .. } => Ok(database_path.exists()),
|
||||
CatalogLocation::StorageRoot(location) => match open_storage_root_child(
|
||||
location,
|
||||
"catalog.redb",
|
||||
OFlags::RDONLY | OFlags::NOFOLLOW,
|
||||
Mode::empty(),
|
||||
) {
|
||||
Ok(_) => Ok(true),
|
||||
Err(CatalogError::Io(error)) if error.kind() == io::ErrorKind::NotFound => {
|
||||
Ok(false)
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn database_path_for_handoff(&self) -> Result<PathBuf, CatalogError> {
|
||||
match &self.location {
|
||||
CatalogLocation::Ambient { database_path, .. } => Ok(database_path.clone()),
|
||||
CatalogLocation::StorageRoot(location) => {
|
||||
require_mount_identity(&location.root_label, location.identity)
|
||||
.map_err(CatalogError::Io)?;
|
||||
Ok(location.root_label.join("catalog.redb"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn acquire_lock(path: &Path, wait: Duration) -> Result<ExclusiveFileLock, CatalogError> {
|
||||
|
|
@ -586,6 +684,56 @@ fn acquire_lock(path: &Path, wait: Duration) -> Result<ExclusiveFileLock, Catalo
|
|||
}
|
||||
}
|
||||
|
||||
fn acquire_storage_root_lock(
|
||||
location: &StorageRootCatalog,
|
||||
wait: Duration,
|
||||
) -> Result<ExclusiveFileLock, CatalogError> {
|
||||
let deadline = Instant::now() + wait;
|
||||
loop {
|
||||
let file = File::from(open_storage_root_child(
|
||||
location,
|
||||
"storage-catalog.lock",
|
||||
OFlags::CREATE | OFlags::RDWR | OFlags::NOFOLLOW,
|
||||
Mode::from_raw_mode(0o600),
|
||||
)?);
|
||||
match try_exclusive_flock(file) {
|
||||
Ok(lock) => return Ok(lock),
|
||||
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
|
||||
let now = Instant::now();
|
||||
if now >= deadline {
|
||||
return Err(CatalogError::LockTimeout);
|
||||
}
|
||||
thread::sleep(LOCK_RETRY_INTERVAL.min(deadline.saturating_duration_since(now)));
|
||||
}
|
||||
Err(error) => return Err(CatalogError::Io(error)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_storage_root_child(
|
||||
location: &StorageRootCatalog,
|
||||
name: &str,
|
||||
flags: OFlags,
|
||||
mode: Mode,
|
||||
) -> Result<std::os::fd::OwnedFd, CatalogError> {
|
||||
if mount_identity(&location.root).map_err(CatalogError::Io)? != location.identity {
|
||||
return Err(CatalogError::Io(io::Error::other(
|
||||
"retained Storage Root identity changed",
|
||||
)));
|
||||
}
|
||||
openat2(
|
||||
location.root.as_fd(),
|
||||
name,
|
||||
flags | OFlags::CLOEXEC,
|
||||
mode,
|
||||
ResolveFlags::BENEATH
|
||||
| ResolveFlags::NO_MAGICLINKS
|
||||
| ResolveFlags::NO_SYMLINKS
|
||||
| ResolveFlags::NO_XDEV,
|
||||
)
|
||||
.map_err(|error| CatalogError::Io(error.into()))
|
||||
}
|
||||
|
||||
fn open_lock(path: &Path) -> Result<File, CatalogError> {
|
||||
OpenOptions::new()
|
||||
.create(true)
|
||||
|
|
|
|||
Loading…
Reference in a new issue