use clap::{ArgAction, ArgGroup, Command, ValueHint, arg, value_parser};
use std::path::PathBuf;
use super::{HOSTLIST_HELP, output_flag, output_flag_long_only};
fn add_apply_hw_group_args(cmd: Command) -> Command {
cmd
.arg_required_else_help(true)
.arg(
arg!(-P -- pattern <PATTERN> "Hardware pattern: <component>:<qty>[:<component>:<qty>...].\neg: 'a100:12:epyc:5'")
.required(true),
)
.arg(
arg!(-t --"target-group" <TARGET_GROUP_NAME> "Group to rescale")
.required(true)
.visible_alias("target-cluster"),
)
.arg(
arg!(-p --"parent-group" <PARENT_GROUP_NAME> "Group that donates or receives the redistributed nodes")
.required(true)
.visible_alias("parent-cluster"),
)
.arg(arg!(-d --"dry-run" "Simulate the operation without making changes").action(ArgAction::SetTrue))
.arg(
arg!(-c --"create-target-group" "Create the target group if it does not exist")
.visible_alias("create-target-hsm-group"),
)
.arg(
arg!(-D --"delete-empty-parent-group" "Delete the parent group if empty after this operation")
.visible_alias("delete-empty-parent-hsm-group"),
)
.arg(arg!(-u --"unpin-nodes" "Allow any available nodes to be selected"))
.arg(output_flag())
}
pub fn subcommand_apply_hw_configuration() -> Command {
Command::new("hardware")
.about("[experimental] Rescale a group's hardware allocation")
.arg_required_else_help(true)
.subcommand(
add_apply_hw_group_args(Command::new("group"))
.about("[experimental] Rescale a group's hardware allocation")
.long_about(
"[experimental] Upscale or downscale a group by specifying a hardware component pattern.\n\n\
If the group does not exist it will be created; otherwise its node assignment is updated.\n\n\
Pattern format: <component>:<quantity>[:<component>:<quantity>...]\n\
eg: 'a100:12:epyc:5' — assign nodes with 12 A100 GPUs and 5 EPYC CPUs total",
),
)
}
pub fn subcommand_apply_template() -> Command {
Command::new("template")
.arg_required_else_help(true)
.about("Boot nodes using an existing session template")
.arg(arg!(-n --name <VALUE> "Name of the boot session to create"))
.arg(
arg!(-o --operation <VALUE> "Boot operation to perform")
.value_parser(["reboot", "boot", "shutdown"])
.default_value("reboot"),
)
.arg(arg!(-t --template <VALUE> "Session template name").required(true))
.arg(
arg!(-l --limit <VALUE>
"Limit to specific nodes, groups, or roles (OR by default; prefix with '&' for AND or '!' for NOT)")
.required(true),
)
.arg(
arg!(-i --"include-disabled" "Include nodes marked as disabled in the hardware state manager")
.action(ArgAction::SetTrue),
)
.arg(arg!(-d --"dry-run" "Simulate the operation without making changes").action(ArgAction::SetTrue))
}
pub fn subcommand_apply_ephemeral_environment() -> Command {
Command::new("ephemeral-environment")
.arg_required_else_help(true)
.about("Launch an ephemeral SSH environment from an image")
.long_about(
"Launch an ephemeral SSH environment from an image.\n\n\
Returns an SSH hostname once the environment is ready (usually within a few seconds).",
)
.arg(arg!(-i --"image-id" <IMAGE_ID> "Image ID to use").required(true))
}
pub fn subcommand_apply_sat_file() -> Command {
Command::new("sat-file")
.arg_required_else_help(true)
.about("Process a SAT file to create configurations, images, and session templates")
.long_about(
"Process a SAT file containing up to three sections:\n\
\n\
- `configurations`: configurations to create\n\
- `images`: images to build from those configurations\n\
- `session_templates`: session templates to create\n\
\n\
Use --image-only to process only configurations and images.\n\
Use --sessiontemplate-only to process only configurations and session templates.",
)
.arg(
arg!(-t --"sat-template-file" <FILE> "SAT file path (may be a jinja2 template)")
.value_parser(value_parser!(PathBuf))
.required(true)
.value_hint(ValueHint::FilePath),
)
.arg(
arg!(-f --"values-file" <FILE> "Values file to expand jinja2 variables in the SAT file")
.value_parser(value_parser!(PathBuf))
.value_hint(ValueHint::FilePath),
)
.arg(arg!(-V --"values" <VALUE> ... "Inline values to expand jinja2 variables (overrides --values-file)"))
.arg(arg!(--"reboot" "Reboot nodes after applying session templates").action(ArgAction::SetTrue))
.arg(
arg!(-v --"ansible-verbosity" <VALUE> "Ansible verbosity level (1 = -v, 2 = -vv, …, max 4)")
.value_parser(["1", "2", "3", "4"])
.num_args(1)
.default_value("2")
.default_missing_value("2"),
)
.arg(
arg!(-P --"ansible-passthrough" <VALUE>
"Additional Ansible flags (limited to --extra-vars, --forks, --skip-tags, --start-at-task, --tags)")
.allow_hyphen_values(true),
)
.arg(
arg!(-o --"overwrite-configuration" "Overwrite an existing configuration with the same name")
.action(ArgAction::SetTrue),
)
.arg(arg!(-w --"watch-logs" "Stream session logs to stdout").action(ArgAction::SetTrue))
.arg(arg!(-T --timestamps "Show log timestamps").action(ArgAction::SetTrue))
.arg(
arg!(-i --"image-only" "Process only the `configurations` and `images` sections")
.action(ArgAction::SetTrue),
)
.arg(
arg!(-s --"sessiontemplate-only" "Process only the `configurations` and `session_templates` sections")
.action(ArgAction::SetTrue),
)
.arg(arg!(-p --"pre-hook" <SCRIPT> "Command to run before processing.\neg: --pre-hook \"echo hello\""))
.arg(arg!(-a --"post-hook" <SCRIPT> "Command to run after successful processing.\neg: --post-hook \"echo hello\""))
.arg(arg!(-y --"assume-yes" "Skip confirmation prompts").action(ArgAction::SetTrue))
.arg(arg!(-d --"dry-run" "Simulate the operation without making changes").action(ArgAction::SetTrue))
.arg(output_flag_long_only())
}
pub fn subcommand_apply_boot_nodes() -> Command {
Command::new("nodes")
.arg_required_else_help(true)
.about("Update boot parameters for a set of nodes")
.long_about(
"Update the boot parameters (image, runtime configuration, and kernel parameters) for a set of nodes.\n\n\
The boot image can be specified by image ID or by the configuration name used to build it \
(the most recent matching image is used).\n\n\
eg:\n \
manta apply boot nodes \\\n \
--boot-image-configuration <config-name> \\\n \
--runtime-configuration <config-name> <nodes>",
)
.arg(arg!(-i --"boot-image" <IMAGE_ID> "Image ID to boot the nodes"))
.arg(
arg!(-b --"boot-image-configuration" <NAME>
"Configuration name used to build the boot image (uses the most recent matching image)"),
)
.arg(arg!(-r --"runtime-configuration" <NAME> "Configuration to apply to nodes after booting"))
.arg(arg!(-k --"kernel-parameters" <VALUE> "Kernel parameters to assign to the nodes"))
.arg(arg!(-d --"dry-run" "Simulate the operation without making changes").action(ArgAction::SetTrue))
.group(
ArgGroup::new("boot-image_or_boot-config")
.args(["boot-image", "boot-image-configuration"]),
)
.arg(arg!(<VALUE>).value_name("NODES").help(HOSTLIST_HELP))
.arg(output_flag())
}
fn add_apply_boot_group_args(cmd: Command) -> Command {
cmd
.arg_required_else_help(true)
.arg(arg!(-i --"boot-image" <IMAGE_ID> "Image ID to boot the nodes"))
.arg(
arg!(-b --"boot-image-configuration" <NAME>
"Configuration name used to build the boot image (uses the most recent matching image)"),
)
.arg(arg!(-r --"runtime-configuration" <NAME> "Configuration to apply to nodes after booting"))
.arg(arg!(-k --"kernel-parameters" <VALUE> "Kernel parameters to assign to all group members"))
.arg(arg!(-d --"dry-run" "Simulate the operation without making changes").action(ArgAction::SetTrue))
.group(
ArgGroup::new("boot-image_or_boot-config")
.args(["boot-image", "boot-image-configuration"]),
)
.arg(arg!(<GROUP_NAME> "Group name").required(true))
.arg(output_flag())
}
pub fn subcommand_apply_boot_group() -> Command {
add_apply_boot_group_args(Command::new("group"))
.about("Update boot parameters for all nodes in a group")
.long_about(
"Update the boot parameters (image, runtime configuration, and kernel parameters) for all nodes in a group.\n\n\
The boot image can be specified by image ID or by the configuration name used to build it \
(the most recent matching image is used).\n\n\
eg:\n \
manta apply boot group \\\n \
--boot-image-configuration <config-name> \\\n \
--runtime-configuration <config-name> <group-name>",
)
}
fn add_boot_parameters_args(cmd: Command) -> Command {
cmd
.arg_required_else_help(true)
.arg(
arg!(-H --"hosts" <XNAMES> "Xnames of the nodes to update")
.required(true),
)
.arg(arg!(-p --"params" <VALUE> "Kernel parameters"))
.arg(arg!(-k --"kernel" <VALUE> "S3 path to the kernel file"))
.arg(arg!(-i --"initrd" <VALUE> "S3 path to the initrd file"))
.arg(
arg!(-d --"dry-run" "Simulate the operation without making changes")
.action(ArgAction::SetTrue),
)
.arg(output_flag())
}
pub fn subcommand_apply_boot_parameters() -> Command {
add_boot_parameters_args(Command::new("boot-parameters"))
.about("Update boot parameters for nodes")
}
fn add_redfish_endpoint_args(cmd: Command) -> Command {
cmd
.visible_alias("redfish-endpoint")
.arg_required_else_help(true)
.arg(arg!(-i --id <XNAME> "Xname of the endpoint to update").required(true))
.arg(arg!(-n --name <VALUE> "Arbitrary user-provided name for the endpoint"))
.arg(arg!(-H --hostname <VALUE> "Hostname (FQDN host portion)"))
.arg(arg!(-d --domain <VALUE> "Domain (FQDN domain portion)"))
.arg(
arg!(-f --fqdn <VALUE> "Fully-qualified domain name on the management network"),
)
.arg(arg!(-e --enabled "Enable the endpoint").action(ArgAction::SetTrue))
.arg(arg!(-u --user <VALUE> "Username for endpoint authentication"))
.arg(arg!(-p --password <VALUE> "Password for endpoint authentication"))
.arg(arg!(-U --"use-ssdp" "Use SSDP for discovery if the endpoint supports it").action(ArgAction::SetTrue))
.arg(arg!(-m --"mac-required" "Require a MAC address for geolocation").action(ArgAction::SetTrue))
.arg(arg!(-M --macaddr <VALUE> "MAC address of the Redfish endpoint on the management network"))
.arg(
arg!(-I --ipaddress <VALUE> "IP address of the Redfish endpoint on the management network (IPv4 or IPv6)"),
)
.arg(
arg!(-r --"rediscover-on-update" "Trigger rediscovery when endpoint information is updated")
.action(ArgAction::SetTrue),
)
.arg(arg!(-t --"template-id" <VALUE> "Discovery template ID"))
.arg(output_flag())
}
pub fn subcommand_apply_redfish_endpoint() -> Command {
add_redfish_endpoint_args(Command::new("redfish-endpoints"))
.about("Update an existing Redfish endpoint")
}
pub fn subcommand_apply_kernel_parameters() -> Command {
Command::new("kernel-parameters")
.arg_required_else_help(true)
.about("Replace the full kernel-parameters string on nodes (drops any existing parameters not listed)")
.arg(arg!(-n --nodes <NODES>).help(HOSTLIST_HELP))
.arg(
arg!(-H --group <GROUP_NAME> "Replace kernel parameters on every node in this group")
.visible_alias("hsm-group"),
)
.arg(arg!(-d --"dry-run" "Simulate the operation without making changes").action(ArgAction::SetTrue))
.arg(
arg!(<VALUE> "Space-separated kernel parameters to apply.\neg: bos_update_frequency=4h console=ttyS0,115200 crashkernel=512M")
.value_name("PARAMS"),
)
.group(
ArgGroup::new("cluster_or_nodes")
.args(["group", "nodes"])
.required(true),
)
.arg(output_flag())
}
pub fn subcommand_apply() -> Command {
Command::new("apply")
.arg_required_else_help(true)
.about("Roll out configurations, images, session templates, boot/kernel parameters, and hardware rescaling")
.subcommand(subcommand_apply_hw_configuration())
.subcommand(subcommand_apply_sat_file())
.subcommand(
Command::new("boot")
.arg_required_else_help(true)
.about("Update boot parameters and runtime configuration")
.subcommand(subcommand_apply_boot_nodes())
.subcommand(subcommand_apply_boot_group()),
)
.subcommand(subcommand_apply_boot_parameters())
.subcommand(subcommand_apply_redfish_endpoint())
.subcommand(subcommand_apply_kernel_parameters())
.subcommand(subcommand_apply_ephemeral_environment())
.subcommand(subcommand_apply_template())
}