|
|
|
@@ -11,10 +11,14 @@ package cli
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
|
|
|
|
|
|
"git.cloudinit.dev/coreci/orca/internal/certpaths"
|
|
|
|
|
"git.cloudinit.dev/coreci/orca/internal/sshpush"
|
|
|
|
|
"git.cloudinit.dev/coreci/orca/internal/store"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
@@ -72,8 +76,11 @@ var nodeCapacitySetCmd = &cobra.Command{
|
|
|
|
|
Short: "Declare capacity for a node (used by bin-packing)",
|
|
|
|
|
Long: "Write cpu_millicores, memory_mib, and disk_mib for the named node. Idempotent: subsequent calls overwrite.",
|
|
|
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
|
|
|
if capSetCPU <= 0 || capSetMem <= 0 || capSetDisk <= 0 {
|
|
|
|
|
return fmt.Errorf("--cpu, --memory, and --disk must all be positive")
|
|
|
|
|
// REQ-168: allow partial updates. At least one dimension
|
|
|
|
|
// must be positive; the others are read from the existing
|
|
|
|
|
// row (or default to 0 if no row exists yet).
|
|
|
|
|
if capSetCPU <= 0 && capSetMem <= 0 && capSetDisk <= 0 {
|
|
|
|
|
return fmt.Errorf("at least one of --cpu, --memory, or --disk must be positive")
|
|
|
|
|
}
|
|
|
|
|
id := capNodeID
|
|
|
|
|
if id == "" {
|
|
|
|
@@ -87,11 +94,27 @@ var nodeCapacitySetCmd = &cobra.Command{
|
|
|
|
|
}
|
|
|
|
|
defer closer()
|
|
|
|
|
repo := store.NewCapacityRepo(db)
|
|
|
|
|
// Read existing row for partial update.
|
|
|
|
|
existing, _ := repo.Get(ctx, id)
|
|
|
|
|
cpu := capSetCPU
|
|
|
|
|
mem := capSetMem
|
|
|
|
|
disk := capSetDisk
|
|
|
|
|
if existing != nil {
|
|
|
|
|
if cpu <= 0 {
|
|
|
|
|
cpu = existing.CPUMillicores
|
|
|
|
|
}
|
|
|
|
|
if mem <= 0 {
|
|
|
|
|
mem = existing.MemoryMiB
|
|
|
|
|
}
|
|
|
|
|
if disk <= 0 {
|
|
|
|
|
disk = existing.DiskMiB
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
c := &store.NodeCapacity{
|
|
|
|
|
NodeID: id,
|
|
|
|
|
CPUMillicores: capSetCPU,
|
|
|
|
|
MemoryMiB: capSetMem,
|
|
|
|
|
DiskMiB: capSetDisk,
|
|
|
|
|
CPUMillicores: cpu,
|
|
|
|
|
MemoryMiB: mem,
|
|
|
|
|
DiskMiB: disk,
|
|
|
|
|
}
|
|
|
|
|
if err := repo.Upsert(ctx, c); err != nil {
|
|
|
|
|
return err
|
|
|
|
@@ -137,6 +160,92 @@ var nodeCapacityListCmd = &cobra.Command{
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// nodeCapacityAutoCmd discovers capacity by SSHing to the node and
|
|
|
|
|
// reading nproc, /proc/meminfo, df (REQ-168, Phase D2).
|
|
|
|
|
var capAutoPct int
|
|
|
|
|
|
|
|
|
|
var nodeCapacityAutoCmd = &cobra.Command{
|
|
|
|
|
Use: "auto [percentage]",
|
|
|
|
|
Short: "Auto-discover node capacity via SSH (default 75% of physical)",
|
|
|
|
|
Long: `SSH to the specified node and discover CPU cores, memory,
|
|
|
|
|
and disk capacity. Multiplies the physical values by the given
|
|
|
|
|
percentage (default 75) to reserve headroom for the OS. The discovered
|
|
|
|
|
values are written to the capacity table (same as 'orca node capacity set').`,
|
|
|
|
|
Args: cobra.MaximumNArgs(1),
|
|
|
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
|
|
|
pct := 75
|
|
|
|
|
if len(args) > 0 {
|
|
|
|
|
var err error
|
|
|
|
|
pct, err = strconv.Atoi(args[0])
|
|
|
|
|
if err != nil || pct < 1 || pct > 100 {
|
|
|
|
|
return fmt.Errorf("percentage must be 1-100, got %q", args[0])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
id := capNodeID
|
|
|
|
|
if id == "" {
|
|
|
|
|
return fmt.Errorf("--node is required for capacity auto")
|
|
|
|
|
}
|
|
|
|
|
// Build SSH transport and exec discovery commands.
|
|
|
|
|
transport := sshpush.NewTransport(certpaths.SSHKeyPath(), certpaths.KnownHostsPath())
|
|
|
|
|
defer transport.Close()
|
|
|
|
|
ctx, cancel := context.WithTimeout(cmd.Context(), 30*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
// CPU: nproc
|
|
|
|
|
cpuOut, err := transport.Exec(ctx, id, "nproc")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("capacity auto: SSH exec nproc on %s: %w", id, err)
|
|
|
|
|
}
|
|
|
|
|
cores, err := strconv.Atoi(strings.TrimSpace(string(cpuOut)))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("capacity auto: parse nproc output %q: %w", string(cpuOut), err)
|
|
|
|
|
}
|
|
|
|
|
// Memory: MemTotal from /proc/meminfo (in kB -> MiB)
|
|
|
|
|
memOut, err := transport.Exec(ctx, id, "awk '/MemTotal/{print $2}' /proc/meminfo")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("capacity auto: SSH exec meminfo on %s: %w", id, err)
|
|
|
|
|
}
|
|
|
|
|
memKB, err := strconv.ParseInt(strings.TrimSpace(string(memOut)), 10, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("capacity auto: parse meminfo output %q: %w", string(memOut), err)
|
|
|
|
|
}
|
|
|
|
|
// Disk: df on root (1K-blocks -> MiB)
|
|
|
|
|
diskOut, err := transport.Exec(ctx, id, "df --output=size / | tail -1")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("capacity auto: SSH exec df on %s: %w", id, err)
|
|
|
|
|
}
|
|
|
|
|
diskKB, err := strconv.ParseInt(strings.TrimSpace(string(diskOut)), 10, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("capacity auto: parse df output %q: %w", string(diskOut), err)
|
|
|
|
|
}
|
|
|
|
|
// Apply percentage, convert to millicores/MiB.
|
|
|
|
|
cpuM := int64(cores) * 1000 * int64(pct) / 100
|
|
|
|
|
memMib := memKB * int64(pct) / 100 / 1024
|
|
|
|
|
diskMib := diskKB * int64(pct) / 100 / 1024
|
|
|
|
|
// Write to DB.
|
|
|
|
|
db, closer, err := openDB()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer closer()
|
|
|
|
|
repo := store.NewCapacityRepo(db)
|
|
|
|
|
c := &store.NodeCapacity{
|
|
|
|
|
NodeID: id,
|
|
|
|
|
CPUMillicores: cpuM,
|
|
|
|
|
MemoryMiB: memMib,
|
|
|
|
|
DiskMiB: diskMib,
|
|
|
|
|
}
|
|
|
|
|
if err := repo.Upsert(ctx, c); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if jsonOutput {
|
|
|
|
|
return printJSON(c)
|
|
|
|
|
}
|
|
|
|
|
fmt.Fprintf(cmd.OutOrStdout(), "Capacity auto-discovered for %s (%d%%): cpu=%dm, mem=%dMiB, disk=%dMiB\n", id, pct, cpuM, memMib, diskMib)
|
|
|
|
|
return nil
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
|
nodeCapacitySetCmd.Flags().Int64Var(&capSetCPU, "cpu", 0, "CPU capacity in millicores (1000 = 1 vCPU)")
|
|
|
|
|
nodeCapacitySetCmd.Flags().Int64Var(&capSetMem, "memory", 0, "Memory capacity in MiB")
|
|
|
|
@@ -144,6 +253,7 @@ func init() {
|
|
|
|
|
nodeCapacitySetCmd.Flags().StringVar(&capNodeID, "node", "", "node id (defaults to 'self')")
|
|
|
|
|
nodeCapacityShowCmd.Flags().StringVar(&capNodeID, "node", "", "node id (defaults to 'self')")
|
|
|
|
|
|
|
|
|
|
nodeCapacityCmd.AddCommand(nodeCapacityShowCmd, nodeCapacitySetCmd, nodeCapacityListCmd)
|
|
|
|
|
nodeCapacityAutoCmd.Flags().StringVar(&capNodeID, "node", "", "node name or ID to auto-discover capacity for")
|
|
|
|
|
nodeCapacityCmd.AddCommand(nodeCapacityShowCmd, nodeCapacitySetCmd, nodeCapacityListCmd, nodeCapacityAutoCmd)
|
|
|
|
|
nodeCmd.AddCommand(nodeCapacityCmd)
|
|
|
|
|
}
|
|
|
|
|