diff --git a/cmd/iso.go b/cmd/iso.go new file mode 100644 index 0000000..dd8968e --- /dev/null +++ b/cmd/iso.go @@ -0,0 +1,274 @@ +package cmd + +import ( + "bufio" + "fmt" + "path/filepath" + "regexp" + "strings" + + "github.com/spf13/cobra" +) + +// ISO-specific commands for managing Talos ISO images +var isoCmd = &cobra.Command{ + Use: "iso", + Short: "Manage Talos ISO images", + Long: `Manage Talos ISO images for booting bare metal machines. +ISOs are organized by schematic ID and can be downloaded for different platforms.`, +} + +var ( + isoPlatform string + isoForce bool +) + +var isoListCmd = &cobra.Command{ + Use: "list", + Short: "List all downloaded ISO images", + Long: `List all downloaded ISO images across all schematics with their versions and platforms.`, + RunE: func(cmd *cobra.Command, args []string) error { + resp, err := apiClient.Get("/api/v1/assets") + if err != nil { + return err + } + + if outputFormat == "json" { + return printJSON(resp.Data) + } + + schematics := resp.GetArray("schematics") + if len(schematics) == 0 { + fmt.Println("No ISOs found") + return nil + } + + // Collect all ISO assets + type isoInfo struct { + SchematicID string + Version string + Platform string + Path string + Size int64 + } + var isos []isoInfo + + for _, schematic := range schematics { + if m, ok := schematic.(map[string]interface{}); ok { + schematicID := fmt.Sprintf("%v", m["schematic_id"]) + assets := m["assets"] + if assetsList, ok := assets.([]interface{}); ok { + for _, asset := range assetsList { + if assetMap, ok := asset.(map[string]interface{}); ok { + if assetType, _ := assetMap["type"].(string); assetType == "iso" { + if downloaded, _ := assetMap["downloaded"].(bool); downloaded { + path := fmt.Sprintf("%v", assetMap["path"]) + version := extractVersion(path) + platform := extractPlatform(path) + size := int64(0) + if s, ok := assetMap["size"].(float64); ok { + size = int64(s) + } + + isos = append(isos, isoInfo{ + SchematicID: schematicID, + Version: version, + Platform: platform, + Path: path, + Size: size, + }) + } + } + } + } + } + } + } + + if len(isos) == 0 { + fmt.Println("No ISOs downloaded") + return nil + } + + fmt.Printf("%-10s %-10s %-66s %-10s\n", "VERSION", "PLATFORM", "SCHEMATIC ID", "SIZE") + fmt.Println("--------------------------------------------------------------------------------------------------------") + for _, iso := range isos { + sizeMB := float64(iso.Size) / 1024 / 1024 + fmt.Printf("%-10s %-10s %-66s %.2f MB\n", iso.Version, iso.Platform, iso.SchematicID, sizeMB) + } + + return nil + }, +} + +var isoDownloadCmd = &cobra.Command{ + Use: "download ", + Short: "Download an ISO image", + Long: `Download a Talos ISO image for a given schematic ID and version. +The ISO can be used to boot bare metal machines.`, + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + schematicID := args[0] + version := args[1] + + payload := map[string]interface{}{ + "version": version, + "platform": isoPlatform, + "asset_types": []string{"iso"}, + } + + if isoForce { + payload["force"] = true + } + + resp, err := apiClient.Post(fmt.Sprintf("/api/v1/assets/%s/download", schematicID), payload) + if err != nil { + return err + } + + fmt.Printf("Downloading ISO for schematic: %s\n", schematicID) + fmt.Printf(" Version: %s\n", version) + fmt.Printf(" Platform: %s\n", isoPlatform) + + if msg := resp.GetString("message"); msg != "" { + fmt.Printf("\nStatus: %s\n", msg) + } + + return nil + }, +} + +var isoDeleteCmd = &cobra.Command{ + Use: "delete ", + Short: "Delete a schematic and all its assets", + Long: `Delete a schematic and all its downloaded assets including ISOs. +This operation cannot be undone.`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + schematicID := args[0] + + // Prompt for confirmation unless --force is used + if !isoForce { + fmt.Printf("Are you sure you want to delete schematic %s and all its assets? (yes/no): ", schematicID) + reader := bufio.NewReader(cmd.InOrStdin()) + response, err := reader.ReadString('\n') + if err != nil { + return err + } + response = strings.TrimSpace(strings.ToLower(response)) + if response != "yes" && response != "y" { + fmt.Println("Deletion cancelled") + return nil + } + } + + resp, err := apiClient.Delete(fmt.Sprintf("/api/v1/assets/%s", schematicID)) + if err != nil { + return err + } + + fmt.Printf("Schematic deleted: %s\n", schematicID) + if msg := resp.GetString("message"); msg != "" { + fmt.Printf("Status: %s\n", msg) + } + + return nil + }, +} + +var isoInfoCmd = &cobra.Command{ + Use: "info ", + Short: "Show detailed information about ISOs for a schematic", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + schematicID := args[0] + + resp, err := apiClient.Get(fmt.Sprintf("/api/v1/assets/%s", schematicID)) + if err != nil { + return err + } + + if outputFormat == "json" { + return printJSON(resp.Data) + } + + fmt.Printf("Schematic ID: %s\n", resp.GetString("schematic_id")) + fmt.Printf("Version: %s\n", resp.GetString("version")) + + if instances := resp.GetArray("instances"); len(instances) > 0 { + fmt.Printf("\nInstances using this schematic:\n") + for _, inst := range instances { + fmt.Printf(" - %s\n", inst) + } + } + + if assets := resp.GetArray("assets"); len(assets) > 0 { + fmt.Println("\nISO Images:") + hasISO := false + for _, asset := range assets { + if a, ok := asset.(map[string]interface{}); ok { + if assetType, _ := a["type"].(string); assetType == "iso" { + hasISO = true + downloaded, _ := a["downloaded"].(bool) + path := fmt.Sprintf("%v", a["path"]) + version := extractVersion(path) + platform := extractPlatform(path) + + if downloaded { + size := int64(0) + if s, ok := a["size"].(float64); ok { + size = int64(s) + } + sizeMB := float64(size) / 1024 / 1024 + fmt.Printf(" ✓ %s / %s (%.2f MB)\n", version, platform, sizeMB) + fmt.Printf(" Path: %s\n", path) + } else { + fmt.Printf(" ✗ Not downloaded\n") + } + } + } + } + if !hasISO { + fmt.Println(" No ISO images found") + } + } + + return nil + }, +} + +// extractVersion extracts version from ISO filename (e.g., "talos-v1.11.2-metal-amd64.iso" -> "v1.11.2") +func extractVersion(path string) string { + filename := filepath.Base(path) + re := regexp.MustCompile(`talos-(v\d+\.\d+\.\d+)-metal`) + matches := re.FindStringSubmatch(filename) + if len(matches) > 1 { + return matches[1] + } + return "unknown" +} + +// extractPlatform extracts platform from ISO filename (e.g., "talos-v1.11.2-metal-amd64.iso" -> "amd64") +func extractPlatform(path string) string { + filename := filepath.Base(path) + re := regexp.MustCompile(`-(amd64|arm64)\.iso$`) + matches := re.FindStringSubmatch(filename) + if len(matches) > 1 { + return matches[1] + } + return "unknown" +} + +func init() { + // Flags for download command + isoDownloadCmd.Flags().StringVarP(&isoPlatform, "platform", "p", "amd64", "Platform architecture (amd64, arm64)") + isoDownloadCmd.Flags().BoolVarP(&isoForce, "force", "f", false, "Force re-download if already exists") + + // Flags for delete command + isoDeleteCmd.Flags().BoolVarP(&isoForce, "force", "f", false, "Skip confirmation prompt") + + isoCmd.AddCommand(isoListCmd) + isoCmd.AddCommand(isoDownloadCmd) + isoCmd.AddCommand(isoDeleteCmd) + isoCmd.AddCommand(isoInfoCmd) +} diff --git a/cmd/pxe.go b/cmd/pxe.go index a3de007..2211a8a 100644 --- a/cmd/pxe.go +++ b/cmd/pxe.go @@ -6,22 +6,20 @@ import ( "github.com/spf13/cobra" ) -// PXE commands +// PXE/Asset commands (updated to use centralized asset management) var pxeCmd = &cobra.Command{ - Use: "pxe", - Short: "Manage PXE assets", + Use: "pxe", + Aliases: []string{"asset", "assets"}, + Short: "Manage Talos boot assets (centralized)", + Long: `Manage Talos boot assets using centralized asset management. +Assets are organized by schematic ID and shared across instances.`, } var pxeListCmd = &cobra.Command{ Use: "list", - Short: "List PXE assets", + Short: "List available schematics and their assets", RunE: func(cmd *cobra.Command, args []string) error { - inst, err := getInstanceName() - if err != nil { - return err - } - - resp, err := apiClient.Get(fmt.Sprintf("/api/v1/instances/%s/pxe/assets", inst)) + resp, err := apiClient.Get("/api/v1/assets") if err != nil { return err } @@ -30,48 +28,150 @@ var pxeListCmd = &cobra.Command{ return printJSON(resp.Data) } - assets := resp.GetArray("assets") - if len(assets) == 0 { - fmt.Println("No PXE assets found") + schematics := resp.GetArray("schematics") + if len(schematics) == 0 { + fmt.Println("No schematics found") return nil } - fmt.Printf("%-20s %-15s %-12s\n", "NAME", "VERSION", "STATUS") - fmt.Println("--------------------------------------------------") - for _, asset := range assets { - if m, ok := asset.(map[string]interface{}); ok { - fmt.Printf("%-20s %-15s %-12s\n", - m["name"], m["version"], m["status"]) + fmt.Printf("%-66s %-12s %-10s\n", "SCHEMATIC ID", "VERSION", "INSTANCES") + fmt.Println("--------------------------------------------------------------------------------------") + for _, schematic := range schematics { + if m, ok := schematic.(map[string]interface{}); ok { + schematicID := m["schematic_id"] + version := m["version"] + instancesCount := m["instances_count"] + fmt.Printf("%-66s %-12s %-10v\n", schematicID, version, instancesCount) } } return nil }, } +var ( + pxePlatform string + pxeAssetTypes []string +) + var pxeDownloadCmd = &cobra.Command{ - Use: "download ", - Short: "Download PXE asset", + Use: "download ", + Short: "Download assets for a schematic", + Long: `Download Talos boot assets (kernel, initramfs, iso) for a given schematic ID. +Assets are downloaded from the Talos Image Factory.`, + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + schematicID := args[0] + version := args[1] + + payload := map[string]interface{}{ + "version": version, + "platform": pxePlatform, + } + + // Add asset types if specified + if len(pxeAssetTypes) > 0 { + payload["asset_types"] = pxeAssetTypes + } + + resp, err := apiClient.Post(fmt.Sprintf("/api/v1/assets/%s/download", schematicID), payload) + if err != nil { + return err + } + + fmt.Printf("Download started for schematic: %s (version: %s, platform: %s)\n", schematicID, version, pxePlatform) + if msg := resp.GetString("message"); msg != "" { + fmt.Printf("Status: %s\n", msg) + } + return nil + }, +} + +var pxeStatusCmd = &cobra.Command{ + Use: "status ", + Short: "Get download status for a schematic", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - inst, err := getInstanceName() + schematicID := args[0] + + resp, err := apiClient.Get(fmt.Sprintf("/api/v1/assets/%s/status", schematicID)) if err != nil { return err } - resp, err := apiClient.Post(fmt.Sprintf("/api/v1/instances/%s/pxe/assets/%s/download", inst, args[0]), nil) + if outputFormat == "json" { + return printJSON(resp.Data) + } + + downloading := resp.GetBool("downloading") + if downloading { + fmt.Println("Status: Downloading") + if progress, ok := resp.Data["progress"].(map[string]interface{}); ok { + for assetType, progressData := range progress { + if p, ok := progressData.(map[string]interface{}); ok { + status := p["status"] + fmt.Printf(" %s: %s\n", assetType, status) + } + } + } + } else { + fmt.Println("Status: Complete") + } + return nil + }, +} + +var pxeInfoCmd = &cobra.Command{ + Use: "info ", + Short: "Get detailed information about a schematic", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + schematicID := args[0] + + resp, err := apiClient.Get(fmt.Sprintf("/api/v1/assets/%s", schematicID)) if err != nil { return err } - fmt.Printf("Download started for: %s\n", args[0]) - if opID := resp.GetString("operation_id"); opID != "" { - fmt.Printf("Operation ID: %s\n", opID) + if outputFormat == "json" { + return printJSON(resp.Data) + } + + fmt.Printf("Schematic ID: %s\n", resp.GetString("schematic_id")) + fmt.Printf("Version: %s\n", resp.GetString("version")) + + if instances := resp.GetArray("instances"); len(instances) > 0 { + fmt.Printf("Instances using this schematic:\n") + for _, inst := range instances { + fmt.Printf(" - %s\n", inst) + } + } + + if assets := resp.GetArray("assets"); len(assets) > 0 { + fmt.Println("\nAssets:") + for _, asset := range assets { + if a, ok := asset.(map[string]interface{}); ok { + downloaded := a["downloaded"] + assetType := a["type"] + fmt.Printf(" %s: ", assetType) + if downloaded == true { + fmt.Println("✓ Downloaded") + } else { + fmt.Println("✗ Not downloaded") + } + } + } } return nil }, } func init() { + // Flags for download command + pxeDownloadCmd.Flags().StringVarP(&pxePlatform, "platform", "p", "amd64", "Platform architecture (amd64, arm64)") + pxeDownloadCmd.Flags().StringSliceVarP(&pxeAssetTypes, "assets", "a", []string{}, "Asset types to download (kernel, initramfs, iso). Default: all") + pxeCmd.AddCommand(pxeListCmd) pxeCmd.AddCommand(pxeDownloadCmd) + pxeCmd.AddCommand(pxeStatusCmd) + pxeCmd.AddCommand(pxeInfoCmd) } diff --git a/cmd/root.go b/cmd/root.go index b951be9..280f516 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -70,6 +70,7 @@ func init() { rootCmd.AddCommand(secretCmd) rootCmd.AddCommand(nodeCmd) rootCmd.AddCommand(pxeCmd) + rootCmd.AddCommand(isoCmd) rootCmd.AddCommand(clusterCmd) rootCmd.AddCommand(serviceCmd) rootCmd.AddCommand(appCmd) diff --git a/internal/client/client.go b/internal/client/client.go index 367ee28..9a4dbed 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -146,6 +146,15 @@ func (r *APIResponse) GetArray(key string) []interface{} { return nil } +// GetBool extracts boolean data from API response +func (r *APIResponse) GetBool(key string) bool { + val := r.GetData(key) + if b, ok := val.(bool); ok { + return b + } + return false +} + // BaseURL returns the base URL of the client func (c *Client) BaseURL() string { return c.baseURL