manta-cli 2.0.0-beta.63

Another CLI for ALPS
//! Clap definitions for `manta get *` subcommands.
//!
//! Builds the `manta get` read-only subtree: groups, hardware,
//! sessions, configurations, BOS templates, node details, images, and
//! boot/kernel parameters. Execution dispatched in
//! `crate::dispatch::get`.
//!
//! Notes:
//! - Several listing subcommands use mutually-exclusive `ArgGroup`s
//!   (eg. `hsm-group_or_configuration`, `session_limit`,
//!   `configuration_safety_filter`, `image_safety_filter`,
//!   `hsm-group_or_xnames_or_name`) so that conflicting filters error
//!   at parse time.
//! - `subcommand_get_hardware_nodes` is `pub` because the dispatch
//!   layer's unit tests reuse the production builder.

use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
use clap::{ArgAction, ArgGroup, Command, arg, value_parser};

use super::HOSTLIST_HELP;

/// `manta get groups` — list/look-up node groups. Handler:
/// `crate::dispatch::get::group`.
pub fn subcommand_get_group() -> Command {
  Command::new("groups")
    .about("List node groups visible to your token (or look up one by name)")
    // ID preserved as "VALUE" for handler compatibility
    .arg(
      arg!(<VALUE> "Group name (lists all groups if omitted)")
        .value_name("GROUP_NAME")
        .required(false),
    )
    .arg(
      arg!(-o --output <FORMAT> "Output format")
        .value_parser(["json", "table"])
        .default_value("table"),
    )
}

/// Subcommand `manta get hardware nodes`. Extracted so unit tests in
/// [`crate::dispatch::get::hardware_nodes`] can reuse the production
/// builder instead of mirroring it by hand.
pub fn subcommand_get_hardware_nodes() -> Command {
  Command::new("nodes")
    .arg_required_else_help(true)
    .about("Show hardware inventory for a set of nodes")
    // ID preserved as "VALUE" for handler compatibility
    .arg(arg!(<VALUE>).value_name("NODES").help(HOSTLIST_HELP))
    .arg(
      arg!(-o --output <FORMAT> "Output format")
        .value_parser(["table", "json"])
        .default_value("table"),
    )
}

/// `manta get hardware` — parent of `nodes` and `group` hardware
/// inspection subcommands. Handler:
/// `crate::dispatch::get::hardware`.
pub fn subcommand_get_hardware() -> Command {
  Command::new("hardware")
    .arg_required_else_help(true)
    .about("Inspect hardware components")
    .subcommand(subcommand_get_hardware_nodes())
    .subcommand(subcommand_get_hardware_group())
}

/// `manta get hardware group` — hardware inventory for every node in
/// a group. Handler: `crate::dispatch::get::hardware_group`.
pub fn subcommand_get_hardware_group() -> Command {
  Command::new("group")
    .arg_required_else_help(true)
    .about("Show hardware inventory for a group")
    .arg(arg!(<GROUP_NAME> "Group name").required(true))
    .arg(
      arg!(-o --output <FORMAT> "Output format")
        .value_parser(["json", "summary", "details", "pattern"])
        .default_value("summary"),
    )
}

/// `manta get configurations` — list CFS configurations. Handler:
/// `crate::dispatch::get::configuration`.
pub fn subcommand_get_cfs_configuration() -> Command {
  Command::new("configurations")
    .about("List CFS configurations (filter by name, glob, group, or recency)")
    .arg(arg!(-n --name <NAME> "Show only the configuration with this exact name"))
    .arg(arg!(-p --pattern <PATTERN> "Glob pattern to filter by name (eg: 'my-cfg*')"))
    .arg(arg!(-m --"most-recent" "Return only the most recent (equivalent to --limit 1)"))
    .arg(
      arg!(-l --limit <VALUE> "Return only the <VALUE> most recent configurations")
        .value_parser(value_parser!(u8).range(1..)),
    )
    .arg(arg!(-o --output <FORMAT> "Output format").value_parser(["json"]))
    .arg(
      arg!(-H --group <GROUP_NAME> "Show only configurations whose layers target this group")
        .visible_alias("hsm-group"),
    )
    .arg(
      arg!(--"only-safe-to-delete" "Show only configurations that are safe to delete"),
    )
    .arg(
      arg!(--"only-unsafe-to-delete" "Show only configurations that are NOT safe to delete (in use)"),
    )
    .group(ArgGroup::new("hsm-group_or_configuration").args(["group", "name"]))
    .group(ArgGroup::new("configuration_limit").args(["most-recent", "limit"]))
    .group(ArgGroup::new("configuration_safety_filter").args([
      "only-safe-to-delete",
      "only-unsafe-to-delete",
    ]))
}

