Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f38efc7096 | ||
|
|
2d0c225f98 |
@@ -27,12 +27,26 @@ pub struct HotConfig {
|
|||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct WarmConfig {
|
pub struct WarmConfig {
|
||||||
pub projects_path: PathBuf,
|
pub projects_path: PathBuf,
|
||||||
|
/// Dataset name for `zfs`/`zpool` operations against the warm
|
||||||
|
/// tier. The literal value `"none"` means this node's warm tier
|
||||||
|
/// is a plain directory, not ZFS-backed (e.g. a build node with
|
||||||
|
/// no ZFS pool) — snapshot/replicate become no-ops instead of
|
||||||
|
/// erroring on a missing `zfs`/`zpool` binary. See
|
||||||
|
/// [`WarmConfig::zfs_enabled`].
|
||||||
pub zfs_dataset: String,
|
pub zfs_dataset: String,
|
||||||
pub snapshot_retain_hours: u64,
|
pub snapshot_retain_hours: u64,
|
||||||
pub snapshot_retain_days: u64,
|
pub snapshot_retain_days: u64,
|
||||||
pub snapshot_retain_weeks: u64,
|
pub snapshot_retain_weeks: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl WarmConfig {
|
||||||
|
/// `false` when `zfs_dataset = "none"` — this node's warm tier
|
||||||
|
/// has no ZFS pool underneath it.
|
||||||
|
pub fn zfs_enabled(&self) -> bool {
|
||||||
|
self.zfs_dataset != "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct ColdConfig {
|
pub struct ColdConfig {
|
||||||
pub archive_path: PathBuf,
|
pub archive_path: PathBuf,
|
||||||
|
|||||||
@@ -188,6 +188,9 @@ pub async fn run(cfg: Config, mut manifest: Manifest) -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = snap_tick.tick() => {
|
_ = snap_tick.tick() => {
|
||||||
|
if !cfg.warm.zfs_enabled() {
|
||||||
|
tracing::debug!("skipping snapshot tick — zfs_dataset = \"none\" on this node");
|
||||||
|
} else {
|
||||||
let ts = chrono::Utc::now().format("%Y-%m-%d-%H%M").to_string();
|
let ts = chrono::Utc::now().format("%Y-%m-%d-%H%M").to_string();
|
||||||
tracing::info!("taking snapshot {}", ts);
|
tracing::info!("taking snapshot {}", ts);
|
||||||
if let Err(e) = snapshot::run_snapshot_cycle(
|
if let Err(e) = snapshot::run_snapshot_cycle(
|
||||||
@@ -199,8 +202,11 @@ pub async fn run(cfg: Config, mut manifest: Manifest) -> Result<()> {
|
|||||||
tracing::error!("snapshot failed: {:#}", e);
|
tracing::error!("snapshot failed: {:#}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
_ = repl_tick.tick() => {
|
_ = repl_tick.tick() => {
|
||||||
if let Some(rep) = &cfg.replication {
|
if !cfg.warm.zfs_enabled() {
|
||||||
|
tracing::debug!("skipping replication tick — zfs_dataset = \"none\" on this node");
|
||||||
|
} else if let Some(rep) = &cfg.replication {
|
||||||
if let (Some(host), Some(user), Some(dest)) = (
|
if let (Some(host), Some(user), Some(dest)) = (
|
||||||
&rep.send_to_host, &rep.send_to_user, &rep.cold_dataset_on_peer
|
&rep.send_to_host, &rep.send_to_user, &rep.cold_dataset_on_peer
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1502,6 +1502,10 @@ fn cmd_gc(cfg: &Config, manifest: &mut Manifest) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn cmd_snapshot(cfg: &Config, zfs: &SystemZfs) -> Result<()> {
|
fn cmd_snapshot(cfg: &Config, zfs: &SystemZfs) -> Result<()> {
|
||||||
|
if !cfg.warm.zfs_enabled() {
|
||||||
|
println!("Warm tier is not ZFS-backed on this node (zfs_dataset = \"none\") — nothing to snapshot.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
let ts = chrono::Utc::now().format("%Y-%m-%d-%H%M").to_string();
|
let ts = chrono::Utc::now().format("%Y-%m-%d-%H%M").to_string();
|
||||||
snapshot::run_snapshot_cycle(
|
snapshot::run_snapshot_cycle(
|
||||||
zfs, &cfg.warm.zfs_dataset, &ts,
|
zfs, &cfg.warm.zfs_dataset, &ts,
|
||||||
@@ -1514,6 +1518,10 @@ fn cmd_snapshot(cfg: &Config, zfs: &SystemZfs) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn cmd_list_snapshots(cfg: &Config, zfs: &SystemZfs, _project: &str) -> Result<()> {
|
fn cmd_list_snapshots(cfg: &Config, zfs: &SystemZfs, _project: &str) -> Result<()> {
|
||||||
|
if !cfg.warm.zfs_enabled() {
|
||||||
|
println!("Warm tier is not ZFS-backed on this node (zfs_dataset = \"none\").");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
let snaps = zfs.list_snapshots(&cfg.warm.zfs_dataset)?;
|
let snaps = zfs.list_snapshots(&cfg.warm.zfs_dataset)?;
|
||||||
if snaps.is_empty() { println!("No snapshots found."); return Ok(()); }
|
if snaps.is_empty() { println!("No snapshots found."); return Ok(()); }
|
||||||
for s in &snaps { println!(" {}", s); }
|
for s in &snaps { println!(" {}", s); }
|
||||||
@@ -1531,6 +1539,10 @@ fn cmd_restore(cfg: &Config, zfs: &SystemZfs, project: &str, snap: &str) -> Resu
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn cmd_replicate(cfg: &Config, zfs: &SystemZfs) -> Result<()> {
|
fn cmd_replicate(cfg: &Config, zfs: &SystemZfs) -> Result<()> {
|
||||||
|
if !cfg.warm.zfs_enabled() {
|
||||||
|
println!("Warm tier is not ZFS-backed on this node (zfs_dataset = \"none\") — nothing to replicate.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
let rep = cfg.replication.as_ref()
|
let rep = cfg.replication.as_ref()
|
||||||
.context("no replication config — this node does not replicate")?;
|
.context("no replication config — this node does not replicate")?;
|
||||||
let host = rep.send_to_host.as_ref().context("send_to_host not set")?;
|
let host = rep.send_to_host.as_ref().context("send_to_host not set")?;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function NodeCard({ node }) {
|
|||||||
border,
|
border,
|
||||||
].join(' '), children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: `inline-block w-2.5 h-2.5 rounded-full ${dot}` }), _jsx("span", { className: "text-lg font-semibold text-slate-100", children: node.node_name })] }), _jsx("span", { className: "text-xs text-slate-500 font-mono", children: node.zone || '—' })] }), !node.online && (_jsx("div", { className: "text-sm text-red-400 break-words", children: node.error ?? 'offline' })), node.online && (_jsxs(_Fragment, { children: [node.filesystem && (_jsx(StorageBar, { label: "disk", used: node.filesystem.used_bytes, total: node.filesystem.total_bytes })), node.hot && node.hot.max_bytes > 0 && (_jsx(StorageBar, { label: "hot tier", used: node.hot.used_bytes, total: node.hot.max_bytes, pinned: node.hot.pinned_bytes ?? undefined })), _jsxs("div", { className: "grid grid-cols-2 gap-y-1 text-sm", children: [_jsx("span", { className: "text-slate-500", children: "mount" }), _jsx("span", { className: "text-right font-mono text-xs", children: node.mount?.active ? (_jsx("span", { className: "text-emerald-300", children: "\u2713 mounted" })) : (_jsx("span", { className: "text-slate-500", children: "not mounted" })) }), _jsx("span", { className: "text-slate-500", children: "cache hit rate" }), _jsx("span", { className: "text-right font-mono text-xs", children: node.cache && node.cache.hits + node.cache.misses > 0
|
].join(' '), children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: `inline-block w-2.5 h-2.5 rounded-full ${dot}` }), _jsx("span", { className: "text-lg font-semibold text-slate-100", children: node.node_name })] }), _jsx("span", { className: "text-xs text-slate-500 font-mono", children: node.zone || '—' })] }), !node.online && (_jsx("div", { className: "text-sm text-red-400 break-words", children: node.error ?? 'offline' })), node.online && (_jsxs(_Fragment, { children: [node.filesystem && (_jsx(StorageBar, { label: "disk", used: node.filesystem.used_bytes, total: node.filesystem.total_bytes })), node.hot && node.hot.max_bytes > 0 && (_jsx(StorageBar, { label: "hot tier", used: node.hot.used_bytes, total: node.hot.max_bytes, pinned: node.hot.pinned_bytes ?? undefined })), _jsxs("div", { className: "grid grid-cols-2 gap-y-1 text-sm", children: [_jsx("span", { className: "text-slate-500", children: "mount" }), _jsx("span", { className: "text-right font-mono text-xs", children: node.mount?.active ? (_jsx("span", { className: "text-emerald-300", children: "\u2713 mounted" })) : (_jsx("span", { className: "text-slate-500", children: "not mounted" })) }), _jsx("span", { className: "text-slate-500", children: "cache hit rate" }), _jsx("span", { className: "text-right font-mono text-xs", children: node.cache && node.cache.hits + node.cache.misses > 0
|
||||||
? `${Math.round(node.cache.hit_rate * 100)}%`
|
? `${Math.round(node.cache.hit_rate * 100)}%`
|
||||||
: _jsx("span", { className: "text-slate-500", children: "idle" }) }), _jsx("span", { className: "text-slate-500", children: "next scheduled job" }), _jsx("span", { className: "text-right font-mono text-xs", children: nextTimer(node) })] })] }))] }) }));
|
: _jsx("span", { className: "text-slate-500", children: "idle" }) }), _jsx("span", { className: "text-slate-500", children: "next scheduled job" }), _jsx("span", { className: "text-right font-mono text-xs", children: nextTimer(node) })] })] })), _jsx("div", { className: "pt-1 border-t border-slate-800 text-xs text-slate-500", children: "view detail \u00B7 maintenance & shutdown prep \u2192" })] }) }));
|
||||||
}
|
}
|
||||||
function healthOf(n) {
|
function healthOf(n) {
|
||||||
if (!n.online)
|
if (!n.online)
|
||||||
|
|||||||
@@ -95,6 +95,10 @@ export function NodeCard({ node }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="pt-1 border-t border-slate-800 text-xs text-slate-500">
|
||||||
|
view detail · maintenance & shutdown prep →
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -211,14 +211,21 @@ step "flushing filesystem buffers"
|
|||||||
sync
|
sync
|
||||||
ok "sync complete"
|
ok "sync complete"
|
||||||
|
|
||||||
POOL=$(df --output=source /slab 2>/dev/null | tail -1 | tr -d '[:space:]')
|
step "zpool health check"
|
||||||
if [ -n "$POOL" ] && [ "$POOL" != "none" ]; then
|
if ! command -v zpool >/dev/null 2>&1; then
|
||||||
step "zpool health check ($POOL)"
|
ok "no zpool binary on this node — warm tier is not ZFS-backed here, nothing to check"
|
||||||
|
else
|
||||||
|
POOL=$(df --output=source /slab 2>/dev/null | tail -1 | tr -d '[:space:]')
|
||||||
|
if [ -n "$POOL" ] && [ "$POOL" != "none" ]; then
|
||||||
|
echo " pool: $POOL"
|
||||||
zpool status -x "$POOL" 2>&1 | sed 's/^/ /'
|
zpool status -x "$POOL" 2>&1 | sed 's/^/ /'
|
||||||
if [ "$EXPORT_ZPOOL" -eq 1 ]; then
|
if [ "$EXPORT_ZPOOL" -eq 1 ]; then
|
||||||
step "exporting $POOL (--export-zpool)"
|
step "exporting $POOL (--export-zpool)"
|
||||||
run sudo zpool export "$POOL" && ok "exported — remember: zpool import $POOL after reboot" || fail "export failed"
|
run sudo zpool export "$POOL" && ok "exported — remember: zpool import $POOL after reboot" || fail "export failed"
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
|
warn "zpool present but /slab isn't a recognizable ZFS mount"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
hr
|
hr
|
||||||
|
|||||||
Reference in New Issue
Block a user