use std::collections::BTreeMap;
use gunnar_grpc::ControlPlane;
use gunnar_proto::{CapacityVerdict, PrincipalKind, SetGrowth};
pub mod wire {
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct ProductReport {
pub tenants: Vec<TenantOwner>,
pub usage: BTreeMap<String, Usage>,
pub servable: Option<Servable>,
pub metered: Option<MeteredDoor>,
pub room: BTreeMap<String, u64>,
pub promised: BTreeMap<String, BTreeMap<String, u64>>,
pub with_iron: Option<String>,
pub names: Vec<String>,
pub accounts: BTreeMap<String, AccountRoom>,
pub activity: BTreeMap<String, Activity>,
pub measured_unix_ms: u64,
}
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
pub struct ActivityDay {
pub day_unix_ms: u64,
pub meters: BTreeMap<String, u64>,
}
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
pub struct Activity {
pub days: Vec<ActivityDay>,
pub retained_days: u64,
}
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct AccountRoom {
pub unused_bytes: Option<u64>,
pub room_bytes: u64,
pub seats: u32,
pub seats_open: u32,
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct TenantOwner {
pub tenant: String,
pub person: bool,
pub seller: bool,
}
#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
pub struct Usage {
pub meters: BTreeMap<String, u64>,
pub measured_at_unix_ms: u64,
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum Growth {
Sealed(String),
OperatorOnly(String),
Unknown(String),
AtRestart(String),
AtRuntime(String),
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum Servable {
Measured {
servable_bytes: u64,
total_bytes: u64,
growth: Growth,
ceilings: BTreeMap<String, u64>,
},
Unmeasured(String),
}
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum MeteredDoor {
Enforcing(String),
Absent(String),
Unproven(String),
}
}
use wire::{Growth, MeteredDoor, ProductReport, Servable, TenantOwner, Usage};
pub const ACTIVITY_DAYS: u64 = 30;
pub const DATA_METERS: [&str; 3] = ["pack_bytes", "lfs_bytes", "cache_bytes"];
pub const RAM_BYTES_PER_OBJECT: u64 = 382;
const USAGE_PAGE: usize = 1000;
pub fn read(
control: &dyn ControlPlane,
promised: BTreeMap<String, BTreeMap<String, u64>>,
metered: MeteredDoor,
now_unix_ms: u64,
) -> ProductReport {
let mut names = std::collections::BTreeSet::new();
let tenants = match control.list_namespaces() {
Ok(namespaces) => namespaces
.into_iter()
.inspect(|n| {
names.insert(n.name.clone());
})
.filter(|n| n.owner_kind != PrincipalKind::Machine as i32)
.map(|n| TenantOwner {
person: n.owner_kind == PrincipalKind::User as i32,
seller: n.owner_server_grant,
tenant: n.name,
})
.collect(),
Err(e) => {
tracing::warn!(target: crate::repair::TARGET, "Report.product: the namespaces could not be listed: {e}");
Vec::new()
}
};
match control.list_principals() {
Ok(principals) => names.extend(
principals
.into_iter()
.flat_map(|p| [p.id, p.name])
.filter(|n| !n.is_empty()),
),
Err(e) => {
tracing::warn!(target: crate::repair::TARGET, "Report.product: the principals could not be listed: {e}")
}
}
let servable = servable_of(control);
let (room, with_iron) = room_of(&servable);
ProductReport {
tenants,
usage: usage_of(control, now_unix_ms),
servable: Some(servable),
metered: Some(metered),
room,
promised,
with_iron,
names: names.into_iter().collect(),
accounts: BTreeMap::new(),
activity: BTreeMap::new(),
measured_unix_ms: now_unix_ms,
}
}
pub fn activity_of(
control: &dyn ControlPlane,
report: &ProductReport,
now_unix_ms: u64,
) -> BTreeMap<String, wire::Activity> {
let since = now_unix_ms.saturating_sub(ACTIVITY_DAYS * 86_400_000) as i64;
report
.tenants
.iter()
.filter_map(|t| match control.activity(&t.tenant, since, 0) {
Ok((days, retained)) => Some((t.tenant.clone(), wire::Activity { days: days.iter().map(activity_day).collect(), retained_days: retained.max(0) as u64 })),
Err(e) => {
tracing::debug!(target: crate::repair::TARGET, "Report.product: {}'s activity: {e}", t.tenant);
None
}
})
.collect()
}
pub fn activity_day(d: &gunnar_proto::DayActivity) -> wire::ActivityDay {
let mut meters: BTreeMap<String, u64> = [
("bytes_in", d.bytes_in),
("bytes_out", d.bytes_out),
("pushes", d.pushes),
("fetches", d.fetches),
("lfs_bytes_in", d.lfs_bytes_in),
("lfs_bytes_out", d.lfs_bytes_out),
("refused", d.refused),
("auth_failures", d.auth_failures),
("distinct_principals", d.distinct_principals),
("last_push_unix_ms", d.last_push_unix_ms.max(0) as u64),
("last_fetch_unix_ms", d.last_fetch_unix_ms.max(0) as u64),
]
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect();
for (reason, count) in &d.refused_by_reason {
meters.insert(format!("refused_{reason}"), *count);
}
wire::ActivityDay {
day_unix_ms: d.day_unix_ms.max(0) as u64,
meters,
}
}
pub fn accounts_of(
control: &dyn ControlPlane,
stores: &crate::stores::ServerStores,
report: &ProductReport,
) -> BTreeMap<String, wire::AccountRoom> {
let room_bytes = gunnar_grpc::service::accounts::ROOM_PER_USER_BYTES;
report
.tenants
.iter()
.filter(|t| t.person)
.filter_map(|t| {
let cap = report.promised.get(&t.tenant).and_then(|c| c.get("pack_bytes")).copied().filter(|c| *c > 0);
let stored = report.usage.get(&t.tenant).and_then(|u| u.meters.get("pack_bytes")).copied().unwrap_or(0);
let ask = gunnar_grpc::EmployeeAsk { room_bytes, seats: 1, cap, stored, dry_run: true, ..Default::default() };
let answer = control.enroll_employee(&t.tenant, &ask).map_err(|e| tracing::debug!(target: crate::repair::TARGET, "Report.product: {}'s room: {e}", t.tenant)).ok()?;
let (seats, seats_open) = stores.auth().seats_of(&t.tenant).ok()?;
Some((t.tenant.clone(), wire::AccountRoom { unused_bytes: answer.unused_bytes, room_bytes, seats, seats_open }))
})
.collect()
}
fn usage_of(control: &dyn ControlPlane, now_unix_ms: u64) -> BTreeMap<String, Usage> {
let mut out = BTreeMap::new();
let mut token = String::new();
loop {
let page = match control.usage_page(&token, USAGE_PAGE, "") {
Ok(p) => p,
Err(e) => {
tracing::warn!(target: crate::repair::TARGET, "Report.product: usage could not be read: {e}");
return out;
}
};
for u in &page.usages {
out.insert(u.namespace.clone(), usage_from(u, now_unix_ms));
}
if page.next_page_token.is_empty() || page.next_page_token == token {
return out;
}
token = page.next_page_token;
}
}
pub fn usage_from(u: &gunnar_proto::NamespaceUsage, fallback_now: u64) -> Usage {
let meters = [
("pack_bytes", u.pack_bytes),
("cache_bytes", u.cache_bytes),
("cold_bytes", u.cold_bytes),
("lfs_bytes", u.lfs_bytes),
("tombstoned_bytes", u.tombstoned_bytes),
("repos", u.repos),
("objects", u.objects),
(
"open_store_ram_bytes",
u.objects.saturating_mul(RAM_BYTES_PER_OBJECT),
),
("bytes_in_today", u.bytes_in_today),
("bytes_out_today", u.bytes_out_today),
("pushes_today", u.pushes_today),
("fetches_today", u.fetches_today),
("refused_today", u.refused_today),
("last_push_unix_ms", u.last_push_unix_ms.max(0) as u64),
("last_fetch_unix_ms", u.last_fetch_unix_ms.max(0) as u64),
]
.into_iter()
.map(|(k, v)| (k.to_owned(), v))
.collect();
let measured = if u.measured_at_unix_ms > 0 {
u.measured_at_unix_ms as u64
} else {
fallback_now
};
Usage {
meters,
measured_at_unix_ms: measured,
}
}
fn servable_of(control: &dyn ControlPlane) -> Servable {
let mut answer = match control.capacity(&gunnar_proto::CapacityRequest::default()) {
Ok(a) => a,
Err(e) => {
return Servable::Unmeasured(format!(
"gunnar did not answer how much it can serve: {e}. THAT IS A MEASUREMENT THAT DID NOT HAPPEN, not a full disk"
))
}
};
if answer.growth() == SetGrowth::GrowthSealed
&& control.grow_status().is_ok_and(|s| s.engine_present)
{
answer.growth = SetGrowth::GrowthAtRuntime as i32;
}
match answer.verdict() {
CapacityVerdict::CapacityUnmeasured | CapacityVerdict::Unspecified => Servable::Unmeasured(format!(
"gunnar COULD NOT MEASURE its data set, so it has not said how much it can serve: {}. An unmeasured box has not said it is full and has not said it is empty",
answer.reason
)),
CapacityVerdict::CapacityAvailable | CapacityVerdict::CapacityExhausted => {
let ceilings = [("cpu_millicores", answer.cpu_millicores_total), ("ram_bytes", answer.ram_bytes_total)]
.into_iter()
.filter_map(|(m, v)| v.filter(|v| *v > 0).map(|v| (m.to_owned(), v)))
.collect();
Servable::Measured { servable_bytes: answer.servable_bytes, total_bytes: answer.total_bytes, growth: growth_from(answer.growth()), ceilings }
}
}
}
pub fn growth_from(growth: SetGrowth) -> Growth {
match growth {
SetGrowth::GrowthSealed => Growth::Sealed(
"AND THIS SET CANNOT GROW WHILE GUNNAR RUNS: this gunnar build has no runtime data-set growth (its grow door reports no grow-runtime engine) and is PID 1 with no shell, so a disk bought and attached to this box would be BILLED AND INVISIBLE. Serving more is an OPERATOR MAINTENANCE OPERATION that enlarges the set's existing volumes in place (or a fresh install onto a larger set), not another disk: nothing here buys or attaches one".into(),
),
SetGrowth::GrowthOperatorOnly => Growth::OperatorOnly(
"gunnar will not grow the set itself, but this box has a shell beside it: an operator can extend the filesystem out of band and gunnar will measure the larger one on its next look".into(),
),
SetGrowth::GrowthUnknown | SetGrowth::Unspecified => Growth::Unknown(
"gunnar could not tell whether this set can ever grow, and UNKNOWN is not a promise that it can — nothing should be bought for this box until it can be measured".into(),
),
SetGrowth::GrowthAtRestart => Growth::AtRestart(
"this set GROWS AT ITS NEXT RESTART once every member volume has been enlarged to one equal size: still no hot-plug, so a disk attached to this box is billed and invisible — the way to serve more is the data-set growth (monetize's outside actor enlarges the members in place), never another disk".into(),
),
SetGrowth::GrowthAtRuntime => Growth::AtRuntime(
"this set GROWS IN PLACE AT RUNTIME: gunnar drains, unmounts and grows into enlarged member volumes while its control plane stays up — serve more with the Data set panel (Quote, then Grow: monetize's outside actor enlarges the members in place, gunnar grows into them). Only an EXTRA disk attached beside the set would be billed and invisible, so never buy another disk".into(),
),
}
}
pub fn room_of(servable: &Servable) -> (BTreeMap<String, u64>, Option<String>) {
match servable {
Servable::Measured {
servable_bytes,
growth,
..
} => {
let room = DATA_METERS
.iter()
.map(|m| ((*m).to_owned(), *servable_bytes))
.collect();
let iron = match growth {
Growth::OperatorOnly(words) => Some(format!("A disk may be bought; {words}. Until an operator extends the filesystem onto it, the tenant's cap is not served")),
_ => None,
};
(room, iron)
}
Servable::Unmeasured(_) => (
DATA_METERS.iter().map(|m| ((*m).to_owned(), 0)).collect(),
None,
),
}
}