/// `manta get sessions` — list CFS sessions. Handler:
/// `crate::dispatch::get::session`.
pub fn subcommand_get_cfs_session() -> Command {
  Command::new("sessions")
    .about("List configuration sessions")
    .arg(arg!(-n --name <SESSION_NAME> "Return only the session with this name"))
    .arg(arg!(-a --"min-age" <VALUE> "Return only sessions older than this age (eg: '1d', '6h')"))
    .arg(arg!(-A --"max-age" <VALUE> "Return only sessions younger than this age (eg: '1d', '6h')"))
    .arg(
      arg!(-t --type <VALUE> "Return only sessions of this type")
        .value_parser(["image", "runtime"]),
    )
    .arg(
      arg!(-s --status <VALUE> "Return only sessions with this status")
        .value_parser(["pending", "running", "complete"]),
    )
    .arg(arg!(-m --"most-recent" "Return only the most recent session (equivalent to --limit 1)"))
    .arg(
      arg!(-l --limit <VALUE> "Return only the <VALUE> most recent sessions")
        .value_parser(value_parser!(u8).range(1..)),
    )
    .arg(arg!(-o --output <FORMAT> "Output format").value_parser(["json"]))
    .arg(arg!(-x --xnames <NODES> "Xnames, NIDs, or hostlist expression. Returns sessions targeting these nodes or their groups"))
    .arg(arg!(-H --group <GROUP_NAME> "Node group name. Returns sessions targeting this group or its members").visible_alias("hsm-group"))
    .group(ArgGroup::new("hsm-group_or_xnames_or_name").args([
      "group",
      "xnames",
      "name",
    ]))
    .group(ArgGroup::new("session_limit").args(["most-recent", "limit"]))
}

/// `manta get templates` — list BOS session templates. Handler:
/// `crate::dispatch::get::template`.
pub fn subcommand_get_bos_template() -> Command {
  Command::new("templates")
    .about("List BOS session templates (filter by name or group; sorted by name)")
    .arg(arg!(-n --name <NAME> "Show only the template with this exact name"))
    // BOS templates have no timestamp, so these cap the count but do not
    // select by recency (unlike the same flags on images/configs/sessions).
    .arg(arg!(-m --"most-recent" "Return only a single template (equivalent to --limit 1)"))
    .arg(
      arg!(-l --limit <VALUE> "Return at most <VALUE> templates")
        .value_parser(value_parser!(u8).range(1..)),
    )
    .arg(
      arg!(-H --group <GROUP_NAME> "Show only templates whose boot sets target this group")
        .visible_alias("hsm-group"),
    )
    .arg(
      arg!(-o --output <FORMAT> "Output format")
        .value_parser(["json", "table"])
        .default_value("table"),
    )
    .group(ArgGroup::new("hsm-group_or_template").args(["group", "name"]))
}

/// `manta get group-nodes` — node details/status for every member of
/// a group. Handler: `crate::dispatch::get::group_nodes`.
pub fn subcommand_get_group_nodes() -> Command {
  Command::new("group-nodes")
    .about("Show node details and status for a group")
    .arg(arg!(-n --"nids-only-one-line" "Print NIDs on a single line").action(ArgAction::SetTrue))
    .arg(arg!(-x --"xnames-only-one-line" "Print xnames on a single line").action(ArgAction::SetTrue))
    .arg(
      arg!(-s --status <VALUE> "Filter nodes by status")
        .value_parser(["OFF", "ON", "READY", "STANDBY", "PENDING", "FAILED", "CONFIGURED"]),
    )
    .arg(
      arg!(-T --"summary-status" "Show a group status summary:\n\
        OK          — all nodes booted and configured\n\
        OFF         — at least one node is OFF\n\
        ON          — no nodes OFF, at least one is ON\n\
        STANDBY     — at least one node's heartbeat is lost\n\
        UNCONFIGURED — all nodes READY but at least one is still being configured\n\
        FAILED      — at least one node's configuration failed")
        .action(ArgAction::SetTrue),
    )
    .arg(
      arg!(-o --output <FORMAT> "Output format")
        .value_parser(["table", "table-wide", "json", "summary"])
        .default_value("table"),
    )
    .arg_required_else_help(true)
    // ID preserved as "HSM_GROUP_NAME" for handler compatibility
    .arg(arg!(<HSM_GROUP_NAME> "Group name").value_name("GROUP_NAME"))
}

/// `manta get nodes` — node details/status by xname, NID, or hostlist
/// expression. Handler: `crate::dispatch::get::node_details`.
pub fn subcommand_get_node_details() -> Command {
  Command::new("nodes")
    .about("Show node details and status")
    .arg(arg!(-n --"nids-only-one-line" "Print NIDs on a single line"))
    .arg(
      arg!(-s --status <VALUE> "Filter nodes by status")
        .value_parser(["OFF", "ON", "READY", "STANDBY", "PENDING", "FAILED", "CONFIGURED"]),
    )
    .arg(
      arg!(-T --"summary-status" "Show a node status summary:\n\
        OK          — all nodes booted and configured\n\
        OFF         — at least one node is OFF\n\
        ON          — no nodes OFF, at least one is ON\n\
        STANDBY     — at least one node's heartbeat is lost\n\
        UNCONFIGURED — all nodes READY but at least one is still being configured\n\
        FAILED      — at least one node's configuration failed")
        .action(ArgAction::SetTrue),
    )
    .arg(
      arg!(-S --"include-siblings" "Also show sibling nodes that share a power supply with the requested nodes")
        .action(ArgAction::SetTrue),
    )
    .arg(
      arg!(-o --output <FORMAT> "Output format")
        .value_parser(["table", "table-wide", "json", "summary"])
        .default_value("table"),
    )
    .arg_required_else_help(true)
    // ID preserved as "VALUE" for handler compatibility
    .arg(arg!(<VALUE>).value_name("NODES").help(HOSTLIST_HELP))
}

/// Parse a `--since` / `--until` bound: a full `YYYY-MM-DDTHH:MM:SS`
/// timestamp is taken as-is; a bare `YYYY-MM-DD` date is completed with
/// `bare_time` so the named day is covered as the user intends.
///
/// Used behind [`parse_since`] / [`parse_until`] as clap
/// `value_parser`s, so a malformed date fails at parse time with both
/// accepted shapes named rather than surfacing as a server-side 400.
fn parse_filter_date(
  raw: &str,
  bare_time: NaiveTime,
) -> Result<NaiveDateTime, String> {
  if let Ok(v) = NaiveDateTime::parse_from_str(raw, "%Y-%m-%dT%H:%M:%S") {
    return Ok(v);
  }
  NaiveDate::parse_from_str(raw, "%Y-%m-%d")
    .map(|d| d.and_time(bare_time))
    .map_err(|_| {
      format!(
        "invalid date '{raw}': expected YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS"
      )
    })
}

/// `--since` value parser: a bare date starts at midnight, so
/// `--since 2026-01-01` includes all of January 1st.
fn parse_since(raw: &str) -> Result<NaiveDateTime, String> {
  parse_filter_date(raw, NaiveTime::MIN)
}

/// `--until` value parser: a bare date covers the whole named day, so
/// it completes to `23:59:59` rather than midnight. Otherwise
/// `--until 2026-01-01` (an inclusive `created <= bound` filter) would
/// exclude everything created after `00:00:00` that day.
///
/// Second precision, not sub-second: the wire carries
/// `%Y-%m-%dT%H:%M:%S` and the server parses the same, so any fraction
/// would be truncated in transit anyway.
fn parse_until(raw: &str) -> Result<NaiveDateTime, String> {
  let end_of_day =
    NaiveTime::from_hms_opt(23, 59, 59).expect("23:59:59 is a valid time");
  parse_filter_date(raw, end_of_day)
}

/// `manta get images` — list IMS images. Handler:
/// `crate::dispatch::get::image`.
pub fn subcommand_get_images() -> Command {
  Command::new("images")
    .about("List IMS images (filter by id, name glob, creation date, or recency; oldest first, newest last)")
    .arg(arg!(-i --id <IMAGE_ID> "Show only the image with this exact ID"))
    .arg(arg!(-p --pattern <PATTERN> "Glob matched against image name (e.g. 'compute-*'); applied server-side. Invalid glob returns 400."))
    .arg(
      arg!(-s --since <DATE> "Show only images created at or after this date. A bare YYYY-MM-DD starts at midnight; a full YYYY-MM-DDTHH:MM:SS is exact")
        .value_parser(parse_since),
    )
    .arg(
      arg!(-u --until <DATE> "Show only images created at or before this date. A bare YYYY-MM-DD covers the whole day; a full YYYY-MM-DDTHH:MM:SS is exact")
        .value_parser(parse_until),
    )
    .arg(arg!(-m --"most-recent" "Return only the most recent (equivalent to --limit 1)"))
    .arg(
      arg!(-l --limit <VALUE> "Return only the <VALUE> most recent images")
        .value_parser(value_parser!(u8).range(1..)),
    )
    .arg(
      arg!(--"only-safe-to-delete" "Show only images that are safe to delete"),
    )
    .arg(
      arg!(--"only-unsafe-to-delete" "Show only images that are NOT safe to delete (currently used as a node's boot image)"),
    )
    .group(ArgGroup::new("image_safety_filter").args([
      "only-safe-to-delete",
      "only-unsafe-to-delete",
    ]))
}

/// `manta get boot-parameters` — show BSS boot parameters for nodes
/// or a group. Handler:
/// `crate::dispatch::get::boot_parameters`.
pub fn subcommand_get_boot_parameters() -> Command {
  Command::new("boot-parameters")
    .arg_required_else_help(true)
    .about("Show the BSS boot parameters (kernel, initrd, params) for nodes or a group")
    .arg(
      arg!(-H --group <GROUP_NAME> "Show boot parameters for every node in this group")
        .visible_alias("hsm-group"),
    )
    .arg(arg!(-n --nodes <NODES>).help(HOSTLIST_HELP))
}

/// `manta get kernel-parameters` — show kernel parameters for nodes
/// or a group. Required `ArgGroup("hsm-group_or_nodes")` enforces
/// exactly one selector. Handler:
/// `crate::dispatch::get::kernel_parameters`.
pub fn subcommand_get_kernel_parameters() -> Command {
  Command::new("kernel-parameters")
    .about("Show kernel parameters for nodes or a group")
    .arg(arg!(-n --nodes <NODES>).help(HOSTLIST_HELP))
    .arg(arg!(-H --group <GROUP_NAME> "Show kernel parameters for all nodes in this group").visible_alias("hsm-group"))
    .arg(
      arg!(-f --filter <VALUE> "Comma-separated list of parameter names to show.\neg: 'console,bad_page,crashkernel,hugepagelist,root'"),
    )
    .arg(
      arg!(-o --output <FORMAT> "Output format")
        .value_parser(["table", "json"])
        .default_value("table"),
    )
    .group(
      ArgGroup::new("hsm-group_or_nodes")
        .args(["group", "nodes"])
        .required(true),
    )
}

/// `manta get redfish-endpoints` — list registered BMCs/controllers.
/// Handler: `crate::dispatch::get::redfish_endpoint`.
pub fn subcommand_get_redfish_endpoints() -> Command {
  Command::new("redfish-endpoints")
    .about("List the BMCs / controllers the hardware state manager has registered as Redfish endpoints")
    .arg(arg!(-i --id <VALUE> "Filter by xname (can be specified multiple times)"))
    .arg(arg!(-f --fqdn <VALUE> "Filter by FQDN"))
    .arg(arg!(-u --uuid <VALUE> "Filter by UUID"))
    .arg(arg!(-m --macaddr <VALUE> "Filter by MAC address"))
    .arg(
      arg!(-I --ipaddress <VALUE> "Filter by IP address (empty string matches endpoints without an IP)"),
    )
    .arg(
      arg!(-o --output <FORMAT> "Output format")
        .value_parser(["table", "json"])
        .default_value("table"),
    )
}

/// Top-level `manta get` verb — wires every `get <noun>` subcommand
/// together. Invoked from `build_cli` in `super::mod`.
pub fn subcommand_get() -> Command {
  Command::new("get")
    .arg_required_else_help(true)
    .about("Inspect groups, nodes, hardware, images, configurations, sessions, templates, and boot/kernel parameters")
    .subcommand(subcommand_get_group())
    .subcommand(subcommand_get_hardware())
    .subcommand(subcommand_get_cfs_session())
    .subcommand(subcommand_get_cfs_configuration())
    .subcommand(subcommand_get_bos_template())
    .subcommand(subcommand_get_group_nodes())
    .subcommand(subcommand_get_node_details())
    .subcommand(subcommand_get_images())
    .subcommand(subcommand_get_boot_parameters())
    .subcommand(subcommand_get_kernel_parameters())
    .subcommand(subcommand_get_redfish_endpoints())
}