From 7c7beb9d409a6766185ad5200dc1896c6fa73598 Mon Sep 17 00:00:00 2001 From: Enno Gotthold Date: Tue, 9 Jun 2026 15:31:07 +0200 Subject: [PATCH] Update CLI for Cobbler 4.0.0 --- CHANGELOG.md | 47 +++ MIGRATION.md | 100 ++++++ cmd/distro.go | 19 +- cmd/distro_group.go | 295 +++++++++++++++++ cmd/file.go | 479 ---------------------------- cmd/file_test.go | 459 --------------------------- cmd/group_common.go | 70 ++++ cmd/image.go | 1 + cmd/interface.go | 719 ++++++++++++++++++------------------------ cmd/item.go | 45 ++- cmd/list.go | 17 +- cmd/menu.go | 1 + cmd/metadata.go | 208 +++--------- cmd/mgmtclass.go | 500 ----------------------------- cmd/mgmtclass_test.go | 454 -------------------------- cmd/package.go | 449 -------------------------- cmd/package_test.go | 454 -------------------------- cmd/profile.go | 49 +-- cmd/profile_group.go | 295 +++++++++++++++++ cmd/replicate.go | 40 +-- cmd/repo.go | 4 +- cmd/report.go | 41 +-- cmd/root.go | 36 +-- cmd/setting.go | 161 +++++++++- cmd/system.go | 350 +------------------- cmd/system_group.go | 295 +++++++++++++++++ cmd/template.go | 451 ++++++++++++++++++++++++++ go.mod | 9 +- go.sum | 8 +- testing/start.sh | 23 +- 30 files changed, 2170 insertions(+), 3909 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 MIGRATION.md create mode 100644 cmd/distro_group.go delete mode 100644 cmd/file.go delete mode 100644 cmd/file_test.go create mode 100644 cmd/group_common.go delete mode 100644 cmd/mgmtclass.go delete mode 100644 cmd/mgmtclass_test.go delete mode 100644 cmd/package.go delete mode 100644 cmd/package_test.go create mode 100644 cmd/profile_group.go create mode 100644 cmd/system_group.go create mode 100644 cmd/template.go diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..04fd3dd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,47 @@ +# Changelog + +## v1.0.0 (unreleased) + +First release against the Cobbler 4.0.0 backend and cobblerclient `v1.x`. +Clean break — talks only to Cobbler 4.0.0+. For 3.3.x servers, stay on the +`v0.x` line. + +### Breaking changes + +- Removed `cobbler mgmtclass`, `cobbler package`, `cobbler file` (item types + removed from the backend). +- Removed all interface-related flags from `cobbler system add/edit/copy/ + rename/find`. Network interfaces are now managed via the dedicated + `cobbler interface` command, with nested `--ipv4-*` / `--ipv6-*` / `--dns-*` + flag families that mirror the new `NetworkInterface` value objects. +- Removed `--mgmt-classes` and `--mgmt-parameters` flags from every item + command. +- `cobbler list` and `cobbler report` no longer include `mgmtclasses`, + `packages`, `files` sections. +- `cobbler setting edit` now auto-detects the setting's type via a + `GetSettings` round-trip and rejects unknown names. +- `cobbler system dumpvars` and `cobbler profile dumpvars` now call the + backend's `dump_vars` (formerly `get_blended_data`, which was removed in + Cobbler 3.4.0). + +### Added + +- `cobbler template` (add/edit/copy/remove/rename/find/list/report/export + + `content` + `refresh`). +- `cobbler distro-group`, `cobbler profile-group`, `cobbler system-group`. +- `--page` and `--items-per-page` flags on every `find` subcommand + (routes through `find_items_paged`). +- `cobbler setting export`. + +### Removed (no longer accepted) + +- The flags listed in the migration guide. + +### Deferred to a later release + +- `cobbler transaction` subcommand. The cobblerclient `v1.x` wraps + `TransactionBegin/Commit/Abort`, but per-token transactions don't span + multiple shell invocations naturally. Scripted batches need a Go program + today; a session-aware CLI mode is on the post-1.0 roadmap. + +See [MIGRATION.md](MIGRATION.md) for upgrade guidance. diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..c2063b5 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,100 @@ +# Migration guide: cobbler-cli v0.x → v1.0.0 + +This release re-anchors `cobbler-cli` on the Cobbler 4.0.0 backend and the matching cobblerclient `v1.x` library. It is +a clean break — there is no compatibility shim against Cobbler 3.3.x. If you need to talk to a 3.3.x server, stay on +`cobbler-cli v0.0.1-rc*` (which pulls cobblerclient `v0.5.x`). + +## Removed commands + +These item types no longer exist in Cobbler 4.0.0 and the corresponding commands have been removed: + +- `cobbler mgmtclass …` +- `cobbler package …` +- `cobbler file …` + +`cobbler list` and `cobbler report` no longer include sections for these types. + +The `--mgmt-classes` and `--mgmt-parameters` flags have been removed from every item command (`distro`, `profile`, +`system`). + +## Network interfaces moved to a dedicated command + +Network interfaces are first-class items in 4.0.0. Every `--*` flag related to interfaces has been removed from +`cobbler system add/edit/copy/rename/find`. Use `cobbler interface` instead. + +The flag names also reflect the new nested address objects: + +| Old (`cobbler system …`) | New (`cobbler interface …`) | +| ----------------------------- | --------------------------------- | +| `--interface ` | `--name ` plus `--system-name ` (on `add`) | +| `--ip-address` | `--ipv4-address` | +| `--netmask` | `--ipv4-netmask` | +| `--if-gateway` | `--ipv4-gateway` | +| `--static-routes` | `--ipv4-static-routes` | +| `--ipv6-address` | `--ipv6-address` (unchanged) | +| `--ipv6-prefix` | `--ipv6-prefix` (unchanged) | +| `--ipv6-default-gateway` | `--ipv6-default-gateway` (unchanged) | +| `--ipv6-secondaries` | `--ipv6-secondaries` (unchanged) | +| `--ipv6-static-routes` | `--ipv6-static-routes` (unchanged) | +| `--ipv6-mtu` | `--ipv6-mtu` (unchanged) | +| `--dns-name` | `--dns-name` (unchanged) | +| `--cnames` | `--dns-cnames` | +| `--mac-address` | `--mac-address` (unchanged) | +| `--bonding-opts` | `--bonding-opts` (unchanged) | +| `--bridge-opts` | `--bridge-opts` (unchanged) | +| `--interface-type` | `--interface-type` (unchanged) | +| `--interface-master` | `--interface-master` (unchanged) | +| `--connected-mode` | `--connected-mode` (unchanged) | +| `--management` | `--management` (unchanged) | +| `--static` | `--static` (unchanged) | +| `--mtu` | `--mtu` (unchanged) | +| `--virt-bridge` | `--virt-bridge` (now inheritable: `--virt-bridge-inherit`) | +| `--delete-interface` | `cobbler interface remove --name ` | +| `--rename-interface` | `cobbler interface rename --name --newname ` | + +Lifecycle example: + +```bash +cobbler system add --name=server1 --profile=rocky-9-x86_64 +cobbler interface add --system-name=server1 --name=eth0 \ + --mac-address=aa:bb:cc:dd:ee:ff \ + --ipv4-address=10.0.0.5 --ipv4-netmask=255.255.255.0 --ipv4-gateway=10.0.0.1 \ + --dns-name=server1.lab.local +cobbler interface report --system-name=server1 +``` + +## New commands + +- `cobbler template` — manage Cobbler 4.0.0 `Template` items, including `cobbler template content --name=` (dump + rendered content to stdout) and `cobbler template refresh [--name=...]` (reload from disk). +- `cobbler distro-group`, `cobbler profile-group`, `cobbler system-group` — manage the three group item types. +- Every `find` subcommand now supports `--page` and `--items-per-page`, routing through the backend's `find_items_paged` + endpoint and emitting a trailing `# page N of M (T total)` summary. + +## Strict setting types + +`cobbler setting edit --name= --value=` now performs a round-trip to `get_settings` to discover the existing +setting's type, then parses `--value` accordingly. The 4.0.0 backend rejects mistyped values. Examples: + +```bash +cobbler setting edit --name=server --value=cobbler.lab.local # string +cobbler setting edit --name=http_port --value=8080 # int +cobbler setting edit --name=manage_dhcp --value=true # bool +cobbler setting edit --name=cheetah_import_whitelist --value=foo,bar # []string +``` + +Unknown setting names error out cleanly. + +## Transactions: deferred + +Cobbler 4.0.0 exposes per-token `transaction_begin/commit/abort`. The Go client (cobblerclient) wraps them as +`TransactionBegin`/`TransactionCommit`/ `TransactionAbort`. There is no `cobbler transaction` subcommand in v1.0.0 — +the CLI logs in fresh on each invocation, so per-token transactions cannot naturally span multiple shell commands. Use +the Go client directly for scripted atomic batches; a session-aware CLI mode may land in a later release. + +## Things you don't need to do + +- Existing scripts that hit `cobbler distro`, `cobbler profile`, `cobbler system`, `cobbler repo`, `cobbler image`, + `cobbler menu` keep working, modulo the field removals listed above. +- `cobbler version` now shows the build date and git hash of the connected server (was already there in the + cobblerclient — surfaced now). diff --git a/cmd/distro.go b/cmd/distro.go index 5a45737..9a00c3b 100644 --- a/cmd/distro.go +++ b/cmd/distro.go @@ -227,24 +227,6 @@ func updateDistroFromFlags(cmd *cobra.Command, distro *cobbler.Distro) error { distro.KernelOptions.Data = convertMapStringToMapInterface(newKernelOptionsPost) } } - case "mgmt-classes": - fallthrough - case "mgmt-classes-inherit": - var distroNewMgmtClasses []string - distroNewMgmtClasses, err = cmd.Flags().GetStringSlice("mgmt-classes") - if err != nil { - return - } - if cmd.Flags().Lookup("mgmt-classes-inherit").Changed { - distro.MgmtClasses.Data = []string{} - distro.MgmtClasses.IsInherited, err = cmd.Flags().GetBool("mgmt-classes-inherit") - if err != nil { - return - } - } else { - distro.MgmtClasses.IsInherited = false - distro.MgmtClasses.Data = distroNewMgmtClasses - } case "os-version": var distroNewOsVersion string distroNewOsVersion, err = cmd.Flags().GetString("os-version") @@ -534,6 +516,7 @@ func NewDistroFindCmd() (*cobra.Command, error) { addFloatFlags(distroFindCmd, findFloatFlagMetadata) distroFindCmd.Flags().String("source-repos", "", "source repositories") distroFindCmd.Flags().String("tree-build-time", "", "tree build time") + addPaginationFlags(distroFindCmd) return distroFindCmd, nil } diff --git a/cmd/distro_group.go b/cmd/distro_group.go new file mode 100644 index 0000000..d9e2b9d --- /dev/null +++ b/cmd/distro_group.go @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright SUSE LLC + +package cmd + +import ( + "fmt" + + cobbler "github.com/cobbler/cobblerclient" + "github.com/spf13/cobra" +) + +// NewDistroGroupCmd builds the `cobbler distro-group` command tree. +func NewDistroGroupCmd() (*cobra.Command, error) { + cmd := &cobra.Command{ + Use: "distro-group", + Short: "Manage distro groups", + Long: `Manage Cobbler 4.0.0 DistroGroup items.`, + } + cmd.AddCommand(newDistroGroupAddCmd()) + cmd.AddCommand(newDistroGroupCopyCmd()) + cmd.AddCommand(newDistroGroupEditCmd()) + cmd.AddCommand(newDistroGroupFindCmd()) + cmd.AddCommand(newDistroGroupListCmd()) + cmd.AddCommand(newDistroGroupRemoveCmd()) + cmd.AddCommand(newDistroGroupRenameCmd()) + cmd.AddCommand(newDistroGroupReportCmd()) + cmd.AddCommand(newDistroGroupExportCmd()) + return cmd, nil +} + +func newDistroGroupAddCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "add", + Short: "add a distro group", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + g := cobbler.NewDistroGroup() + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + g.Name = name + comment, commentSet, items, itemsSet, err := extractGroupFlags(cmd) + if err != nil { + return err + } + if commentSet { + g.Comment = comment + } + if itemsSet { + g.Items = items + } + created, err := Client.CreateDistroGroup(g) + if err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "Distro group %s created\n", created.Name) + return nil + }, + } + addGroupFlagSet(cmd) + return cmd +} + +func newDistroGroupEditCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "edit", + Short: "edit a distro group", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + g, err := Client.GetDistroGroup(name, false, false) + if err != nil { + return err + } + comment, commentSet, items, itemsSet, err := extractGroupFlags(cmd) + if err != nil { + return err + } + if commentSet { + g.Comment = comment + } + if itemsSet { + g.Items = items + } + return Client.UpdateDistroGroup(g) + }, + } + addGroupFlagSet(cmd) + return cmd +} + +func newDistroGroupCopyCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "copy", + Short: "copy a distro group", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + newName, err := cmd.Flags().GetString("newname") + if err != nil { + return err + } + handle, err := Client.GetDistroGroupHandle(name) + if err != nil { + return err + } + return Client.CopyDistroGroup(handle, newName) + }, + } + addStringFlags(cmd, commonStringFlagMetadata) + addStringFlags(cmd, copyRenameStringFlagMetadata) + return cmd +} + +func newDistroGroupRenameCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "rename", + Short: "rename a distro group", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + newName, err := cmd.Flags().GetString("newname") + if err != nil { + return err + } + handle, err := Client.GetDistroGroupHandle(name) + if err != nil { + return err + } + return Client.RenameDistroGroup(handle, newName) + }, + } + addStringFlags(cmd, commonStringFlagMetadata) + addStringFlags(cmd, copyRenameStringFlagMetadata) + return cmd +} + +func newDistroGroupRemoveCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "remove", + Short: "remove a distro group", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + return RemoveItemRecursive(cmd, args, "distro_group") + }, + } + cmd.Flags().String("name", "", "the distro group name") + cmd.Flags().Bool("recursive", false, "also delete child objects") + return cmd +} + +func newDistroGroupFindCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "find", + Short: "find distro groups", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + return FindItemNames(cmd, args, "distro_group") + }, + } + addStringFlags(cmd, commonStringFlagMetadata) + addStringSliceFlags(cmd, groupStringSliceFlagMetadata) + addPaginationFlags(cmd) + return cmd +} + +func newDistroGroupListCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "list all distro groups", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + names, err := Client.ListDistroGroupNames() + if err != nil { + return err + } + listItems(cmd, "distro_groups", names) + return nil + }, + } + return cmd +} + +func newDistroGroupReportCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "report", + Short: "show distro group details", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + names := make([]string, 0) + if name == "" { + names, err = Client.ListDistroGroupNames() + if err != nil { + return err + } + } else { + names = append(names, name) + } + for _, n := range names { + g, err := Client.GetDistroGroup(n, false, false) + if err != nil { + return err + } + printStructured(cmd, g) + fmt.Fprintln(cmd.OutOrStdout(), "") + } + return nil + }, + } + cmd.Flags().String("name", "", "the distro group name") + return cmd +} + +func newDistroGroupExportCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "export", + Short: "export distro groups", + PreRunE: func(cmd *cobra.Command, args []string) error { + format, err := cmd.Flags().GetString("format") + if err != nil { + return err + } + if format != "json" && format != "yaml" { + return fmt.Errorf("format must be json or yaml") + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + format, err := cmd.Flags().GetString("format") + if err != nil { + return err + } + names := make([]string, 0) + if name == "" { + names, err = Client.ListDistroGroupNames() + if err != nil { + return err + } + } else { + names = append(names, name) + } + for _, n := range names { + g, err := Client.GetDistroGroup(n, false, false) + if err != nil { + return err + } + if err := writeExport(cmd, format, g); err != nil { + return err + } + } + return nil + }, + } + cmd.Flags().String("name", "", "the distro group name") + cmd.Flags().String(exportStringMetadata["format"].Name, exportStringMetadata["format"].DefaultValue, exportStringMetadata["format"].Usage) + return cmd +} diff --git a/cmd/file.go b/cmd/file.go deleted file mode 100644 index 6ac1e48..0000000 --- a/cmd/file.go +++ /dev/null @@ -1,479 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// SPDX-FileCopyrightText: 2021 Dominik Gedon -// SPDX-FileCopyrightText: Copyright SUSE LLC - -package cmd - -import ( - "encoding/json" - "fmt" - cobbler "github.com/cobbler/cobblerclient" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "gopkg.in/yaml.v3" -) - -func updateFileFromFlags(cmd *cobra.Command, file *cobbler.File) error { - // This object type doesn't have the in-place flag - var err error - cmd.Flags().Visit(func(flag *pflag.Flag) { - if err != nil { - // If one of the previous flags has had an error just directly return. - return - } - switch flag.Name { - // The rename & copy operations are special operations as such we cannot blindly set this inside here. - // Any rename & copy operation must be handled outside of this method. - case "comment": - var fileNewComment string - fileNewComment, err = cmd.Flags().GetString("comment") - if err != nil { - return - } - file.Comment = fileNewComment - case "owners": - fallthrough - case "owners-inherit": - var fileNewOwners []string - fileNewOwners, err = cmd.Flags().GetStringSlice("owners") - if err != nil { - return - } - if cmd.Flags().Lookup("owners-inherit").Changed { - file.Owners.Data = []string{} - file.Owners.IsInherited, err = cmd.Flags().GetBool("owners-inherit") - if err != nil { - return - } - } else { - file.Owners.IsInherited = false - file.Owners.Data = fileNewOwners - } - case "action": - var fileNewAction string - fileNewAction, err = cmd.Flags().GetString("action") - if err != nil { - return - } - file.Action = fileNewAction - case "mode": - var fileNewMode string - fileNewMode, err = cmd.Flags().GetString("mode") - if err != nil { - return - } - file.Mode = fileNewMode - case "template": - var fileNewTemplate string - fileNewTemplate, err = cmd.Flags().GetString("template") - if err != nil { - return - } - file.Template = fileNewTemplate - case "path": - var fileNewPath string - fileNewPath, err = cmd.Flags().GetString("path") - if err != nil { - return - } - file.Path = fileNewPath - case "group": - var fileNewGroup string - fileNewGroup, err = cmd.Flags().GetString("group") - if err != nil { - return - } - file.Group = fileNewGroup - case "owner": - var fileNewOwner string - fileNewOwner, err = cmd.Flags().GetString("owner") - if err != nil { - return - } - file.Owner = fileNewOwner - case "is-dir": - var fileNewIsDir bool - fileNewIsDir, err = cmd.Flags().GetBool("is-dir") - if err != nil { - return - } - file.IsDir = fileNewIsDir - } - }) - // Don't blindly return nil because maybe one of the flags had an issue retrieving an argument. - return err -} - -// NewFileCmd builds a new command that represents the file action -func NewFileCmd() (*cobra.Command, error) { - fileCmd := &cobra.Command{ - Use: "file", - Short: "File management", - Long: `Let you manage files. -See https://cobbler.readthedocs.io/en/latest/cobbler.html#cobbler-file for more information.`, - Run: func(cmd *cobra.Command, args []string) { - _ = cmd.Help() - }, - } - fileCmd.AddCommand(NewFileAddCmd()) - fileCmd.AddCommand(NewFileCopyCmd()) - fileCmd.AddCommand(NewFileEditCmd()) - fileCmd.AddCommand(NewFileFindCmd()) - fileCmd.AddCommand(NewFileListCmd()) - fileCmd.AddCommand(NewFileRemoveCmd()) - fileCmd.AddCommand(NewFileRenameCmd()) - fileCmd.AddCommand(NewFileReportCmd()) - fileCmd.AddCommand(NewFileExportCmd()) - return fileCmd, nil -} - -func NewFileAddCmd() *cobra.Command { - fileAddCmd := &cobra.Command{ - Use: "add", - Short: "add file", - Long: `Adds a file.`, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - newFile := cobbler.NewFile() - - // Get special name flag - newFile.Name, err = cmd.Flags().GetString("name") - if err != nil { - return err - } - // Update with the rest of the flags - err = updateFileFromFlags(cmd, &newFile) - if err != nil { - return err - } - // Now create the file via XML-RPC - file, err := Client.CreateFile(newFile) - if err != nil { - return err - } - fmt.Fprintf(cmd.OutOrStdout(), "File %s created\n", file.Name) - return nil - }, - } - addCommonArgs(fileAddCmd) - addStringFlags(fileAddCmd, fileStringFlagMetadata) - addBoolFlags(fileAddCmd, fileBoolFlagMetadata) - return fileAddCmd -} - -func NewFileCopyCmd() *cobra.Command { - fileCopyCmd := &cobra.Command{ - Use: "copy", - Short: "copy file", - Long: `Copies a file.`, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - // Get special name and newname flags - fileName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - fileNewName, err := cmd.Flags().GetString("newname") - if err != nil { - return err - } - - // Now copy the file - fileHandle, err := Client.GetFileHandle(fileName) - if err != nil { - return err - } - err = Client.CopyFile(fileHandle, fileNewName) - if err != nil { - return err - } - newFile, err := Client.GetFile(fileNewName, false, false) - if err != nil { - return err - } - err = updateFileFromFlags(cmd, newFile) - if err != nil { - return err - } - return Client.UpdateFile(newFile) - }, - } - addCommonArgs(fileCopyCmd) - addStringFlags(fileCopyCmd, fileStringFlagMetadata) - addBoolFlags(fileCopyCmd, fileBoolFlagMetadata) - fileCopyCmd.Flags().String("newname", "", "the new file name") - return fileCopyCmd -} - -func NewFileEditCmd() *cobra.Command { - fileEditCmd := &cobra.Command{ - Use: "edit", - Short: "edit file", - Long: `Edits a file.`, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - // Get the file name - fileName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - - // Now get the file from the API - newFile, err := Client.GetFile(fileName, false, false) - if err != nil { - return err - } - // Update the file in-memory - err = updateFileFromFlags(cmd, newFile) - if err != nil { - return err - } - // Now update the file via XML-RPC - return Client.UpdateFile(newFile) - }, - } - addCommonArgs(fileEditCmd) - addStringFlags(fileEditCmd, fileStringFlagMetadata) - addBoolFlags(fileEditCmd, fileBoolFlagMetadata) - return fileEditCmd -} - -func NewFileFindCmd() *cobra.Command { - fileFindCmd := &cobra.Command{ - Use: "find", - Short: "find file", - Long: `Finds a given file.`, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - return FindItemNames(cmd, args, "file") - }, - } - addCommonArgs(fileFindCmd) - addStringFlags(fileFindCmd, fileStringFlagMetadata) - addBoolFlags(fileFindCmd, fileBoolFlagMetadata) - addStringFlags(fileFindCmd, findStringFlagMetadata) - addIntFlags(fileFindCmd, findIntFlagMetadata) - addFloatFlags(fileFindCmd, findFloatFlagMetadata) - return fileFindCmd -} - -func NewFileListCmd() *cobra.Command { - fileListCmd := &cobra.Command{ - Use: "list", - Short: "list all files", - Long: `Lists all available files.`, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - fileNames, err := Client.ListFileNames() - if err != nil { - return err - } - listItems(cmd, "files", fileNames) - return nil - }, - } - return fileListCmd -} - -func NewFileRemoveCmd() *cobra.Command { - fileRemoveCmd := &cobra.Command{ - Use: "remove", - Short: "remove file", - Long: `Removes a given file.`, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - return RemoveItemRecursive(cmd, args, "file") - }, - } - fileRemoveCmd.Flags().String("name", "", "the file name") - fileRemoveCmd.Flags().Bool("recursive", false, "also delete child objects") - return fileRemoveCmd -} - -func NewFileRenameCmd() *cobra.Command { - fileRenameCmd := &cobra.Command{ - Use: "rename", - Short: "rename file", - Long: `Renames a given file.`, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - // Get the special name and newname flags - fileName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - fileNewName, err := cmd.Flags().GetString("newname") - if err != nil { - return err - } - - // Get the file handle - fileHandle, err := Client.GetFileHandle(fileName) - if err != nil { - return err - } - // Rename the file (server-side) - err = Client.RenameFile(fileHandle, fileNewName) - if err != nil { - return err - } - // Get the renamed file from the API - newFile, err := Client.GetFile(fileNewName, false, false) - if err != nil { - return err - } - // Update the file in-memory - err = updateFileFromFlags(cmd, newFile) - if err != nil { - return err - } - // Update the file via XML-RPC - return Client.UpdateFile(newFile) - }, - } - addCommonArgs(fileRenameCmd) - addStringFlags(fileRenameCmd, fileStringFlagMetadata) - addBoolFlags(fileRenameCmd, fileBoolFlagMetadata) - fileRenameCmd.Flags().String("newname", "", "the new file name") - return fileRenameCmd -} - -func reportFiles(cmd *cobra.Command, fileNames []string) error { - for _, itemName := range fileNames { - file, err := Client.GetFile(itemName, false, false) - if err != nil { - return err - } - printStructured(cmd, file) - fmt.Fprintln(cmd.OutOrStdout(), "") - } - return nil -} - -func NewFileReportCmd() *cobra.Command { - fileReportCmd := &cobra.Command{ - Use: "report", - Short: "list all files in detail", - Long: `Shows detailed information about all files.`, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - name, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - itemNames := make([]string, 0) - if name == "" { - itemNames, err = Client.ListFileNames() - if err != nil { - return err - } - } else { - itemNames = append(itemNames, name) - } - return reportFiles(cmd, itemNames) - }, - } - fileReportCmd.Flags().String("name", "", "the file name") - return fileReportCmd -} - -func NewFileExportCmd() *cobra.Command { - fileExportCmd := &cobra.Command{ - Use: "export", - Short: "export files", - Long: `Export files.`, - PreRunE: func(cmd *cobra.Command, args []string) error { - formatOption, err := cmd.Flags().GetString("format") - if err != nil { - return err - } - if formatOption != "json" && formatOption != "yaml" { - return fmt.Errorf("format must be json or yaml") - } - return nil - }, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - name, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - formatOption, err := cmd.Flags().GetString("format") - if err != nil { - return err - } - - itemNames := make([]string, 0) - if name == "" { - itemNames, err = Client.ListFileNames() - if err != nil { - return err - } - } else { - itemNames = append(itemNames, name) - } - - for _, itemName := range itemNames { - file, err := Client.GetFile(itemName, false, false) - if err != nil { - return err - } - if formatOption == "json" { - jsonDocument, err := json.Marshal(file) - if err != nil { - return err - } - fmt.Fprintln(cmd.OutOrStdout(), string(jsonDocument)) - } - if formatOption == "yaml" { - yamlDocument, err := yaml.Marshal(file) - if err != nil { - return err - } - fmt.Fprintln(cmd.OutOrStdout(), "---") - fmt.Fprintln(cmd.OutOrStdout(), string(yamlDocument)) - } - } - return nil - }, - } - fileExportCmd.Flags().String("name", "", "the file name") - fileExportCmd.Flags().String(exportStringMetadata["format"].Name, exportStringMetadata["format"].DefaultValue, exportStringMetadata["format"].Usage) - return fileExportCmd -} diff --git a/cmd/file_test.go b/cmd/file_test.go deleted file mode 100644 index c35b3c3..0000000 --- a/cmd/file_test.go +++ /dev/null @@ -1,459 +0,0 @@ -package cmd - -import ( - "bytes" - "fmt" - cobbler "github.com/cobbler/cobblerclient" - "github.com/spf13/cobra" - "io" - "strings" - "testing" -) - -func createFile(client cobbler.Client, name string) (*cobbler.File, error) { - file := cobbler.NewFile() - file.Name = name - file.Path = "/my/custom/folder" - file.Owner = "root" - file.Group = "root" - file.Mode = "0755" - file.IsDir = true - return client.CreateFile(file) -} - -func removeFile(client cobbler.Client, name string) error { - return client.DeleteFile(name) -} - -func Test_FileAddCmd(t *testing.T) { - type args struct { - command []string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "plain", - args: args{command: []string{"--config", "../testing/.cobbler.yaml", "file", "add", "--name", "test-plain", "--path", "/my/custom/folder", "--group", "root", "--owner", "root", "--mode", "0755", "--is-dir", "true"}}, - want: "File test-plain created", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Cleanup - var err error - defer func() { - // Client is initialized since this is the cleanup - cleanupErr := removeFile(Client, tt.args.command[5]) - cobbler.FailOnError(t, cleanupErr) - }() - // Arrange - cobra.OnInitialize(initConfig, setupLogger) - rootCmd := NewRootCmd() - rootCmd.SetArgs(tt.args.command) - stdout := bytes.NewBufferString("") - stderr := bytes.NewBufferString("") - rootCmd.SetOut(stdout) - rootCmd.SetErr(stderr) - - // Act - err = rootCmd.Execute() - - // Assert - cobbler.FailOnError(t, err) - FailOnNonEmptyStream(t, stderr) - stdoutBytes, err := io.ReadAll(stdout) - if err != nil { - t.Fatal(err) - } - stdoutString := string(stdoutBytes) - if !strings.Contains(stdoutString, tt.want) { - fmt.Println(stdoutString) - t.Fatal("Item creation message missing") - } - }) - } -} - -func Test_FileCopyCmd(t *testing.T) { - type args struct { - command []string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "plain", - args: args{command: []string{"--config", "../testing/.cobbler.yaml", "file", "copy", "--name", "file-to-copy", "--newname", "copied-file"}}, - want: "Event ID:", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Cleanup - var err error - defer func() { - // Client is initialized since this is the cleanup - cleanupErr := removeFile(Client, tt.args.command[5]) - cobbler.FailOnError(t, cleanupErr) - cleanupErr = removeFile(Client, tt.args.command[7]) - cobbler.FailOnError(t, cleanupErr) - }() - // Arrange - setupClient(t) - _, err = createFile(Client, tt.args.command[5]) - cobbler.FailOnError(t, err) - cobra.OnInitialize(initConfig, setupLogger) - rootCmd := NewRootCmd() - rootCmd.SetArgs(tt.args.command) - stdout := bytes.NewBufferString("") - stderr := bytes.NewBufferString("") - rootCmd.SetOut(stdout) - rootCmd.SetErr(stderr) - - // Act - err = rootCmd.Execute() - - // Assert - cobbler.FailOnError(t, err) - FailOnNonEmptyStream(t, stderr) - FailOnNonEmptyStream(t, stdout) - _, err = Client.GetFile(tt.args.command[7], false, false) - cobbler.FailOnError(t, err) - }) - } -} - -func Test_FileEditCmd(t *testing.T) { - type args struct { - command []string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "plain", - args: args{command: []string{"--config", "../testing/.cobbler.yaml", "file", "edit", "--name", "test-file-edit", "--comment", "testcomment"}}, - want: "Event ID:", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Cleanup - var err error - defer func() { - // Client is initialized since this is the cleanup - cleanupErr := removeFile(Client, tt.args.command[5]) - cobbler.FailOnError(t, cleanupErr) - }() - // Arrange - setupClient(t) - _, err = createFile(Client, tt.args.command[5]) - cobbler.FailOnError(t, err) - cobra.OnInitialize(initConfig, setupLogger) - rootCmd := NewRootCmd() - rootCmd.SetArgs(tt.args.command) - stdout := bytes.NewBufferString("") - stderr := bytes.NewBufferString("") - rootCmd.SetOut(stdout) - rootCmd.SetErr(stderr) - - // Act - err = rootCmd.Execute() - - // Assert - cobbler.FailOnError(t, err) - FailOnNonEmptyStream(t, stderr) - FailOnNonEmptyStream(t, stdout) - updatedFile, err := Client.GetFile(tt.args.command[5], false, false) - cobbler.FailOnError(t, err) - if updatedFile.Comment != "testcomment" { - t.Fatal("file update wasn't successful") - } - }) - } -} - -func Test_FileFindCmd(t *testing.T) { - type args struct { - command []string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "plain", - args: args{command: []string{"--config", "../testing/.cobbler.yaml", "file", "find", "--name", "test-file-find"}}, - want: "Event ID:", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Cleanup - fileName := "test-file-find" - var err error - defer func() { - // Client is initialized since this is the cleanup - cleanupErr := removeFile(Client, fileName) - cobbler.FailOnError(t, cleanupErr) - }() - // Arrange - setupClient(t) - _, err = createFile(Client, fileName) - cobbler.FailOnError(t, err) - cobra.OnInitialize(initConfig, setupLogger) - rootCmd := NewRootCmd() - rootCmd.SetArgs(tt.args.command) - stdout := bytes.NewBufferString("") - stderr := bytes.NewBufferString("") - rootCmd.SetOut(stdout) - rootCmd.SetErr(stderr) - - // Act - err = rootCmd.Execute() - - // Assert - cobbler.FailOnError(t, err) - FailOnNonEmptyStream(t, stderr) - stdoutBytes, err := io.ReadAll(stdout) - if err != nil { - t.Fatal(err) - } - stdoutString := string(stdoutBytes) - if !strings.Contains(stdoutString, fileName) { - fmt.Println(stdoutString) - t.Fatal("file not successfully found") - } - }) - } -} - -func Test_FileListCmd(t *testing.T) { - type args struct { - command []string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "plain", - args: args{command: []string{"--config", "../testing/.cobbler.yaml", "file", "list"}}, - want: "files:", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Arrange - cobra.OnInitialize(initConfig, setupLogger) - rootCmd := NewRootCmd() - rootCmd.SetArgs(tt.args.command) - stdout := bytes.NewBufferString("") - stderr := bytes.NewBufferString("") - rootCmd.SetOut(stdout) - rootCmd.SetErr(stderr) - - // Act - err := rootCmd.Execute() - - // Assert - cobbler.FailOnError(t, err) - FailOnNonEmptyStream(t, stderr) - stdoutBytes, err := io.ReadAll(stdout) - if err != nil { - t.Fatal(err) - } - stdoutString := string(stdoutBytes) - if !strings.Contains(stdoutString, tt.want) { - fmt.Println(stdoutString) - t.Fatal("file list marker not located in output") - } - }) - } -} - -func Test_FileRemoveCmd(t *testing.T) { - type args struct { - command []string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "plain", - args: args{command: []string{"--config", "../testing/.cobbler.yaml", "file", "remove", "--name", "test-file-remove"}}, - want: "Event ID:", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Arrange - setupClient(t) - _, err := createFile(Client, tt.args.command[5]) - cobbler.FailOnError(t, err) - cobra.OnInitialize(initConfig, setupLogger) - rootCmd := NewRootCmd() - rootCmd.SetArgs(tt.args.command) - stdout := bytes.NewBufferString("") - stderr := bytes.NewBufferString("") - rootCmd.SetOut(stdout) - rootCmd.SetErr(stderr) - - // Act - err = rootCmd.Execute() - - // Assert - cobbler.FailOnError(t, err) - FailOnNonEmptyStream(t, stderr) - FailOnNonEmptyStream(t, stdout) - result, err := Client.HasItem("file", tt.args.command[5]) - cobbler.FailOnError(t, err) - if result { - // A missing item means we get "false", as such we error when we find an item. - t.Fatal("file not successfully removed") - } - }) - } -} - -func Test_FileRenameCmd(t *testing.T) { - type args struct { - command []string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "plain", - args: args{command: []string{"--config", "../testing/.cobbler.yaml", "file", "rename", "--name", "test-file-rename", "--newname", "test-file-renamed"}}, - want: "Event ID:", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Cleanup - fileName := "test-file-rename" - newFileName := "test-file-renamed" - var err error - defer func() { - // Client is initialized since this is the cleanup - cleanupErr := removeFile(Client, newFileName) - cobbler.FailOnError(t, cleanupErr) - }() - // Arrange - setupClient(t) - _, err = createFile(Client, fileName) - cobbler.FailOnError(t, err) - cobra.OnInitialize(initConfig, setupLogger) - rootCmd := NewRootCmd() - rootCmd.SetArgs(tt.args.command) - stdout := bytes.NewBufferString("") - stderr := bytes.NewBufferString("") - rootCmd.SetOut(stdout) - rootCmd.SetErr(stderr) - - // Act - err = rootCmd.Execute() - - // Assert - cobbler.FailOnError(t, err) - FailOnNonEmptyStream(t, stderr) - FailOnNonEmptyStream(t, stdout) - resultOldName, err := Client.HasItem("file", fileName) - cobbler.FailOnError(t, err) - if resultOldName { - t.Fatal("file not successfully renamed (old name present)") - } - resultNewName, err := Client.HasItem("file", newFileName) - cobbler.FailOnError(t, err) - if !resultNewName { - t.Fatal("file not successfully renamed (new name not present)") - } - }) - } -} - -func Test_FileReportCmd(t *testing.T) { - type args struct { - command []string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "plain", - args: args{command: []string{"--config", "../testing/.cobbler.yaml", "file", "report", "--name", "test-file-report"}}, - want: ": test-file-report", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Cleanup - fileName := "test-file-report" - var err error - defer func() { - // Client is initialized since this is the cleanup - cleanupErr := removeFile(Client, fileName) - cobbler.FailOnError(t, cleanupErr) - }() - // Arrange - setupClient(t) - _, err = createFile(Client, fileName) - cobbler.FailOnError(t, err) - cobra.OnInitialize(initConfig, setupLogger) - rootCmd := NewRootCmd() - rootCmd.SetArgs(tt.args.command) - stdout := bytes.NewBufferString("") - stderr := bytes.NewBufferString("") - rootCmd.SetOut(stdout) - rootCmd.SetErr(stderr) - - // Act - err = rootCmd.Execute() - - // Assert - cobbler.FailOnError(t, err) - FailOnNonEmptyStream(t, stderr) - stdoutBytes, err := io.ReadAll(stdout) - if err != nil { - t.Fatal(err) - } - stdoutString := string(stdoutBytes) - if !strings.Contains(stdoutString, tt.want) { - fmt.Println(stdoutString) - t.Fatal("No Event ID present") - } - }) - } -} diff --git a/cmd/group_common.go b/cmd/group_common.go new file mode 100644 index 0000000..60d6fce --- /dev/null +++ b/cmd/group_common.go @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright SUSE LLC + +package cmd + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "gopkg.in/yaml.v3" +) + +// groupStringSliceFlagMetadata holds the shared --items flag for the three +// 4.0.0 group item types (distro_group, profile_group, system_group). +var groupStringSliceFlagMetadata = map[string]FlagMetadata[[]string]{ + "items": { + Name: "items", + DefaultValue: []string{}, + Usage: "member item names (comma delimited)", + }, +} + +// addGroupFlagSet registers the common flag set used by every group subcommand. +func addGroupFlagSet(cmd *cobra.Command) { + addCommonArgs(cmd) + addStringSliceFlags(cmd, groupStringSliceFlagMetadata) +} + +// extractGroupFlags reads the --items / --comment flags off cmd and returns +// the values. The bools indicate whether the user explicitly set that flag. +func extractGroupFlags(cmd *cobra.Command) (comment string, commentSet bool, items []string, itemsSet bool, err error) { + cmd.Flags().Visit(func(flag *pflag.Flag) { + if err != nil { + return + } + switch flag.Name { + case "comment": + comment, err = cmd.Flags().GetString("comment") + commentSet = true + case "items": + items, err = cmd.Flags().GetStringSlice("items") + itemsSet = true + } + }) + return +} + +// writeExport marshals v to stdout in the requested format (json or yaml). +func writeExport(cmd *cobra.Command, format string, v interface{}) error { + switch format { + case "json": + out, err := json.Marshal(v) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), string(out)) + case "yaml": + out, err := yaml.Marshal(v) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), "---") + fmt.Fprintln(cmd.OutOrStdout(), string(out)) + default: + return fmt.Errorf("format must be json or yaml") + } + return nil +} diff --git a/cmd/image.go b/cmd/image.go index 238430e..bc956e3 100644 --- a/cmd/image.go +++ b/cmd/image.go @@ -523,6 +523,7 @@ func NewImageFindCmd() *cobra.Command { addStringFlags(imageFindCmd, findStringFlagMetadata) addIntFlags(imageFindCmd, findIntFlagMetadata) addFloatFlags(imageFindCmd, findFloatFlagMetadata) + addPaginationFlags(imageFindCmd) return imageFindCmd } diff --git a/cmd/interface.go b/cmd/interface.go index f61e70f..1919130 100644 --- a/cmd/interface.go +++ b/cmd/interface.go @@ -1,197 +1,159 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright SUSE LLC + package cmd import ( "encoding/json" "fmt" + "sort" + "strings" + cobbler "github.com/cobbler/cobblerclient" "github.com/spf13/cobra" "github.com/spf13/pflag" "gopkg.in/yaml.v3" - "strings" ) -func updateNetworkInterfaceFromFlags(cmd *cobra.Command, networkInterface *cobbler.Interface) error { +// parseNetworkInterfaceType converts the CLI string form of an interface type +// (na, bond, bond_slave, bridge, ...) to the typed enum used by cobblerclient. +func parseNetworkInterfaceType(s string) (cobbler.NetworkInterfaceType, error) { + switch strings.ToLower(s) { + case "", "na": + return cobbler.NetworkInterfaceTypeNA, nil + case "bond": + return cobbler.NetworkInterfaceTypeBond, nil + case "bond_slave": + return cobbler.NetworkInterfaceTypeBondSlave, nil + case "bridge": + return cobbler.NetworkInterfaceTypeBridge, nil + case "bridge_slave": + return cobbler.NetworkInterfaceTypeBridgeSlave, nil + case "bonded_bridge_slave": + return cobbler.NetworkInterfaceTypeBondedBridgeSlave, nil + case "infiniband": + return cobbler.NetworkInterfaceTypeInfiniband, nil + } + return cobbler.NetworkInterfaceTypeNA, fmt.Errorf("unknown interface type %q", s) +} + +// updateNetworkInterfaceFromFlags applies any --flags the user supplied on the +// command line to iface. Flag names mirror NetworkInterface field names with +// IPv4/IPv6/DNS prefixes used for the nested value objects. +func updateNetworkInterfaceFromFlags(cmd *cobra.Command, iface *cobbler.NetworkInterface) error { var err error cmd.Flags().Visit(func(flag *pflag.Flag) { if err != nil { - // If one of the previous flags has had an error just directly return. return } switch flag.Name { - // The rename & copy operations are special operations as such we cannot blindly set this inside here. - // Any rename & copy operation must be handled outside of this method. - case "bonding-opts": - var systemNewBondingOpts string - systemNewBondingOpts, err = cmd.Flags().GetString("bonding-opts") + // Top-level link-layer + case "mac-address": + iface.MacAddress, err = cmd.Flags().GetString("mac-address") + case "interface-type": + var v string + v, err = cmd.Flags().GetString("interface-type") if err != nil { return } - networkInterface.BondingOpts = systemNewBondingOpts + iface.InterfaceType, err = parseNetworkInterfaceType(v) + case "interface-master": + iface.InterfaceMaster, err = cmd.Flags().GetString("interface-master") + case "bonding-opts": + iface.BondingOpts, err = cmd.Flags().GetString("bonding-opts") case "bridge-opts": - var systemNewBridgeOpts string - systemNewBridgeOpts, err = cmd.Flags().GetString("bridge-opts") - if err != nil { - return - } - networkInterface.BridgeOpts = systemNewBridgeOpts - case "cnames": - var systemNewCNames []string - systemNewCNames, err = cmd.Flags().GetStringSlice("cnames") - if err != nil { - return - } - networkInterface.CNAMEs = systemNewCNames + iface.BridgeOpts, err = cmd.Flags().GetString("bridge-opts") case "connected-mode": - var systemNewConnectedMode bool - systemNewConnectedMode, err = cmd.Flags().GetBool("connected-mode") - if err != nil { - return - } - networkInterface.ConnectedMode = systemNewConnectedMode + iface.ConnectedMode, err = cmd.Flags().GetBool("connected-mode") + case "management": + iface.Management, err = cmd.Flags().GetBool("management") + case "static": + iface.Static, err = cmd.Flags().GetBool("static") case "dhcp-tag": - var systemNewDhcpTag string - systemNewDhcpTag, err = cmd.Flags().GetString("dhcp-tag") - if err != nil { - return - } - networkInterface.DHCPTag = systemNewDhcpTag - case "dns-name": - var systemNewDnsName string - systemNewDnsName, err = cmd.Flags().GetString("dns-name") - if err != nil { - return - } - networkInterface.DNSName = systemNewDnsName - case "if-gateway": - var systemNewIfGateway string - systemNewIfGateway, err = cmd.Flags().GetString("if-gateway") - if err != nil { - return - } - networkInterface.Gateway = systemNewIfGateway - case "interface-master": - var systemNewInterfaceMaster string - systemNewInterfaceMaster, err = cmd.Flags().GetString("interface-master") - if err != nil { - return - } - networkInterface.InterfaceMaster = systemNewInterfaceMaster - case "interface-type": - var systemNewInterfaceType string - systemNewInterfaceType, err = cmd.Flags().GetString("interface-type") - if err != nil { - return - } - networkInterface.InterfaceType = systemNewInterfaceType - case "ip-address": - var systemNewIpAddress string - systemNewIpAddress, err = cmd.Flags().GetString("ip-address") - if err != nil { - return - } - networkInterface.IPAddress = systemNewIpAddress + iface.DHCPTag, err = cmd.Flags().GetString("dhcp-tag") + case "mtu": + iface.MTU, err = cmd.Flags().GetString("mtu") + case "virt-bridge": + fallthrough + case "virt-bridge-inherit": + if cmd.Flags().Lookup("virt-bridge-inherit") != nil && + cmd.Flags().Lookup("virt-bridge-inherit").Changed { + iface.VirtBridge.Data = "" + iface.VirtBridge.IsInherited, err = cmd.Flags().GetBool("virt-bridge-inherit") + } else { + iface.VirtBridge.IsInherited = false + iface.VirtBridge.Data, err = cmd.Flags().GetString("virt-bridge") + } + // IPv4 + case "ipv4-address": + iface.IPv4.Address, err = cmd.Flags().GetString("ipv4-address") + case "ipv4-netmask": + iface.IPv4.Netmask, err = cmd.Flags().GetString("ipv4-netmask") + case "ipv4-gateway": + iface.IPv4.Gateway, err = cmd.Flags().GetString("ipv4-gateway") + case "ipv4-static-routes": + iface.IPv4.StaticRoutes, err = cmd.Flags().GetStringSlice("ipv4-static-routes") + // IPv6 case "ipv6-address": - var systemNewIpv6Address string - systemNewIpv6Address, err = cmd.Flags().GetString("ipv6-address") - if err != nil { - return - } - networkInterface.IPv6Address = systemNewIpv6Address - case "ipv6-default-gateway": - var systemNewIpv6DefaultGateway string - systemNewIpv6DefaultGateway, err = cmd.Flags().GetString("ipv6-default-gateway") - if err != nil { - return - } - networkInterface.IPv6DefaultGateway = systemNewIpv6DefaultGateway - case "ipv6-mtu": - var systemNewIpv6Mtu string - systemNewIpv6Mtu, err = cmd.Flags().GetString("ipv6-mtu") - if err != nil { - return - } - networkInterface.IPv6MTU = systemNewIpv6Mtu + iface.IPv6.Address, err = cmd.Flags().GetString("ipv6-address") case "ipv6-prefix": - var systemNewIpv6Prefix string - systemNewIpv6Prefix, err = cmd.Flags().GetString("ipv6-prefix") - if err != nil { - return - } - networkInterface.IPv6Prefix = systemNewIpv6Prefix + iface.IPv6.Prefix, err = cmd.Flags().GetString("ipv6-prefix") case "ipv6-secondaries": - var systemNewIpv6Secondaries []string - systemNewIpv6Secondaries, err = cmd.Flags().GetStringSlice("ipv6-secondaries") - if err != nil { - return - } - networkInterface.IPv6Secondaries = systemNewIpv6Secondaries + iface.IPv6.Secondaries, err = cmd.Flags().GetStringSlice("ipv6-secondaries") + case "ipv6-mtu": + iface.IPv6.MTU, err = cmd.Flags().GetString("ipv6-mtu") case "ipv6-static-routes": - var systemNewIpv6StaticRoutes []string - systemNewIpv6StaticRoutes, err = cmd.Flags().GetStringSlice("ipv6-static-routes") - if err != nil { - return - } - networkInterface.IPv6StaticRoutes = systemNewIpv6StaticRoutes - case "mac-address": - var systemNewMacAddress string - systemNewMacAddress, err = cmd.Flags().GetString("mac-address") - if err != nil { - return - } - networkInterface.MACAddress = systemNewMacAddress - case "management": - var systemNewManagement bool - systemNewManagement, err = cmd.Flags().GetBool("management") - if err != nil { - return - } - networkInterface.Management = systemNewManagement - case "mtu": - var systemNewMtu string - systemNewMtu, err = cmd.Flags().GetString("mtu") - if err != nil { - return - } - networkInterface.MTU = systemNewMtu - case "netmask": - var systemNewNetmask string - systemNewNetmask, err = cmd.Flags().GetString("netmask") - if err != nil { - return - } - networkInterface.Netmask = systemNewNetmask - case "static": - var systemNewStatic bool - systemNewStatic, err = cmd.Flags().GetBool("static") - if err != nil { - return - } - networkInterface.Static = systemNewStatic - case "static-routes": - var systemNewStaticRoutes []string - systemNewStaticRoutes, err = cmd.Flags().GetStringSlice("static-routes") - if err != nil { - return - } - networkInterface.StaticRoutes = systemNewStaticRoutes - case "virt-bridge": - var systemNewVirtBridge string - systemNewVirtBridge, err = cmd.Flags().GetString("virt-bridge") - if err != nil { - return - } - networkInterface.VirtBridge = systemNewVirtBridge + iface.IPv6.StaticRoutes, err = cmd.Flags().GetStringSlice("ipv6-static-routes") + case "ipv6-default-gateway": + iface.IPv6.DefaultGateway, err = cmd.Flags().GetString("ipv6-default-gateway") + // DNS + case "dns-name": + iface.DNS.Name, err = cmd.Flags().GetString("dns-name") + case "dns-cnames": + iface.DNS.CNames, err = cmd.Flags().GetStringSlice("dns-cnames") } }) - // Don't blindly return nil because maybe one of the flags had an issue retrieving an argument. return err } +// addInterfaceFlagSet registers the full set of interface configuration flags +// onto a subcommand. +func addInterfaceFlagSet(cmd *cobra.Command) { + addStringFlags(cmd, interfaceStringFlagMetadata) + addBoolFlags(cmd, interfaceBoolFlagMetadata) + addStringSliceFlags(cmd, interfaceStringSliceFlagMetadata) +} + +// resolveSystemUid looks up the UID of a system, given either --system-uid or +// --system-name. Returns an error if neither is set. +func resolveSystemUid(cmd *cobra.Command) (string, error) { + systemUid, err := cmd.Flags().GetString("system-uid") + if err != nil { + return "", err + } + if systemUid != "" { + return systemUid, nil + } + systemName, err := cmd.Flags().GetString("system-name") + if err != nil { + return "", err + } + if systemName == "" { + return "", fmt.Errorf("one of --system-name or --system-uid is required") + } + system, err := Client.GetSystem(systemName, false, false) + if err != nil { + return "", err + } + return system.Uid, nil +} + +// NewInterfaceCommand builds the `cobbler interface` command and its subtree. func NewInterfaceCommand() (*cobra.Command, error) { interfaceCmd := &cobra.Command{ Use: "interface", - Short: "Manage interfaces", - Long: "Let's you manage network interfaces for systems.", + Short: "Manage network interfaces", + Long: `Manage network interfaces as first-class Cobbler 4.0.0 items.`, } interfaceCmd.AddCommand(NewInterfaceAddCommand()) interfaceCmd.AddCommand(NewInterfaceCopyCommand()) @@ -206,415 +168,342 @@ func NewInterfaceCommand() (*cobra.Command, error) { } func NewInterfaceAddCommand() *cobra.Command { - interfaceAddCmd := &cobra.Command{ - Use: "add", + cmd := &cobra.Command{ + Use: "add", + Short: "add a network interface", RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { + if err := generateCobblerClient(); err != nil { return err } - - systemName, err := cmd.Flags().GetString("system-name") + name, err := cmd.Flags().GetString("name") if err != nil { return err } - networkInterfaceName, err := cmd.Flags().GetString("interface-name") + if name == "" { + return fmt.Errorf("--name is required") + } + systemUid, err := resolveSystemUid(cmd) if err != nil { return err } - - systemObject, err := Client.GetSystem(systemName, false, false) - if err != nil { + iface := cobbler.NewNetworkInterface() + iface.Name = name + iface.SystemUid = systemUid + if err := updateNetworkInterfaceFromFlags(cmd, &iface); err != nil { return err } - - networkInterface := cobbler.NewInterface() - err = updateNetworkInterfaceFromFlags(cmd, &networkInterface) + created, err := Client.CreateNetworkInterface(systemUid, iface) if err != nil { return err } - - return systemObject.CreateInterface(networkInterfaceName, networkInterface) + fmt.Fprintf(cmd.OutOrStdout(), "Network interface %s created\n", created.Name) + return nil }, } - interfaceAddCmd.Flags().String("interface-name", "", "the interface to operate on") - interfaceAddCmd.Flags().String("system-name", "", "the system to operate on") - addStringFlags(interfaceAddCmd, interfaceStringFlagMetadata) - addBoolFlags(interfaceAddCmd, interfaceBoolFlagMetadata) - addStringSliceFlags(interfaceAddCmd, interfaceStringSliceFlagMetadata) - return interfaceAddCmd + cmd.Flags().String("name", "", "the network interface name") + cmd.Flags().String("system-name", "", "the parent system name (resolved to UID)") + cmd.Flags().String("system-uid", "", "the parent system UID") + addInterfaceFlagSet(cmd) + return cmd } -func NewInterfaceCopyCommand() *cobra.Command { - interfaceCopyCmd := &cobra.Command{ - Use: "copy", +func NewInterfaceEditCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "edit", + Short: "edit a network interface", RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { + if err := generateCobblerClient(); err != nil { return err } - - systemName, err := cmd.Flags().GetString("system-name") - if err != nil { - return err - } - networkInterfaceName, err := cmd.Flags().GetString("interface-name") + name, err := cmd.Flags().GetString("name") if err != nil { return err } - newNetworkInterfaceName, err := cmd.Flags().GetString("new-interface-name") + iface, err := Client.GetNetworkInterface(name, false, false) if err != nil { return err } - - systemObject, err := Client.GetSystem(systemName, false, false) - if err != nil { + if err := updateNetworkInterfaceFromFlags(cmd, iface); err != nil { return err } - networkInterfaceObject, err := systemObject.GetInterface(networkInterfaceName) - if err != nil { - return err - } - networkInterfaceObject.MACAddress = "" - networkInterfaceObject.IPAddress = "" - networkInterfaceObject.IPv6Address = "" - err = systemObject.CreateInterface(newNetworkInterfaceName, networkInterfaceObject) - if err != nil { - return err - } - - return nil + return Client.UpdateNetworkInterface(iface) }, } - interfaceCopyCmd.Flags().String("interface-name", "", "the interface to operate on") - interfaceCopyCmd.Flags().String("system-name", "", "the system to operate on") - interfaceCopyCmd.Flags().String("new-interface-name", "", "the new name for the network interface") - return interfaceCopyCmd + cmd.Flags().String("name", "", "the network interface name") + addInterfaceFlagSet(cmd) + return cmd } -func NewInterfaceEditCommand() *cobra.Command { - interfaceEditCmd := &cobra.Command{ - Use: "edit", +func NewInterfaceCopyCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "copy", + Short: "copy a network interface", RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { + if err := generateCobblerClient(); err != nil { return err } - - systemName, err := cmd.Flags().GetString("system-name") + name, err := cmd.Flags().GetString("name") if err != nil { return err } - networkInterfaceName, err := cmd.Flags().GetString("interface-name") + newName, err := cmd.Flags().GetString("newname") if err != nil { return err } - - systemHandle, err := Client.GetSystemHandle(systemName) + handle, err := Client.GetNetworkInterfaceHandle(name) if err != nil { return err } - - editedProperties := make(map[string]interface{}) - cmd.Flags().Visit(func(flag *pflag.Flag) { - propertyName := fmt.Sprintf("%s-%s", networkInterfaceName, strings.Replace(flag.Name, "-", "_", -1)) - editedProperties[propertyName] = flag.Value - }) - err = Client.ModifyInterface(systemHandle, editedProperties) - if err != nil { + if err := Client.CopyNetworkInterface(handle, newName); err != nil { return err } - return nil - }, - } - interfaceEditCmd.Flags().String("interface-name", "", "the interface to operate on") - interfaceEditCmd.Flags().String("system-name", "", "the system to operate on") - addStringFlags(interfaceEditCmd, interfaceStringFlagMetadata) - addBoolFlags(interfaceEditCmd, interfaceBoolFlagMetadata) - addStringSliceFlags(interfaceEditCmd, interfaceStringSliceFlagMetadata) - return interfaceEditCmd -} - -func NewInterfaceFindCommand() *cobra.Command { - interfaceFindCmd := &cobra.Command{ - Use: "find", - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() + // Clear identity-bound fields on the copy by default; users can + // re-set them via --mac-address / --ipv4-address / --ipv6-address. + fresh, err := Client.GetNetworkInterface(newName, false, false) if err != nil { return err } - - return FindItemNames(cmd, args, "system") + fresh.MacAddress = "" + fresh.IPv4.Address = "" + fresh.IPv6.Address = "" + if err := updateNetworkInterfaceFromFlags(cmd, fresh); err != nil { + return err + } + return Client.UpdateNetworkInterface(fresh) }, } - // Network interface flags - addStringFlags(interfaceFindCmd, interfaceStringFlagMetadata) - addBoolFlags(interfaceFindCmd, interfaceBoolFlagMetadata) - addStringSliceFlags(interfaceFindCmd, interfaceStringSliceFlagMetadata) - interfaceFindCmd.Flags().String("name", "", "the system to operate on") - interfaceFindCmd.Flags().String("interface", "", "the interface to operate on") - return interfaceFindCmd -} - -func printNetworkInterfaceNames(cmd *cobra.Command, system *cobbler.System) error { - networkInterfaces, err := system.GetInterfaces() - if err != nil { - return err - } - fmt.Fprintf(cmd.OutOrStdout(), "%s:\n", system.Name) - for interfaceName := range networkInterfaces { - fmt.Fprintf(cmd.OutOrStdout(), " %s\n", interfaceName) - } - return nil + cmd.Flags().String("name", "", "the network interface to copy") + cmd.Flags().String("newname", "", "the new interface name") + addInterfaceFlagSet(cmd) + return cmd } -func NewInterfaceListCommand() *cobra.Command { - interfaceListCmd := &cobra.Command{ - Use: "list", +func NewInterfaceRenameCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "rename", + Short: "rename a network interface", RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() + if err := generateCobblerClient(); err != nil { + return err + } + name, err := cmd.Flags().GetString("name") if err != nil { return err } - - systems, err := Client.GetSystems() + newName, err := cmd.Flags().GetString("newname") if err != nil { return err } - for _, system := range systems { - err = printNetworkInterfaceNames(cmd, system) - if err != nil { - return err - } + handle, err := Client.GetNetworkInterfaceHandle(name) + if err != nil { + return err } - return nil + return Client.RenameNetworkInterface(handle, newName) }, } - return interfaceListCmd + cmd.Flags().String("name", "", "the network interface to rename") + cmd.Flags().String("newname", "", "the new interface name") + return cmd } func NewInterfaceRemoveCommand() *cobra.Command { - interfaceRemoveCmd := &cobra.Command{ - Use: "remove", + cmd := &cobra.Command{ + Use: "remove", + Short: "remove a network interface", RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { + if err := generateCobblerClient(); err != nil { return err } - - systemName, err := cmd.Flags().GetString("system-name") - if err != nil { - return err - } - networkInterfaceName, err := cmd.Flags().GetString("interface-name") + name, err := cmd.Flags().GetString("name") if err != nil { return err } + return Client.DeleteNetworkInterface(name) + }, + } + cmd.Flags().String("name", "", "the network interface to remove") + return cmd +} - systemHandle, err := Client.GetSystemHandle(systemName) - if err != nil { - return err - } - err = Client.DeleteNetworkInterface(systemHandle, networkInterfaceName) - if err != nil { +func NewInterfaceFindCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "find", + Short: "find network interfaces", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { return err } - - return nil + return FindItemNames(cmd, args, "network_interface") }, } - interfaceRemoveCmd.Flags().String("interface-name", "", "the interface to operate on") - interfaceRemoveCmd.Flags().String("system-name", "", "the system to operate on") - return interfaceRemoveCmd + addInterfaceFlagSet(cmd) + cmd.Flags().String("name", "", "match by interface name") + cmd.Flags().String("system-name", "", "filter by parent system name") + cmd.Flags().String("system-uid", "", "filter by parent system UID") + addPaginationFlags(cmd) + return cmd } -func NewInterfaceRenameCommand() *cobra.Command { - interfaceRenameCmd := &cobra.Command{ - Use: "rename", +func NewInterfaceListCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "list network interfaces grouped by system", RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { + if err := generateCobblerClient(); err != nil { return err } - - systemName, err := cmd.Flags().GetString("system-name") + interfaces, err := Client.GetNetworkInterfaces() if err != nil { return err } - networkInterfaceName, err := cmd.Flags().GetString("interface-name") - if err != nil { - return err + grouped := make(map[string][]string) + for _, iface := range interfaces { + grouped[iface.SystemName] = append(grouped[iface.SystemName], iface.Name) } - newNetworkInterfaceName, err := cmd.Flags().GetString("new-interface-name") - if err != nil { - return err + systemNames := make([]string, 0, len(grouped)) + for systemName := range grouped { + systemNames = append(systemNames, systemName) } - - err = Client.RenameNetworkInterface(systemName, networkInterfaceName, newNetworkInterfaceName) - if err != nil { - return err + sort.Strings(systemNames) + for _, systemName := range systemNames { + ifaceNames := grouped[systemName] + sort.Strings(ifaceNames) + fmt.Fprintf(cmd.OutOrStdout(), "%s:\n", systemName) + for _, n := range ifaceNames { + fmt.Fprintf(cmd.OutOrStdout(), " %s\n", n) + } } - return nil }, } - interfaceRenameCmd.Flags().String("interface-name", "", "the interface to operate on") - interfaceRenameCmd.Flags().String("system-name", "", "the system to operate on") - interfaceRenameCmd.Flags().String("new-interface-name", "", "the new name for the network interface") - return interfaceRenameCmd + return cmd } func NewInterfaceReportCommand() *cobra.Command { - interfaceReportCmd := &cobra.Command{ - Use: "report", + cmd := &cobra.Command{ + Use: "report", + Short: "show network interface details", RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { + if err := generateCobblerClient(); err != nil { return err } - - systemName, err := cmd.Flags().GetString("system-name") + name, err := cmd.Flags().GetString("name") if err != nil { return err } - interfaceName, err := cmd.Flags().GetString("interface-name") + systemName, err := cmd.Flags().GetString("system-name") if err != nil { return err } - itemNames := make([]string, 0) - if systemName == "" { - itemNames, err = Client.ListSystemNames() + + var interfaces []*cobbler.NetworkInterface + switch { + case name != "": + iface, err := Client.GetNetworkInterface(name, false, false) + if err != nil { + return err + } + interfaces = []*cobbler.NetworkInterface{iface} + case systemName != "": + interfaces, err = Client.FindNetworkInterface(map[string]interface{}{ + "system_name": systemName, + }) + if err != nil { + return err + } + default: + interfaces, err = Client.GetNetworkInterfaces() if err != nil { return err } - } else { - itemNames = append(itemNames, systemName) - } - return reportNetworkInterfaces(cmd, itemNames, interfaceName) - }, - } - interfaceReportCmd.Flags().String("interface-name", "", "the interface to operate on") - interfaceReportCmd.Flags().String("system-name", "", "the system to operate on") - return interfaceReportCmd -} - -func reportNetworkInterfaces(cmd *cobra.Command, systemNames []string, interfaceName string) error { - for _, itemName := range systemNames { - system, err := Client.GetSystem(itemName, false, false) - if err != nil { - return err - } - fmt.Fprintf(cmd.OutOrStdout(), "%s:\n", itemName) - if interfaceName == "" { - networkInterfaces, err := system.GetInterfaces() - if err != nil { - return err - } - for networkInterfaceName := range networkInterfaces { - networkInterface := system.Interfaces[networkInterfaceName] - printStructured(cmd, &networkInterface) } - fmt.Fprintln(cmd.OutOrStdout(), "") - } else { - networkInterface, err := system.GetInterface(interfaceName) - if err != nil { - fmt.Fprintf(cmd.OutOrStdout(), " Interface %s not found on system", interfaceName) - continue + for _, iface := range interfaces { + printStructured(cmd, iface) + fmt.Fprintln(cmd.OutOrStdout(), "") } - printStructured(cmd, &networkInterface) - } + return nil + }, } - return nil + cmd.Flags().String("name", "", "the network interface name") + cmd.Flags().String("system-name", "", "filter by parent system name") + return cmd } func NewInterfaceExportCmd() *cobra.Command { - networkInterfaceExportCmd := &cobra.Command{ + cmd := &cobra.Command{ Use: "export", Short: "export network interfaces", - Long: `Export network interfaces.`, PreRunE: func(cmd *cobra.Command, args []string) error { - formatOption, err := cmd.Flags().GetString("format") + format, err := cmd.Flags().GetString("format") if err != nil { return err } - if formatOption != "json" && formatOption != "yaml" { + if format != "json" && format != "yaml" { return fmt.Errorf("format must be json or yaml") } return nil }, RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { + if err := generateCobblerClient(); err != nil { return err } - - systemName, err := cmd.Flags().GetString("system-name") + name, err := cmd.Flags().GetString("name") if err != nil { return err } - interfaceName, err := cmd.Flags().GetString("interface-name") + systemName, err := cmd.Flags().GetString("system-name") if err != nil { return err } - formatOption, err := cmd.Flags().GetString("format") + format, err := cmd.Flags().GetString("format") if err != nil { return err } - itemNames := make([]string, 0) - if systemName == "" { - itemNames, err = Client.ListSystemNames() + var interfaces []*cobbler.NetworkInterface + switch { + case name != "": + iface, err := Client.GetNetworkInterface(name, false, false) if err != nil { return err } - } else { - itemNames = append(itemNames, systemName) - } - - for _, itemName := range itemNames { - system, err := Client.GetSystem(itemName, false, false) + interfaces = []*cobbler.NetworkInterface{iface} + case systemName != "": + interfaces, err = Client.FindNetworkInterface(map[string]interface{}{ + "system_name": systemName, + }) if err != nil { return err } - - var systemInterfaces cobbler.Interfaces - if interfaceName == "" { - systemInterfaces = system.Interfaces - } else { - systemInterfaces = make(map[string]cobbler.Interface) - intf, interfaceExists := system.Interfaces[interfaceName] - if interfaceExists { - systemInterfaces[interfaceName] = intf - } - } - exportData := struct { - SystemName string `json:"system_name" yaml:"system_name"` - Interfaces cobbler.Interfaces - }{ - itemName, - systemInterfaces, + default: + interfaces, err = Client.GetNetworkInterfaces() + if err != nil { + return err } - if formatOption == "json" { - jsonDocument, err := json.Marshal(exportData) + } + + for _, iface := range interfaces { + switch format { + case "json": + out, err := json.Marshal(iface) if err != nil { return err } - fmt.Fprintln(cmd.OutOrStdout(), string(jsonDocument)) - } - if formatOption == "yaml" { - yamlDocument, err := yaml.Marshal(exportData) + fmt.Fprintln(cmd.OutOrStdout(), string(out)) + case "yaml": + out, err := yaml.Marshal(iface) if err != nil { return err } fmt.Fprintln(cmd.OutOrStdout(), "---") - fmt.Fprintln(cmd.OutOrStdout(), string(yamlDocument)) + fmt.Fprintln(cmd.OutOrStdout(), string(out)) } } return nil }, } - networkInterfaceExportCmd.Flags().String("interface-name", "", "the network interface name") - networkInterfaceExportCmd.Flags().String("system-name", "", "the system name") - networkInterfaceExportCmd.Flags().String(exportStringMetadata["format"].Name, exportStringMetadata["format"].DefaultValue, exportStringMetadata["format"].Usage) - return networkInterfaceExportCmd + cmd.Flags().String("name", "", "the network interface name") + cmd.Flags().String("system-name", "", "filter by parent system name") + cmd.Flags().String(exportStringMetadata["format"].Name, exportStringMetadata["format"].DefaultValue, exportStringMetadata["format"].Usage) + return cmd } diff --git a/cmd/item.go b/cmd/item.go index 7a8cc53..435c469 100644 --- a/cmd/item.go +++ b/cmd/item.go @@ -116,25 +116,60 @@ func RemoveItemRecursive(cmd *cobra.Command, args []string, what string) error { return Client.RemoveItem(what, itemName, recursiveDelete) } -// FindItemNames accesses the given flags and attempts to perform a search for the given item type +// addPaginationFlags registers --page and --items-per-page on a find subcommand. +// When either is supplied the find handler routes through Client.FindItemsPaged +// and emits a trailing `# page N of M (T total)` summary line. +func addPaginationFlags(cmd *cobra.Command) { + cmd.Flags().Int("page", 0, "page number for paginated find (1-based; omit for unpaged)") + cmd.Flags().Int("items-per-page", 0, "results per page (only honoured when --page is set)") +} + +// FindItemNames accesses the given flags and performs a search for items of the +// given type. When --page is specified it uses the paginated backend endpoint +// and prints a trailing summary line; otherwise it falls back to the unpaged +// find. func FindItemNames(cmd *cobra.Command, args []string, what string) error { _ = args + page, _ := cmd.Flags().GetInt("page") + itemsPerPage, _ := cmd.Flags().GetInt("items-per-page") criteria := make(map[string]interface{}) cmd.Flags().Visit(func(flag *pflag.Flag) { - if flag.Name == "config" { + switch flag.Name { + case "config", "page", "items-per-page": return } key := strings.Replace(flag.Name, "-", "_", -1) criteria[key] = flag.Value.String() }) - // Now perform the actual search + if page > 0 { + if itemsPerPage <= 0 { + itemsPerPage = 20 + } + result, err := Client.FindItemsPaged(what, criteria, "name", int32(page), int32(itemsPerPage)) + if err != nil { + return err + } + for _, raw := range result.FoundItems { + if asMap, ok := raw.(map[string]interface{}); ok { + if name, ok := asMap["name"].(string); ok { + fmt.Fprintln(cmd.OutOrStdout(), name) + continue + } + } + fmt.Fprintln(cmd.OutOrStdout(), raw) + } + fmt.Fprintf(cmd.OutOrStdout(), "# page %d of %d (%d total)\n", + result.PageInfo.Page, result.PageInfo.NumPages, result.PageInfo.NumItems) + return nil + } + itemNames, err := Client.FindItemNames(what, criteria, "name") if err != nil { return err } - for _, distroName := range itemNames { - fmt.Fprintln(cmd.OutOrStdout(), distroName) + for _, name := range itemNames { + fmt.Fprintln(cmd.OutOrStdout(), name) } return nil } diff --git a/cmd/list.go b/cmd/list.go index 0a64930..74def3e 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -16,7 +16,7 @@ func NewListCmd() *cobra.Command { Use: "list", Short: "List configuration", Long: `Lists all configuration which Cobbler can obtain from the saved data. There are also report subcommands for -most of the other Cobbler commands (currently: distro, profile, system, repo, image, mgmtclass, package, file, menu). +most of the other Cobbler commands (currently: distro, profile, system, repo, image, menu). Identical to 'cobbler report'`, RunE: func(cmd *cobra.Command, args []string) error { @@ -45,18 +45,6 @@ Identical to 'cobbler report'`, if err != nil { return err } - mgmtClassNames, err := Client.ListMgmtClassNames() - if err != nil { - return err - } - packageNames, err := Client.ListPackageNames() - if err != nil { - return err - } - fileNames, err := Client.ListFileNames() - if err != nil { - return err - } menuNames, err := Client.ListMenuNames() if err != nil { return err @@ -66,9 +54,6 @@ Identical to 'cobbler report'`, listItems(cmd, "systems", systemNames) listItems(cmd, "repos", repoNames) listItems(cmd, "images", imageNames) - listItems(cmd, "mgmtclasses", mgmtClassNames) - listItems(cmd, "packages", packageNames) - listItems(cmd, "files", fileNames) listItems(cmd, "menus", menuNames) return nil }, diff --git a/cmd/menu.go b/cmd/menu.go index 0efa43a..bce3317 100644 --- a/cmd/menu.go +++ b/cmd/menu.go @@ -235,6 +235,7 @@ func NewMenuFindCmd() *cobra.Command { addStringFlags(menuFindCmd, findStringFlagMetadata) addIntFlags(menuFindCmd, findIntFlagMetadata) addFloatFlags(menuFindCmd, findFloatFlagMetadata) + addPaginationFlags(menuFindCmd) return menuFindCmd } diff --git a/cmd/metadata.go b/cmd/metadata.go index b291307..fcbc902 100644 --- a/cmd/metadata.go +++ b/cmd/metadata.go @@ -87,11 +87,6 @@ var distroStringSliceFlagMetadata = map[string]FlagMetadata[[]string]{ Usage: "boot loaders (network installation boot loaders)", IsInheritable: true, }, - "mgmt-classes": { - Name: "mgmt-classes", - DefaultValue: []string{}, - Usage: "management classes (for external config management)", - }, } var distroMapFlagMetadata = map[string]FlagMetadata[map[string]string]{ @@ -270,13 +265,7 @@ var profileStringSliceFlagMetadata = map[string]FlagMetadata[[]string]{ }, } -var profileMapFlagMetadata = map[string]FlagMetadata[map[string]string]{ - "mgmt-parameters": { - Name: "mgmt-parameters", - DefaultValue: map[string]string{}, - Usage: "Parameters which will be handed to your management application (must be a valid YAML dictionary))", - }, -} +var profileMapFlagMetadata = map[string]FlagMetadata[map[string]string]{} var systemStringFlagMetadata = map[string]FlagMetadata[string]{ "autoinstall": { @@ -434,11 +423,6 @@ var systemStringSliceFlagMetadata = map[string]FlagMetadata[[]string]{ Usage: "boot loaders (network installation boot loaders)", IsInheritable: true, }, - "mgmt-classes": { - Name: "mgmt-classes", - DefaultValue: []string{}, - Usage: "management classes (for external config management)", - }, "name-servers": { Name: "name-servers", DefaultValue: []string{}, @@ -488,11 +472,6 @@ var systemMapFlagMetadata = map[string]FlagMetadata[map[string]string]{ Usage: "template files (file mappings for built-in config management)", IsInheritable: true, }, - "mgmt-parameters": { - Name: "mgmt-parameters", - DefaultValue: map[string]string{}, - Usage: "Parameters which will be handed to your management application (must be a valid YAML dictionary))", - }, } var systemPowerStringFlagMetadata = map[string]FlagMetadata[string]{ @@ -533,16 +512,20 @@ var systemPowerStringFlagMetadata = map[string]FlagMetadata[string]{ }, } +// interfaceStringFlagMetadata holds the string flags for the dedicated +// `cobbler interface` command. In Cobbler 4.0.0 the IPv4/IPv6/DNS configuration +// lives in nested value objects on NetworkInterface; the flag names are +// dot-separated by convention (--ipv4-address sets IPv4.Address, etc.). var interfaceStringFlagMetadata = map[string]FlagMetadata[string]{ "bonding-opts": { Name: "bonding-opts", DefaultValue: "", - Usage: "bonding opts (should be used with --interface)", + Usage: "bonding opts", }, "bridge-opts": { Name: "bridge-opts", DefaultValue: "", - Usage: "bridge opts (should be used with --interface)", + Usage: "bridge opts", }, "dhcp-tag": { Name: "dhcp-tag", @@ -552,63 +535,68 @@ var interfaceStringFlagMetadata = map[string]FlagMetadata[string]{ "dns-name": { Name: "dns-name", DefaultValue: "", - Usage: "DNS name (should be used with --interface)", - }, - "if-gateway": { - Name: "if-gateway", - DefaultValue: "", - Usage: "per-Interface Gateway (should be used with --interface)", + Usage: "DNS name", }, "interface-master": { Name: "interface-master", DefaultValue: "", - Usage: "master interface (Should be used with --interface)", + Usage: "master interface", }, "interface-type": { Name: "interface-type", + DefaultValue: "na", + Usage: "interface type (na,bond,bond_slave,bridge,bridge_slave,bonded_bridge_slave,infiniband)", + }, + "ipv4-address": { + Name: "ipv4-address", + DefaultValue: "", + Usage: "IPv4 address", + }, + "ipv4-netmask": { + Name: "ipv4-netmask", DefaultValue: "", - Usage: `interface Type. Valid options: na,bond,bond_slave,bridge,bridge_slave,bonded_bridge_slave,bmc,infiniband. - (should be used with --interface)`, + Usage: "IPv4 subnet mask", }, - "ip-address": { - Name: "ip-address", + "ipv4-gateway": { + Name: "ipv4-gateway", DefaultValue: "", - Usage: "IPv4 address (should be used with --interface)", + Usage: "per-interface IPv4 gateway", }, "ipv6-address": { Name: "ipv6-address", DefaultValue: "", - Usage: "IPv6 address (should be used with --interface)", + Usage: "IPv6 address", }, - "ipv6-default-gateway": { - Name: "ipv6-default-gateway", + "ipv6-prefix": { + Name: "ipv6-prefix", DefaultValue: "", - Usage: "IPv6 Default Gateway (should be used with --interface)", + Usage: "IPv6 prefix", }, "ipv6-mtu": { Name: "ipv6-mtu", DefaultValue: "", Usage: "IPv6 MTU", }, - "ipv6-prefix": { - Name: "ipv6-prefix", + "ipv6-default-gateway": { + Name: "ipv6-default-gateway", DefaultValue: "", - Usage: "IPv6 Prefix (should be used with --interface)", + Usage: "IPv6 default gateway", }, "mac-address": { Name: "mac-address", DefaultValue: "", - Usage: "MAC Address (place 'random' in this field for a random MAC Address.)", + Usage: "MAC address (use 'random' for a random MAC address)", }, "mtu": { Name: "mtu", DefaultValue: "", - Usage: "MTU (should be used with --interface)", + Usage: "MTU", }, - "netmask": { - Name: "netmask", - DefaultValue: "", - Usage: "Subnet mask (should be used with --interface)", + "virt-bridge": { + Name: "virt-bridge", + DefaultValue: "", + Usage: "virt bridge", + IsInheritable: true, }, } @@ -616,40 +604,40 @@ var interfaceBoolFlagMetadata = map[string]FlagMetadata[bool]{ "connected-mode": { Name: "connected-mode", DefaultValue: false, - Usage: "InfiniBand connected mode (should be used with --interface)", + Usage: "InfiniBand connected mode", }, "management": { Name: "management", DefaultValue: false, - Usage: "declares the interface as management interface (should be used with --interface)", + Usage: "declares the interface as a management interface", }, "static": { Name: "static", DefaultValue: false, - Usage: "Is this interface static? (should be used with --interface)", + Usage: "is this interface static?", }, } var interfaceStringSliceFlagMetadata = map[string]FlagMetadata[[]string]{ - "cnames": { - Name: "cnames", + "dns-cnames": { + Name: "dns-cnames", + DefaultValue: []string{}, + Usage: "canonical name records (comma delimited)", + }, + "ipv4-static-routes": { + Name: "ipv4-static-routes", DefaultValue: []string{}, - Usage: "Cannonical Name Records, should be used with --interface (comma delimited)", + Usage: "IPv4 static routes (comma delimited)", }, "ipv6-secondaries": { Name: "ipv6-secondaries", DefaultValue: []string{}, - Usage: "IPv6 Secondaries (should be used with --interface)", + Usage: "IPv6 secondary addresses (comma delimited)", }, "ipv6-static-routes": { Name: "ipv6-static-routes", DefaultValue: []string{}, - Usage: "IPv6 Static Routes (should be used with --interface)", - }, - "static-routes": { - Name: "static-routes", - DefaultValue: []string{}, - Usage: "static routes (should be used with --interface)", + Usage: "IPv6 static routes (comma delimited)", }, } @@ -766,102 +754,6 @@ var menuStringFlagMetadata = map[string]FlagMetadata[string]{ }, } -var fileStringFlagMetadata = map[string]FlagMetadata[string]{ - "action": { - Name: "action", - DefaultValue: "", - Usage: "create or remove file resource", - }, - "mode": { - Name: "mode", - DefaultValue: "", - Usage: "file modes", - }, - "template": { - Name: "template", - DefaultValue: "", - Usage: "the template for the file", - }, - "path": { - Name: "path", - DefaultValue: "", - Usage: "the path of the file", - }, - "group": { - Name: "group", - DefaultValue: "", - Usage: "file owner group in file system", - }, - "owner": { - Name: "owner", - DefaultValue: "", - Usage: "file owner user in file system", - }, -} - -var fileBoolFlagMetadata = map[string]FlagMetadata[bool]{ - "is-dir": { - Name: "is-dir", - DefaultValue: false, - Usage: "treat file resource as a directory", - }, -} - -var packageStringFlagMetadata = map[string]FlagMetadata[string]{ - "action": { - Name: "action", - DefaultValue: "", - Usage: "install or remove package resource", - }, - "installer": { - Name: "installer", - DefaultValue: "", - Usage: "package manager", - }, - "version": { - Name: "version", - DefaultValue: "", - Usage: "package version", - }, -} - -var mgmtclassStringFlagMetadata = map[string]FlagMetadata[string]{ - "class-name": { - Name: "class-name", - DefaultValue: "", - Usage: "actual class name (leave blank to use the name field)", - }, -} - -var mgmtclassBoolFlagMetadata = map[string]FlagMetadata[bool]{ - "is-definition": { - Name: "is-definition", - DefaultValue: false, - Usage: "is Definition? Treat this class as a definition (puppet only)", - }, -} - -var mgmtclassStringSliceFlagMetadata = map[string]FlagMetadata[[]string]{ - "files": { - Name: "files", - DefaultValue: []string{}, - Usage: "file resources", - }, - "packages": { - Name: "packages", - DefaultValue: []string{}, - Usage: "package resources", - }, -} - -var mgmtclassStringMapFlagMetadata = map[string]FlagMetadata[map[string]string]{ - "params": { - Name: "params", - DefaultValue: make(map[string]string), - Usage: "list of parameters/variables", - }, -} - var repoStringFlagMetadata = map[string]FlagMetadata[string]{ "arch": { Name: "arch", diff --git a/cmd/mgmtclass.go b/cmd/mgmtclass.go deleted file mode 100644 index aaf8848..0000000 --- a/cmd/mgmtclass.go +++ /dev/null @@ -1,500 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// SPDX-FileCopyrightText: 2021 Dominik Gedon -// SPDX-FileCopyrightText: Copyright SUSE LLC - -package cmd - -import ( - "encoding/json" - "fmt" - cobbler "github.com/cobbler/cobblerclient" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "gopkg.in/yaml.v3" -) - -func updateMgmtClassFromFlags(cmd *cobra.Command, mgmtClass *cobbler.MgmtClass) error { - var inPlace bool - var err error - if cmd.Flags().Lookup("in-place") != nil { - inPlace, err = cmd.Flags().GetBool("in-place") - if err != nil { - return err - } - } - cmd.Flags().Visit(func(flag *pflag.Flag) { - if err != nil { - // If one of the previous flags has had an error just directly return. - return - } - switch flag.Name { - // The rename & copy operations are special operations as such we cannot blindly set this inside here. - // Any rename & copy operation must be handled outside of this method. - case "comment": - var mgmtClassNewComment string - mgmtClassNewComment, err = cmd.Flags().GetString("comment") - if err != nil { - return - } - mgmtClass.Comment = mgmtClassNewComment - case "owners": - fallthrough - case "owners-inherit": - if cmd.Flags().Lookup("owners-inherit").Changed { - mgmtClass.Owners.Data = []string{} - mgmtClass.Owners.IsInherited, err = cmd.Flags().GetBool("owners-inherit") - if err != nil { - return - } - } else { - var mgmtClassNewOwners []string - mgmtClassNewOwners, err = cmd.Flags().GetStringSlice("owners") - if err != nil { - return - } - mgmtClass.Owners.IsInherited = false - mgmtClass.Owners.Data = mgmtClassNewOwners - } - case "files": - var mgmtClassNewFiles []string - mgmtClassNewFiles, err = cmd.Flags().GetStringSlice("files") - if err != nil { - return - } - mgmtClass.Files = mgmtClassNewFiles - case "packages": - var mgmtClassNewPackages []string - mgmtClassNewPackages, err = cmd.Flags().GetStringSlice("packages") - if err != nil { - return - } - mgmtClass.Packages = mgmtClassNewPackages - case "params": - var mgmtClassNewParams map[string]string - mgmtClassNewParams, err = cmd.Flags().GetStringToString("params") - if err != nil { - return - } - if inPlace { - err = Client.ModifyItemInPlace( - "mgmtclass", - mgmtClass.Name, - "params", - convertMapStringToMapInterface(mgmtClassNewParams), - ) - if err != nil { - return - } - } else { - mgmtClass.Params = mgmtClassNewParams - } - case "class-name": - var mgmtClassNewClassName string - mgmtClassNewClassName, err = cmd.Flags().GetString("class-name") - if err != nil { - return - } - mgmtClass.ClassName = mgmtClassNewClassName - case "is-definition": - var mgmtClassNewIsDefinition bool - mgmtClassNewIsDefinition, err = cmd.Flags().GetBool("is-definition") - if err != nil { - return - } - mgmtClass.IsDefiniton = mgmtClassNewIsDefinition - } - }) - // Don't blindly return nil because maybe one of the flags had an issue retrieving an argument. - return err -} - -// NewMgmtClassCmd builds a new command that represents the mgmtclass action -func NewMgmtClassCmd() (*cobra.Command, error) { - mgmtclassCmd := &cobra.Command{ - Use: "mgmtclass", - Short: "Mgmtclass management", - Long: `Let you manage mgmtclasses. -See https://cobbler.readthedocs.io/en/latest/cobbler.html#cobbler-mgmtclass for more information.`, - Run: func(cmd *cobra.Command, args []string) { - _ = cmd.Help() - }, - } - mgmtclassCmd.AddCommand(NewMgmtClassAddCmd()) - mgmtclassCmd.AddCommand(NewMgmtClassCopyCmd()) - mgmtclassCmd.AddCommand(NewMgmtClassEditCmd()) - mgmtclassCmd.AddCommand(NewMgmtClassFindCmd()) - mgmtclassCmd.AddCommand(NewMgmtClassListCmd()) - mgmtclassCmd.AddCommand(NewMgmtClassRemoveCmd()) - mgmtclassCmd.AddCommand(NewMgmtClassRenameCmd()) - mgmtclassCmd.AddCommand(NewMgmtClassReportCmd()) - mgmtclassCmd.AddCommand(NewMgmtClassExportCmd()) - return mgmtclassCmd, nil -} - -func NewMgmtClassAddCmd() *cobra.Command { - mgmtclassAddCmd := &cobra.Command{ - Use: "add", - Short: "add mgmtclass", - Long: `Adds a mgmtclass.`, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - newMgmtClass := cobbler.NewMgmtClass() - - // Get special name flag - newMgmtClass.Name, err = cmd.Flags().GetString("name") - if err != nil { - return err - } - // Update with the rest of the flags - err = updateMgmtClassFromFlags(cmd, &newMgmtClass) - if err != nil { - return err - } - // Now create the file via XML-RPC - mgmtClass, err := Client.CreateMgmtClass(newMgmtClass) - if err != nil { - return err - } - fmt.Fprintf(cmd.OutOrStdout(), "Mgmtclass %s created\n", mgmtClass.Name) - return nil - }, - } - addCommonArgs(mgmtclassAddCmd) - addStringFlags(mgmtclassAddCmd, mgmtclassStringFlagMetadata) - addBoolFlags(mgmtclassAddCmd, mgmtclassBoolFlagMetadata) - addStringSliceFlags(mgmtclassAddCmd, mgmtclassStringSliceFlagMetadata) - addMapFlags(mgmtclassAddCmd, mgmtclassStringMapFlagMetadata) - return mgmtclassAddCmd -} - -func NewMgmtClassCopyCmd() *cobra.Command { - mgmtclassCopyCmd := &cobra.Command{ - Use: "copy", - Short: "copy mgmtclass", - Long: `Copies a mgmtclass.`, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - // Get special name and newname flags - mgmtClassName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - mgmtClassNewName, err := cmd.Flags().GetString("newname") - if err != nil { - return err - } - - // Get API handle - mgmtClassHandle, err := Client.GetMgmtClassHandle(mgmtClassName) - if err != nil { - return err - } - // Copy the mgmtclass server-side - err = Client.CopyMgmtClass(mgmtClassHandle, mgmtClassNewName) - if err != nil { - return err - } - // Get the copied mgmtclass - newMgmtClass, err := Client.GetMgmtClass(mgmtClassNewName, false, false) - if err != nil { - return err - } - // Update the mgmtclass in-memory - err = updateMgmtClassFromFlags(cmd, newMgmtClass) - if err != nil { - return err - } - // Update the mgmtclass via XML-RPC - return Client.UpdateMgmtClass(newMgmtClass) - }, - } - addCommonArgs(mgmtclassCopyCmd) - addStringFlags(mgmtclassCopyCmd, mgmtclassStringFlagMetadata) - addBoolFlags(mgmtclassCopyCmd, mgmtclassBoolFlagMetadata) - addStringSliceFlags(mgmtclassCopyCmd, mgmtclassStringSliceFlagMetadata) - addMapFlags(mgmtclassCopyCmd, mgmtclassStringMapFlagMetadata) - mgmtclassCopyCmd.Flags().String("newname", "", "the new mgmtclass name") - mgmtclassCopyCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") - return mgmtclassCopyCmd -} - -func NewMgmtClassEditCmd() *cobra.Command { - mgmtclassEditCmd := &cobra.Command{ - Use: "edit", - Short: "edit mgmtclass", - Long: `Edits a mgmtclass.`, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - // Collect CLI flags - mgmtClassName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - - // Get mgmtclass from the API - mgmtClassToEdit, err := Client.GetMgmtClass(mgmtClassName, false, false) - if err != nil { - return err - } - // Update mgmtclass in-memory - err = updateMgmtClassFromFlags(cmd, mgmtClassToEdit) - if err != nil { - return err - } - // Update the mgmtclass via XML-RPC - return Client.UpdateMgmtClass(mgmtClassToEdit) - }, - } - addCommonArgs(mgmtclassEditCmd) - addStringFlags(mgmtclassEditCmd, mgmtclassStringFlagMetadata) - addBoolFlags(mgmtclassEditCmd, mgmtclassBoolFlagMetadata) - addStringSliceFlags(mgmtclassEditCmd, mgmtclassStringSliceFlagMetadata) - addMapFlags(mgmtclassEditCmd, mgmtclassStringMapFlagMetadata) - mgmtclassEditCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") - return mgmtclassEditCmd -} - -func NewMgmtClassFindCmd() *cobra.Command { - mgmtclassFindCmd := &cobra.Command{ - Use: "find", - Short: "find mgmtclass", - Long: `Finds a given mgmtclass.`, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - return FindItemNames(cmd, args, "mgmtclass") - }, - } - addCommonArgs(mgmtclassFindCmd) - addStringFlags(mgmtclassFindCmd, mgmtclassStringFlagMetadata) - addBoolFlags(mgmtclassFindCmd, mgmtclassBoolFlagMetadata) - addStringSliceFlags(mgmtclassFindCmd, mgmtclassStringSliceFlagMetadata) - addMapFlags(mgmtclassFindCmd, mgmtclassStringMapFlagMetadata) - addStringFlags(mgmtclassFindCmd, findStringFlagMetadata) - addIntFlags(mgmtclassFindCmd, findIntFlagMetadata) - addFloatFlags(mgmtclassFindCmd, findFloatFlagMetadata) - return mgmtclassFindCmd -} - -func NewMgmtClassListCmd() *cobra.Command { - mgmtclassListCmd := &cobra.Command{ - Use: "list", - Short: "list all mgmtclasses", - Long: `Lists all available mgmtclasses.`, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - mgmtclassNames, err := Client.ListMgmtClassNames() - if err != nil { - return err - } - listItems(cmd, "mgmtclasses", mgmtclassNames) - return nil - }, - } - return mgmtclassListCmd -} - -func NewMgmtClassRemoveCmd() *cobra.Command { - mgmtclassRemoveCmd := &cobra.Command{ - Use: "remove", - Short: "remove mgmtclass", - Long: `Removes a given mgmtclass.`, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - return RemoveItemRecursive(cmd, args, "mgmtclass") - }, - } - mgmtclassRemoveCmd.Flags().String("name", "", "the mgmtclass name") - mgmtclassRemoveCmd.Flags().Bool("recursive", false, "also delete child objects") - return mgmtclassRemoveCmd -} - -func NewMgmtClassRenameCmd() *cobra.Command { - mgmtclassRenameCmd := &cobra.Command{ - Use: "rename", - Short: "rename mgmtclass", - Long: `Renames a given mgmtclass.`, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - // Get the special name and newname flags - mgmtClassName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - mgmtClassNewName, err := cmd.Flags().GetString("newname") - if err != nil { - return err - } - - // Get the mgmtclass handle - mgmtClassHandle, err := Client.GetMgmtClassHandle(mgmtClassName) - if err != nil { - return err - } - // Rename the mgmtclass server-side - err = Client.RenameMgmtClass(mgmtClassHandle, mgmtClassNewName) - if err != nil { - return err - } - // Get the renamed mgmtclass - renamedMgmtClass, err := Client.GetMgmtClass(mgmtClassNewName, false, false) - if err != nil { - return err - } - // Update mgmtclass in-memory - err = updateMgmtClassFromFlags(cmd, renamedMgmtClass) - if err != nil { - return err - } - // Update the mgmtclass via XML-RPC - return Client.UpdateMgmtClass(renamedMgmtClass) - }, - } - addCommonArgs(mgmtclassRenameCmd) - addStringFlags(mgmtclassRenameCmd, mgmtclassStringFlagMetadata) - addBoolFlags(mgmtclassRenameCmd, mgmtclassBoolFlagMetadata) - addStringSliceFlags(mgmtclassRenameCmd, mgmtclassStringSliceFlagMetadata) - addMapFlags(mgmtclassRenameCmd, mgmtclassStringMapFlagMetadata) - mgmtclassRenameCmd.Flags().String("newname", "", "the new mgmtclass name") - mgmtclassRenameCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") - return mgmtclassRenameCmd -} - -func reportMgmtClasses(cmd *cobra.Command, mgmtClassNames []string) error { - for _, itemName := range mgmtClassNames { - system, err := Client.GetMgmtClass(itemName, false, false) - if err != nil { - return err - } - printStructured(cmd, system) - fmt.Fprintln(cmd.OutOrStdout(), "") - } - return nil -} - -func NewMgmtClassReportCmd() *cobra.Command { - mgmtclassReportCmd := &cobra.Command{ - Use: "report", - Short: "list all mgmtclasses in detail", - Long: `Shows detailed information about all mgmtclasses.`, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - name, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - itemNames := make([]string, 0) - if name == "" { - itemNames, err = Client.ListMgmtClassNames() - if err != nil { - return err - } - } else { - itemNames = append(itemNames, name) - } - return reportMgmtClasses(cmd, itemNames) - }, - } - mgmtclassReportCmd.Flags().String("name", "", "the mgmtclass name") - return mgmtclassReportCmd -} - -func NewMgmtClassExportCmd() *cobra.Command { - mgmtClassExportCmd := &cobra.Command{ - Use: "export", - Short: "export management classes", - Long: `Export management classes.`, - PreRunE: func(cmd *cobra.Command, args []string) error { - formatOption, err := cmd.Flags().GetString("format") - if err != nil { - return err - } - if formatOption != "json" && formatOption != "yaml" { - return fmt.Errorf("format must be json or yaml") - } - return nil - }, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - name, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - formatOption, err := cmd.Flags().GetString("format") - if err != nil { - return err - } - - itemNames := make([]string, 0) - if name == "" { - itemNames, err = Client.ListMgmtClassNames() - if err != nil { - return err - } - } else { - itemNames = append(itemNames, name) - } - - for _, itemName := range itemNames { - mgmtClass, err := Client.GetMgmtClass(itemName, false, false) - if err != nil { - return err - } - if formatOption == "json" { - jsonDocument, err := json.Marshal(mgmtClass) - if err != nil { - return err - } - fmt.Fprintln(cmd.OutOrStdout(), string(jsonDocument)) - } - if formatOption == "yaml" { - yamlDocument, err := yaml.Marshal(mgmtClass) - if err != nil { - return err - } - fmt.Fprintln(cmd.OutOrStdout(), "---") - fmt.Fprintln(cmd.OutOrStdout(), string(yamlDocument)) - } - } - return nil - }, - } - mgmtClassExportCmd.Flags().String("name", "", "the management class name") - mgmtClassExportCmd.Flags().String(exportStringMetadata["format"].Name, exportStringMetadata["format"].DefaultValue, exportStringMetadata["format"].Usage) - return mgmtClassExportCmd -} diff --git a/cmd/mgmtclass_test.go b/cmd/mgmtclass_test.go deleted file mode 100644 index e0f71dd..0000000 --- a/cmd/mgmtclass_test.go +++ /dev/null @@ -1,454 +0,0 @@ -package cmd - -import ( - "bytes" - "fmt" - cobbler "github.com/cobbler/cobblerclient" - "github.com/spf13/cobra" - "io" - "strings" - "testing" -) - -func createMgmtClass(client cobbler.Client, name string) (*cobbler.MgmtClass, error) { - mgmtclass := cobbler.NewMgmtClass() - mgmtclass.Name = name - return client.CreateMgmtClass(mgmtclass) -} - -func removeMgmtClass(client cobbler.Client, name string) error { - return client.DeleteMgmtClass(name) -} - -func Test_MgmtClassAddCmd(t *testing.T) { - type args struct { - command []string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "plain", - args: args{command: []string{"--config", "../testing/.cobbler.yaml", "mgmtclass", "add", "--name", "test-plain"}}, - want: "Mgmtclass test-plain created", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Cleanup - var err error - defer func() { - // Client is initialized since this is the cleanup - cleanupErr := removeMgmtClass(Client, tt.args.command[5]) - cobbler.FailOnError(t, cleanupErr) - }() - // Arrange - cobra.OnInitialize(initConfig, setupLogger) - rootCmd := NewRootCmd() - rootCmd.SetArgs(tt.args.command) - stdout := bytes.NewBufferString("") - stderr := bytes.NewBufferString("") - rootCmd.SetOut(stdout) - rootCmd.SetErr(stderr) - - // Act - err = rootCmd.Execute() - - // Assert - cobbler.FailOnError(t, err) - FailOnNonEmptyStream(t, stderr) - stdoutBytes, err := io.ReadAll(stdout) - if err != nil { - t.Fatal(err) - } - stdoutString := string(stdoutBytes) - if !strings.Contains(stdoutString, tt.want) { - fmt.Println(stdoutString) - t.Fatal("Item creation message missing") - } - }) - } -} - -func Test_MgmtClassCopyCmd(t *testing.T) { - type args struct { - command []string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "plain", - args: args{command: []string{"--config", "../testing/.cobbler.yaml", "mgmtclass", "copy", "--name", "mgmtclass-to-copy", "--newname", "copied-mgmtclass"}}, - want: "Event ID:", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Cleanup - var err error - defer func() { - // Client is initialized since this is the cleanup - cleanupErr := removeMgmtClass(Client, tt.args.command[5]) - cobbler.FailOnError(t, cleanupErr) - cleanupErr = removeMgmtClass(Client, tt.args.command[7]) - cobbler.FailOnError(t, cleanupErr) - }() - // Arrange - setupClient(t) - _, err = createMgmtClass(Client, tt.args.command[5]) - cobbler.FailOnError(t, err) - cobra.OnInitialize(initConfig, setupLogger) - rootCmd := NewRootCmd() - rootCmd.SetArgs(tt.args.command) - stdout := bytes.NewBufferString("") - stderr := bytes.NewBufferString("") - rootCmd.SetOut(stdout) - rootCmd.SetErr(stderr) - - // Act - err = rootCmd.Execute() - - // Assert - cobbler.FailOnError(t, err) - FailOnNonEmptyStream(t, stderr) - FailOnNonEmptyStream(t, stdout) - _, err = Client.GetMgmtClass(tt.args.command[7], false, false) - cobbler.FailOnError(t, err) - }) - } -} - -func Test_MgmtClassEditCmd(t *testing.T) { - type args struct { - command []string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "plain", - args: args{command: []string{"--config", "../testing/.cobbler.yaml", "mgmtclass", "edit", "--name", "test-mgmtclass-edit", "--comment", "testcomment"}}, - want: "Event ID:", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Cleanup - var err error - defer func() { - // Client is initialized since this is the cleanup - cleanupErr := removeMgmtClass(Client, tt.args.command[5]) - cobbler.FailOnError(t, cleanupErr) - }() - // Arrange - setupClient(t) - _, err = createMgmtClass(Client, tt.args.command[5]) - cobbler.FailOnError(t, err) - cobra.OnInitialize(initConfig, setupLogger) - rootCmd := NewRootCmd() - rootCmd.SetArgs(tt.args.command) - stdout := bytes.NewBufferString("") - stderr := bytes.NewBufferString("") - rootCmd.SetOut(stdout) - rootCmd.SetErr(stderr) - - // Act - err = rootCmd.Execute() - - // Assert - cobbler.FailOnError(t, err) - FailOnNonEmptyStream(t, stderr) - FailOnNonEmptyStream(t, stdout) - updatedMgmtClass, err := Client.GetMgmtClass(tt.args.command[5], false, false) - cobbler.FailOnError(t, err) - if updatedMgmtClass.Comment != "testcomment" { - t.Fatal("mgmtclass update wasn't successful") - } - }) - } -} - -func Test_MgmtClassFindCmd(t *testing.T) { - type args struct { - command []string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "plain", - args: args{command: []string{"--config", "../testing/.cobbler.yaml", "mgmtclass", "find", "--name", "test-mgmtclass-find"}}, - want: "Event ID:", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Cleanup - mgmtclassName := "test-mgmtclass-find" - var err error - defer func() { - // Client is initialized since this is the cleanup - cleanupErr := removeMgmtClass(Client, mgmtclassName) - cobbler.FailOnError(t, cleanupErr) - }() - // Arrange - setupClient(t) - _, err = createMgmtClass(Client, mgmtclassName) - cobbler.FailOnError(t, err) - cobra.OnInitialize(initConfig, setupLogger) - rootCmd := NewRootCmd() - rootCmd.SetArgs(tt.args.command) - stdout := bytes.NewBufferString("") - stderr := bytes.NewBufferString("") - rootCmd.SetOut(stdout) - rootCmd.SetErr(stderr) - - // Act - err = rootCmd.Execute() - - // Assert - cobbler.FailOnError(t, err) - FailOnNonEmptyStream(t, stderr) - stdoutBytes, err := io.ReadAll(stdout) - if err != nil { - t.Fatal(err) - } - stdoutString := string(stdoutBytes) - if !strings.Contains(stdoutString, mgmtclassName) { - fmt.Println(stdoutString) - t.Fatal("mgmtclass not successfully found") - } - }) - } -} - -func Test_MgmtClassListCmd(t *testing.T) { - type args struct { - command []string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "plain", - args: args{command: []string{"--config", "../testing/.cobbler.yaml", "mgmtclass", "list"}}, - want: "mgmtclasses:", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Arrange - cobra.OnInitialize(initConfig, setupLogger) - rootCmd := NewRootCmd() - rootCmd.SetArgs(tt.args.command) - stdout := bytes.NewBufferString("") - stderr := bytes.NewBufferString("") - rootCmd.SetOut(stdout) - rootCmd.SetErr(stderr) - - // Act - err := rootCmd.Execute() - - // Assert - cobbler.FailOnError(t, err) - FailOnNonEmptyStream(t, stderr) - stdoutBytes, err := io.ReadAll(stdout) - if err != nil { - t.Fatal(err) - } - stdoutString := string(stdoutBytes) - if !strings.Contains(stdoutString, tt.want) { - fmt.Println(stdoutString) - t.Fatal("mgmtclass list marker not located in output") - } - }) - } -} - -func Test_MgmtClassRemoveCmd(t *testing.T) { - type args struct { - command []string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "plain", - args: args{command: []string{"--config", "../testing/.cobbler.yaml", "mgmtclass", "remove", "--name", "test-mgmtclass-remove"}}, - want: "Event ID:", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Arrange - setupClient(t) - _, err := createMgmtClass(Client, tt.args.command[5]) - cobbler.FailOnError(t, err) - cobra.OnInitialize(initConfig, setupLogger) - rootCmd := NewRootCmd() - rootCmd.SetArgs(tt.args.command) - stdout := bytes.NewBufferString("") - stderr := bytes.NewBufferString("") - rootCmd.SetOut(stdout) - rootCmd.SetErr(stderr) - - // Act - err = rootCmd.Execute() - - // Assert - cobbler.FailOnError(t, err) - FailOnNonEmptyStream(t, stderr) - FailOnNonEmptyStream(t, stdout) - result, err := Client.HasItem("mgmtclass", tt.args.command[5]) - cobbler.FailOnError(t, err) - if result { - // A missing item means we get "false", as such we error when we find an item. - t.Fatal("mgmtclass not successfully removed") - } - }) - } -} - -func Test_MgmtClassRenameCmd(t *testing.T) { - type args struct { - command []string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "plain", - args: args{command: []string{"--config", "../testing/.cobbler.yaml", "mgmtclass", "rename", "--name", "test-mgmtclass-rename", "--newname", "test-mgmtclass-renamed"}}, - want: "Event ID:", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Cleanup - mgmtclassName := "test-mgmtclass-rename" - newMgmtClassName := "test-mgmtclass-renamed" - var err error - defer func() { - // Client is initialized since this is the cleanup - cleanupErr := removeMgmtClass(Client, newMgmtClassName) - cobbler.FailOnError(t, cleanupErr) - }() - // Arrange - setupClient(t) - _, err = createMgmtClass(Client, mgmtclassName) - cobbler.FailOnError(t, err) - cobra.OnInitialize(initConfig, setupLogger) - rootCmd := NewRootCmd() - rootCmd.SetArgs(tt.args.command) - stdout := bytes.NewBufferString("") - stderr := bytes.NewBufferString("") - rootCmd.SetOut(stdout) - rootCmd.SetErr(stderr) - - // Act - err = rootCmd.Execute() - - // Assert - cobbler.FailOnError(t, err) - FailOnNonEmptyStream(t, stderr) - FailOnNonEmptyStream(t, stdout) - resultOldName, err := Client.HasItem("mgmtclass", mgmtclassName) - cobbler.FailOnError(t, err) - if resultOldName { - t.Fatal("mgmtclass not successfully renamed (old name present)") - } - resultNewName, err := Client.HasItem("mgmtclass", newMgmtClassName) - cobbler.FailOnError(t, err) - if !resultNewName { - t.Fatal("mgmtclass not successfully renamed (new name not present)") - } - }) - } -} - -func Test_MgmtClassReportCmd(t *testing.T) { - type args struct { - command []string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "plain", - args: args{command: []string{"--config", "../testing/.cobbler.yaml", "mgmtclass", "report", "--name", "test-mgmtclass-report"}}, - want: ": test-mgmtclass-report", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Cleanup - mgmtclassName := "test-mgmtclass-report" - var err error - defer func() { - // Client is initialized since this is the cleanup - cleanupErr := removeMgmtClass(Client, mgmtclassName) - cobbler.FailOnError(t, cleanupErr) - }() - // Arrange - setupClient(t) - _, err = createMgmtClass(Client, mgmtclassName) - cobbler.FailOnError(t, err) - cobra.OnInitialize(initConfig, setupLogger) - rootCmd := NewRootCmd() - rootCmd.SetArgs(tt.args.command) - stdout := bytes.NewBufferString("") - stderr := bytes.NewBufferString("") - rootCmd.SetOut(stdout) - rootCmd.SetErr(stderr) - - // Act - err = rootCmd.Execute() - - // Assert - cobbler.FailOnError(t, err) - FailOnNonEmptyStream(t, stderr) - stdoutBytes, err := io.ReadAll(stdout) - if err != nil { - t.Fatal(err) - } - stdoutString := string(stdoutBytes) - if !strings.Contains(stdoutString, tt.want) { - fmt.Println(stdoutString) - t.Fatal("No Event ID present") - } - }) - } -} diff --git a/cmd/package.go b/cmd/package.go deleted file mode 100644 index 2a72a4c..0000000 --- a/cmd/package.go +++ /dev/null @@ -1,449 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -// SPDX-FileCopyrightText: 2021 Dominik Gedon -// SPDX-FileCopyrightText: Copyright SUSE LLC - -package cmd - -import ( - "encoding/json" - "fmt" - cobbler "github.com/cobbler/cobblerclient" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "gopkg.in/yaml.v3" -) - -func updatePackageFromFlags(cmd *cobra.Command, p *cobbler.Package) error { - // This object type doesn't have the in-place flag - var err error - cmd.Flags().Visit(func(flag *pflag.Flag) { - if err != nil { - // If one of the previous flags has had an error just directly return. - return - } - switch flag.Name { - case "comment": - var packageNewComment string - packageNewComment, err = cmd.Flags().GetString("comment") - if err != nil { - return - } - p.Comment = packageNewComment - case "owners": - fallthrough - case "owners-inherit": - if cmd.Flags().Lookup("owners-inherit").Changed { - p.Owners.Data = []string{} - p.Owners.IsInherited, err = cmd.Flags().GetBool("owners-inherit") - if err != nil { - return - } - } else { - var packageNewOwners []string - packageNewOwners, err = cmd.Flags().GetStringSlice("owners") - if err != nil { - return - } - p.Owners.IsInherited = false - p.Owners.Data = packageNewOwners - } - case "action": - var packageNewAction string - packageNewAction, err = cmd.Flags().GetString("action") - if err != nil { - return - } - p.Action = packageNewAction - case "installer": - var packageNewInstaller string - packageNewInstaller, err = cmd.Flags().GetString("installer") - if err != nil { - return - } - p.Action = packageNewInstaller - case "version": - var packageNewVersion string - packageNewVersion, err = cmd.Flags().GetString("version") - if err != nil { - return - } - p.Action = packageNewVersion - } - }) - return err -} - -// NewPackageCmd builds a new command that represents the package action -func NewPackageCmd() (*cobra.Command, error) { - packageCmd := &cobra.Command{ - Use: "package", - Short: "Package management", - Long: `Let you manage packages. -See https://cobbler.readthedocs.io/en/latest/cobbler.html#cobbler-package for more information.`, - Run: func(cmd *cobra.Command, args []string) { - _ = cmd.Help() - }, - } - packageCmd.AddCommand(NewPackageAddCmd()) - packageCmd.AddCommand(NewPackageCopyCmd()) - packageCmd.AddCommand(NewPackageEditCmd()) - packageCmd.AddCommand(NewPackageFindCmd()) - packageCmd.AddCommand(NewPackageListCmd()) - packageCmd.AddCommand(NewPackageRemoveCmd()) - packageCmd.AddCommand(NewPackageRenameCmd()) - packageCmd.AddCommand(NewPackageReportCmd()) - packageCmd.AddCommand(NewPackageExportCmd()) - return packageCmd, nil -} - -func NewPackageAddCmd() *cobra.Command { - packageAddCmd := &cobra.Command{ - Use: "add", - Short: "add package", - Long: `Adds a package.`, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - newPackage := cobbler.NewPackage() - - // internal fields (ctime, mtime, depth, uid) cannot be modified - newPackage.Name, err = cmd.Flags().GetString("name") - if err != nil { - return err - } - // Update package in-memory - err = updatePackageFromFlags(cmd, &newPackage) - if err != nil { - return err - } - // Create package via XML-RPC - linuxpackage, err := Client.CreatePackage(newPackage) - if err != nil { - return err - } - fmt.Fprintf(cmd.OutOrStdout(), "Package %s created\n", linuxpackage.Name) - return nil - }, - } - addCommonArgs(packageAddCmd) - addStringFlags(packageAddCmd, packageStringFlagMetadata) - return packageAddCmd -} - -func NewPackageCopyCmd() *cobra.Command { - packageCopyCmd := &cobra.Command{ - Use: "copy", - Short: "copy package", - Long: `Copies a package.`, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - // Collect CLI flags - packageName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - packageNewName, err := cmd.Flags().GetString("newname") - if err != nil { - return err - } - - // Get package handle - packageHandle, err := Client.GetPackageHandle(packageName) - if err != nil { - return err - } - // Copy the package server-side - err = Client.CopyPackage(packageHandle, packageNewName) - if err != nil { - return err - } - // Get the copied package from the API - newPackage, err := Client.GetPackage(packageNewName, false, false) - if err != nil { - return err - } - // Update package in-memory - err = updatePackageFromFlags(cmd, newPackage) - if err != nil { - return err - } - // Update the package via XML-RPC - return Client.UpdatePackage(newPackage) - }, - } - addCommonArgs(packageCopyCmd) - addStringFlags(packageCopyCmd, packageStringFlagMetadata) - packageCopyCmd.Flags().String("newname", "", "the new package name") - packageCopyCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") - return packageCopyCmd -} - -func NewPackageEditCmd() *cobra.Command { - packageEditCmd := &cobra.Command{ - Use: "edit", - Short: "edit package", - Long: `Edits a package.`, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - packageName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - - // Get package from the API - packageToEdit, err := Client.GetPackage(packageName, false, false) - if err != nil { - return err - } - // Update package in-memory - err = updatePackageFromFlags(cmd, packageToEdit) - if err != nil { - return err - } - // Update package via XML-RPC - return Client.UpdatePackage(packageToEdit) - }, - } - addCommonArgs(packageEditCmd) - addStringFlags(packageEditCmd, packageStringFlagMetadata) - packageEditCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") - return packageEditCmd -} - -func NewPackageFindCmd() *cobra.Command { - packageFindCmd := &cobra.Command{ - Use: "find", - Short: "find package", - Long: `Finds a given package.`, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - return FindItemNames(cmd, args, "package") - }, - } - addCommonArgs(packageFindCmd) - addStringFlags(packageFindCmd, packageStringFlagMetadata) - addStringFlags(packageFindCmd, findStringFlagMetadata) - addIntFlags(packageFindCmd, findIntFlagMetadata) - addFloatFlags(packageFindCmd, findFloatFlagMetadata) - return packageFindCmd -} - -func NewPackageListCmd() *cobra.Command { - packageListCmd := &cobra.Command{ - Use: "list", - Short: "list all packages", - Long: `Lists all available packages.`, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - packageNames, err := Client.ListPackageNames() - if err != nil { - return err - } - listItems(cmd, "packages", packageNames) - return nil - }, - } - return packageListCmd -} - -func NewPackageRemoveCmd() *cobra.Command { - packageRemoveCmd := &cobra.Command{ - Use: "remove", - Short: "remove package", - Long: `Removes a given package.`, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - return RemoveItemRecursive(cmd, args, "package") - }, - } - packageRemoveCmd.Flags().String("name", "", "the package name") - packageRemoveCmd.Flags().Bool("recursive", false, "also delete child objects") - return packageRemoveCmd -} - -func NewPackageRenameCmd() *cobra.Command { - packageRenameCmd := &cobra.Command{ - Use: "rename", - Short: "rename package", - Long: `Renames a given package.`, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - // internal fields (ctime, mtime, depth, uid) cannot be modified - packageName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - packageNewName, err := cmd.Flags().GetString("newname") - if err != nil { - return err - } - - // Get package API handle - packageHandle, err := Client.GetPackageHandle(packageName) - if err != nil { - return err - } - // Perform server-side package rename - err = Client.RenamePackage(packageHandle, packageNewName) - if err != nil { - return err - } - // Get the renamed package from the API - newPackage, err := Client.GetPackage(packageNewName, false, false) - if err != nil { - return err - } - // Update package in-memory - err = updatePackageFromFlags(cmd, newPackage) - if err != nil { - return err - } - // Update package via XML-RPC - return Client.UpdatePackage(newPackage) - }, - } - addCommonArgs(packageRenameCmd) - addStringFlags(packageRenameCmd, packageStringFlagMetadata) - packageRenameCmd.Flags().String("newname", "", "the new package name") - packageRenameCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") - return packageRenameCmd -} - -func reportPackages(cmd *cobra.Command, packageNames []string) error { - for _, itemName := range packageNames { - repo, err := Client.GetPackage(itemName, false, false) - if err != nil { - return err - } - printStructured(cmd, repo) - fmt.Fprintln(cmd.OutOrStdout(), "") - } - return nil -} - -func NewPackageReportCmd() *cobra.Command { - packageReportCmd := &cobra.Command{ - Use: "report", - Short: "list all packages in detail", - Long: `Shows detailed information about all packages.`, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - name, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - itemNames := make([]string, 0) - if name == "" { - itemNames, err = Client.ListRepoNames() - if err != nil { - return err - } - } else { - itemNames = append(itemNames, name) - } - return reportPackages(cmd, itemNames) - }, - } - packageReportCmd.Flags().String("name", "", "the package name") - return packageReportCmd -} - -func NewPackageExportCmd() *cobra.Command { - packageExportCmd := &cobra.Command{ - Use: "export", - Short: "export packages", - Long: `Export packages.`, - PreRunE: func(cmd *cobra.Command, args []string) error { - formatOption, err := cmd.Flags().GetString("format") - if err != nil { - return err - } - if formatOption != "json" && formatOption != "yaml" { - return fmt.Errorf("format must be json or yaml") - } - return nil - }, - RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { - return err - } - - name, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - formatOption, err := cmd.Flags().GetString("format") - if err != nil { - return err - } - - itemNames := make([]string, 0) - if name == "" { - itemNames, err = Client.ListPackageNames() - if err != nil { - return err - } - } else { - itemNames = append(itemNames, name) - } - - for _, itemName := range itemNames { - linuxpackage, err := Client.GetPackage(itemName, false, false) - if err != nil { - return err - } - if formatOption == "json" { - jsonDocument, err := json.Marshal(linuxpackage) - if err != nil { - return err - } - fmt.Fprintln(cmd.OutOrStdout(), string(jsonDocument)) - } - if formatOption == "yaml" { - yamlDocument, err := yaml.Marshal(linuxpackage) - if err != nil { - return err - } - fmt.Fprintln(cmd.OutOrStdout(), "---") - fmt.Fprintln(cmd.OutOrStdout(), string(yamlDocument)) - } - } - return nil - }, - } - packageExportCmd.Flags().String("name", "", "the package name") - packageExportCmd.Flags().String(exportStringMetadata["format"].Name, exportStringMetadata["format"].DefaultValue, exportStringMetadata["format"].Usage) - return packageExportCmd -} diff --git a/cmd/package_test.go b/cmd/package_test.go deleted file mode 100644 index 3d55d93..0000000 --- a/cmd/package_test.go +++ /dev/null @@ -1,454 +0,0 @@ -package cmd - -import ( - "bytes" - "fmt" - cobbler "github.com/cobbler/cobblerclient" - "github.com/spf13/cobra" - "io" - "strings" - "testing" -) - -func createPackage(client cobbler.Client, name string) (*cobbler.Package, error) { - linuxpackage := cobbler.NewPackage() - linuxpackage.Name = name - return client.CreatePackage(linuxpackage) -} - -func removePackage(client cobbler.Client, name string) error { - return client.DeletePackage(name) -} - -func Test_PackageAddCmd(t *testing.T) { - type args struct { - command []string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "plain", - args: args{command: []string{"--config", "../testing/.cobbler.yaml", "package", "add", "--name", "test-plain"}}, - want: "Package test-plain created", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Cleanup - var err error - defer func() { - // Client is initialized since this is the cleanup - cleanupErr := removePackage(Client, tt.args.command[5]) - cobbler.FailOnError(t, cleanupErr) - }() - // Arrange - cobra.OnInitialize(initConfig, setupLogger) - rootCmd := NewRootCmd() - rootCmd.SetArgs(tt.args.command) - stdout := bytes.NewBufferString("") - stderr := bytes.NewBufferString("") - rootCmd.SetOut(stdout) - rootCmd.SetErr(stderr) - - // Act - err = rootCmd.Execute() - - // Assert - cobbler.FailOnError(t, err) - FailOnNonEmptyStream(t, stderr) - stdoutBytes, err := io.ReadAll(stdout) - if err != nil { - t.Fatal(err) - } - stdoutString := string(stdoutBytes) - if !strings.Contains(stdoutString, tt.want) { - fmt.Println(stdoutString) - t.Fatal("Item creation message missing") - } - }) - } -} - -func Test_PackageCopyCmd(t *testing.T) { - type args struct { - command []string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "plain", - args: args{command: []string{"--config", "../testing/.cobbler.yaml", "package", "copy", "--name", "package-to-copy", "--newname", "copied-package"}}, - want: "Event ID:", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Cleanup - var err error - defer func() { - // Client is initialized since this is the cleanup - cleanupErr := removePackage(Client, tt.args.command[5]) - cobbler.FailOnError(t, cleanupErr) - cleanupErr = removePackage(Client, tt.args.command[7]) - cobbler.FailOnError(t, cleanupErr) - }() - // Arrange - setupClient(t) - _, err = createPackage(Client, tt.args.command[5]) - cobbler.FailOnError(t, err) - cobra.OnInitialize(initConfig, setupLogger) - rootCmd := NewRootCmd() - rootCmd.SetArgs(tt.args.command) - stdout := bytes.NewBufferString("") - stderr := bytes.NewBufferString("") - rootCmd.SetOut(stdout) - rootCmd.SetErr(stderr) - - // Act - err = rootCmd.Execute() - - // Assert - cobbler.FailOnError(t, err) - FailOnNonEmptyStream(t, stderr) - FailOnNonEmptyStream(t, stdout) - _, err = Client.GetPackage(tt.args.command[7], false, false) - cobbler.FailOnError(t, err) - }) - } -} - -func Test_PackageEditCmd(t *testing.T) { - type args struct { - command []string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "plain", - args: args{command: []string{"--config", "../testing/.cobbler.yaml", "package", "edit", "--name", "test-package-edit", "--comment", "testcomment"}}, - want: "Event ID:", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Cleanup - var err error - defer func() { - // Client is initialized since this is the cleanup - cleanupErr := removePackage(Client, tt.args.command[5]) - cobbler.FailOnError(t, cleanupErr) - }() - // Arrange - setupClient(t) - _, err = createPackage(Client, tt.args.command[5]) - cobbler.FailOnError(t, err) - cobra.OnInitialize(initConfig, setupLogger) - rootCmd := NewRootCmd() - rootCmd.SetArgs(tt.args.command) - stdout := bytes.NewBufferString("") - stderr := bytes.NewBufferString("") - rootCmd.SetOut(stdout) - rootCmd.SetErr(stderr) - - // Act - err = rootCmd.Execute() - - // Assert - cobbler.FailOnError(t, err) - FailOnNonEmptyStream(t, stderr) - FailOnNonEmptyStream(t, stdout) - updatedPackage, err := Client.GetPackage(tt.args.command[5], false, false) - cobbler.FailOnError(t, err) - if updatedPackage.Comment != "testcomment" { - t.Fatal("package update wasn't successful") - } - }) - } -} - -func Test_PackageFindCmd(t *testing.T) { - type args struct { - command []string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "plain", - args: args{command: []string{"--config", "../testing/.cobbler.yaml", "package", "find", "--name", "test-package-find"}}, - want: "Event ID:", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Cleanup - packageName := "test-package-find" - var err error - defer func() { - // Client is initialized since this is the cleanup - cleanupErr := removePackage(Client, packageName) - cobbler.FailOnError(t, cleanupErr) - }() - // Arrange - setupClient(t) - _, err = createPackage(Client, packageName) - cobbler.FailOnError(t, err) - cobra.OnInitialize(initConfig, setupLogger) - rootCmd := NewRootCmd() - rootCmd.SetArgs(tt.args.command) - stdout := bytes.NewBufferString("") - stderr := bytes.NewBufferString("") - rootCmd.SetOut(stdout) - rootCmd.SetErr(stderr) - - // Act - err = rootCmd.Execute() - - // Assert - cobbler.FailOnError(t, err) - FailOnNonEmptyStream(t, stderr) - stdoutBytes, err := io.ReadAll(stdout) - if err != nil { - t.Fatal(err) - } - stdoutString := string(stdoutBytes) - if !strings.Contains(stdoutString, packageName) { - fmt.Println(stdoutString) - t.Fatal("package not successfully found") - } - }) - } -} - -func Test_PackageListCmd(t *testing.T) { - type args struct { - command []string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "plain", - args: args{command: []string{"--config", "../testing/.cobbler.yaml", "package", "list"}}, - want: "packages:", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Arrange - cobra.OnInitialize(initConfig, setupLogger) - rootCmd := NewRootCmd() - rootCmd.SetArgs(tt.args.command) - stdout := bytes.NewBufferString("") - stderr := bytes.NewBufferString("") - rootCmd.SetOut(stdout) - rootCmd.SetErr(stderr) - - // Act - err := rootCmd.Execute() - - // Assert - cobbler.FailOnError(t, err) - FailOnNonEmptyStream(t, stderr) - stdoutBytes, err := io.ReadAll(stdout) - if err != nil { - t.Fatal(err) - } - stdoutString := string(stdoutBytes) - if !strings.Contains(stdoutString, tt.want) { - fmt.Println(stdoutString) - t.Fatal("package list marker not located in output") - } - }) - } -} - -func Test_PackageRemoveCmd(t *testing.T) { - type args struct { - command []string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "plain", - args: args{command: []string{"--config", "../testing/.cobbler.yaml", "package", "remove", "--name", "test-package-remove"}}, - want: "Event ID:", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Arrange - setupClient(t) - _, err := createPackage(Client, tt.args.command[5]) - cobbler.FailOnError(t, err) - cobra.OnInitialize(initConfig, setupLogger) - rootCmd := NewRootCmd() - rootCmd.SetArgs(tt.args.command) - stdout := bytes.NewBufferString("") - stderr := bytes.NewBufferString("") - rootCmd.SetOut(stdout) - rootCmd.SetErr(stderr) - - // Act - err = rootCmd.Execute() - - // Assert - cobbler.FailOnError(t, err) - FailOnNonEmptyStream(t, stderr) - FailOnNonEmptyStream(t, stdout) - result, err := Client.HasItem("package", tt.args.command[5]) - cobbler.FailOnError(t, err) - if result { - // A missing item means we get "false", as such we error when we find an item. - t.Fatal("package not successfully removed") - } - }) - } -} - -func Test_PackageRenameCmd(t *testing.T) { - type args struct { - command []string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "plain", - args: args{command: []string{"--config", "../testing/.cobbler.yaml", "package", "rename", "--name", "test-package-rename", "--newname", "test-package-renamed"}}, - want: "Event ID:", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Cleanup - packageName := "test-package-rename" - newPackageName := "test-package-renamed" - var err error - defer func() { - // Client is initialized since this is the cleanup - cleanupErr := removePackage(Client, newPackageName) - cobbler.FailOnError(t, cleanupErr) - }() - // Arrange - setupClient(t) - _, err = createPackage(Client, packageName) - cobbler.FailOnError(t, err) - cobra.OnInitialize(initConfig, setupLogger) - rootCmd := NewRootCmd() - rootCmd.SetArgs(tt.args.command) - stdout := bytes.NewBufferString("") - stderr := bytes.NewBufferString("") - rootCmd.SetOut(stdout) - rootCmd.SetErr(stderr) - - // Act - err = rootCmd.Execute() - - // Assert - cobbler.FailOnError(t, err) - FailOnNonEmptyStream(t, stderr) - FailOnNonEmptyStream(t, stdout) - resultOldName, err := Client.HasItem("package", packageName) - cobbler.FailOnError(t, err) - if resultOldName { - t.Fatal("package not successfully renamed (old name present)") - } - resultNewName, err := Client.HasItem("package", newPackageName) - cobbler.FailOnError(t, err) - if !resultNewName { - t.Fatal("package not successfully renamed (new name not present)") - } - }) - } -} - -func Test_PackageReportCmd(t *testing.T) { - type args struct { - command []string - } - tests := []struct { - name string - args args - want string - wantErr bool - }{ - { - name: "plain", - args: args{command: []string{"--config", "../testing/.cobbler.yaml", "package", "report", "--name", "test-package-report"}}, - want: ": test-package-report", - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Cleanup - packageName := "test-package-report" - var err error - defer func() { - // Client is initialized since this is the cleanup - cleanupErr := removePackage(Client, packageName) - cobbler.FailOnError(t, cleanupErr) - }() - // Arrange - setupClient(t) - _, err = createPackage(Client, packageName) - cobbler.FailOnError(t, err) - cobra.OnInitialize(initConfig, setupLogger) - rootCmd := NewRootCmd() - rootCmd.SetArgs(tt.args.command) - stdout := bytes.NewBufferString("") - stderr := bytes.NewBufferString("") - rootCmd.SetOut(stdout) - rootCmd.SetErr(stderr) - - // Act - err = rootCmd.Execute() - - // Assert - cobbler.FailOnError(t, err) - FailOnNonEmptyStream(t, stderr) - stdoutBytes, err := io.ReadAll(stdout) - if err != nil { - t.Fatal(err) - } - stdoutString := string(stdoutBytes) - if !strings.Contains(stdoutString, tt.want) { - fmt.Println(stdoutString) - t.Fatal("No Event ID present") - } - }) - } -} diff --git a/cmd/profile.go b/cmd/profile.go index bc01a59..ad5658d 100644 --- a/cmd/profile.go +++ b/cmd/profile.go @@ -212,24 +212,6 @@ func updateProfileFromFlags(cmd *cobra.Command, profile *cobbler.Profile) error profile.KernelOptionsPost.Data = convertMapStringToMapInterface(profileNewKernelOptionsPost) } } - case "mgmt-classes": - fallthrough - case "mgmt-classes-inherit": - if cmd.Flags().Lookup("mgmt-classes-inherit").Changed { - profile.MgmtClasses.Data = []string{} - profile.MgmtClasses.IsInherited, err = cmd.Flags().GetBool("mgmt-classes-inherit") - if err != nil { - return - } - } else { - var profileNewMgmtClasses []string - profileNewMgmtClasses, err = cmd.Flags().GetStringSlice("mgmt-classes") - if err != nil { - return - } - profile.MgmtClasses.IsInherited = false - profile.MgmtClasses.Data = profileNewMgmtClasses - } case "owners": fallthrough case "owners-inherit": @@ -328,24 +310,6 @@ func updateProfileFromFlags(cmd *cobra.Command, profile *cobbler.Profile) error profile.EnableMenu.IsInherited = false profile.EnableMenu.Data = profileNewEnableMenu } - case "mgmt-parameters": - fallthrough - case "mgmt-parameters-inherit": - if cmd.Flags().Lookup("mgmt-parameters-inherit").Changed { - profile.MgmtParameters.Data = make(map[string]interface{}) - profile.MgmtParameters.IsInherited, err = cmd.Flags().GetBool("mgmt-parameters-inherit") - if err != nil { - return - } - } else { - var profileNewMgmtParameters map[string]string - profileNewMgmtParameters, err = cmd.Flags().GetStringToString("mgmt-parameters") - if err != nil { - return - } - profile.MgmtParameters.IsInherited = false - profile.MgmtParameters.Data = convertMapStringToMapInterface(profileNewMgmtParameters) - } case "name-servers": fallthrough case "name-servers-inherit": @@ -638,18 +602,18 @@ func NewProfileDumpVars() *cobra.Command { return err } - // Get CLI flags profileName, err := cmd.Flags().GetString("name") if err != nil { return err } - - // Now retrieve data - blendedData, err := Client.GetBlendedData(profileName, "") + profile, err := Client.GetProfile(profileName, false, false) + if err != nil { + return err + } + blendedData, err := Client.DumpVars(profile.Uid, false, false) if err != nil { return err } - // Print data printDumpVars(cmd, blendedData) return err }, @@ -726,6 +690,7 @@ func NewProfileFindCmd() *cobra.Command { addStringFlags(profileFindCmd, findStringFlagMetadata) addIntFlags(profileFindCmd, findIntFlagMetadata) addFloatFlags(profileFindCmd, findFloatFlagMetadata) + addPaginationFlags(profileFindCmd) return profileFindCmd } @@ -752,7 +717,7 @@ func NewProfileGetAutoinstallCmd() *cobra.Command { return fmt.Errorf("Profile does not exist!") } - autoinstallRendered, err := Client.GenerateAutoinstall(profileName, "") + autoinstallRendered, err := Client.GenerateAutoinstall(profileName, "profile", "name", "", "") if err != nil { return err } diff --git a/cmd/profile_group.go b/cmd/profile_group.go new file mode 100644 index 0000000..b93f73a --- /dev/null +++ b/cmd/profile_group.go @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright SUSE LLC + +package cmd + +import ( + "fmt" + + cobbler "github.com/cobbler/cobblerclient" + "github.com/spf13/cobra" +) + +// NewProfileGroupCmd builds the `cobbler profile-group` command tree. +func NewProfileGroupCmd() (*cobra.Command, error) { + cmd := &cobra.Command{ + Use: "profile-group", + Short: "Manage profile groups", + Long: `Manage Cobbler 4.0.0 ProfileGroup items.`, + } + cmd.AddCommand(newProfileGroupAddCmd()) + cmd.AddCommand(newProfileGroupCopyCmd()) + cmd.AddCommand(newProfileGroupEditCmd()) + cmd.AddCommand(newProfileGroupFindCmd()) + cmd.AddCommand(newProfileGroupListCmd()) + cmd.AddCommand(newProfileGroupRemoveCmd()) + cmd.AddCommand(newProfileGroupRenameCmd()) + cmd.AddCommand(newProfileGroupReportCmd()) + cmd.AddCommand(newProfileGroupExportCmd()) + return cmd, nil +} + +func newProfileGroupAddCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "add", + Short: "add a profile group", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + g := cobbler.NewProfileGroup() + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + g.Name = name + comment, commentSet, items, itemsSet, err := extractGroupFlags(cmd) + if err != nil { + return err + } + if commentSet { + g.Comment = comment + } + if itemsSet { + g.Items = items + } + created, err := Client.CreateProfileGroup(g) + if err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "Profile group %s created\n", created.Name) + return nil + }, + } + addGroupFlagSet(cmd) + return cmd +} + +func newProfileGroupEditCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "edit", + Short: "edit a profile group", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + g, err := Client.GetProfileGroup(name, false, false) + if err != nil { + return err + } + comment, commentSet, items, itemsSet, err := extractGroupFlags(cmd) + if err != nil { + return err + } + if commentSet { + g.Comment = comment + } + if itemsSet { + g.Items = items + } + return Client.UpdateProfileGroup(g) + }, + } + addGroupFlagSet(cmd) + return cmd +} + +func newProfileGroupCopyCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "copy", + Short: "copy a profile group", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + newName, err := cmd.Flags().GetString("newname") + if err != nil { + return err + } + handle, err := Client.GetProfileGroupHandle(name) + if err != nil { + return err + } + return Client.CopyProfileGroup(handle, newName) + }, + } + addStringFlags(cmd, commonStringFlagMetadata) + addStringFlags(cmd, copyRenameStringFlagMetadata) + return cmd +} + +func newProfileGroupRenameCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "rename", + Short: "rename a profile group", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + newName, err := cmd.Flags().GetString("newname") + if err != nil { + return err + } + handle, err := Client.GetProfileGroupHandle(name) + if err != nil { + return err + } + return Client.RenameProfileGroup(handle, newName) + }, + } + addStringFlags(cmd, commonStringFlagMetadata) + addStringFlags(cmd, copyRenameStringFlagMetadata) + return cmd +} + +func newProfileGroupRemoveCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "remove", + Short: "remove a profile group", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + return RemoveItemRecursive(cmd, args, "profile_group") + }, + } + cmd.Flags().String("name", "", "the profile group name") + cmd.Flags().Bool("recursive", false, "also delete child objects") + return cmd +} + +func newProfileGroupFindCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "find", + Short: "find profile groups", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + return FindItemNames(cmd, args, "profile_group") + }, + } + addStringFlags(cmd, commonStringFlagMetadata) + addStringSliceFlags(cmd, groupStringSliceFlagMetadata) + addPaginationFlags(cmd) + return cmd +} + +func newProfileGroupListCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "list all profile groups", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + names, err := Client.ListProfileGroupNames() + if err != nil { + return err + } + listItems(cmd, "profile_groups", names) + return nil + }, + } + return cmd +} + +func newProfileGroupReportCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "report", + Short: "show profile group details", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + names := make([]string, 0) + if name == "" { + names, err = Client.ListProfileGroupNames() + if err != nil { + return err + } + } else { + names = append(names, name) + } + for _, n := range names { + g, err := Client.GetProfileGroup(n, false, false) + if err != nil { + return err + } + printStructured(cmd, g) + fmt.Fprintln(cmd.OutOrStdout(), "") + } + return nil + }, + } + cmd.Flags().String("name", "", "the profile group name") + return cmd +} + +func newProfileGroupExportCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "export", + Short: "export profile groups", + PreRunE: func(cmd *cobra.Command, args []string) error { + format, err := cmd.Flags().GetString("format") + if err != nil { + return err + } + if format != "json" && format != "yaml" { + return fmt.Errorf("format must be json or yaml") + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + format, err := cmd.Flags().GetString("format") + if err != nil { + return err + } + names := make([]string, 0) + if name == "" { + names, err = Client.ListProfileGroupNames() + if err != nil { + return err + } + } else { + names = append(names, name) + } + for _, n := range names { + g, err := Client.GetProfileGroup(n, false, false) + if err != nil { + return err + } + if err := writeExport(cmd, format, g); err != nil { + return err + } + } + return nil + }, + } + cmd.Flags().String("name", "", "the profile group name") + cmd.Flags().String(exportStringMetadata["format"].Name, exportStringMetadata["format"].DefaultValue, exportStringMetadata["format"].Usage) + return cmd +} diff --git a/cmd/replicate.go b/cmd/replicate.go index a995754..41ef20c 100644 --- a/cmd/replicate.go +++ b/cmd/replicate.go @@ -47,18 +47,6 @@ See https://cobbler.readthedocs.io/en/latest/cobbler.html#cobbler-replicate for if err != nil { return err } - mgmtClassesOption, err := cmd.Flags().GetString("mgmtclasses") - if err != nil { - return err - } - packagesOption, err := cmd.Flags().GetString("packages") - if err != nil { - return err - } - filesOption, err := cmd.Flags().GetString("files") - if err != nil { - return err - } portOption, err := cmd.Flags().GetString("port") if err != nil { return err @@ -84,20 +72,17 @@ See https://cobbler.readthedocs.io/en/latest/cobbler.html#cobbler-replicate for return err } replicateOptions := cobblerclient.ReplicateOptions{ - Master: masterOption, - Port: portOption, - DistroPatterns: distrosOption, - ProfilePatterns: profilesOption, - SystemPatterns: systemsOption, - RepoPatterns: reposOption, - Imagepatterns: imagesOption, - MgmtclassPatterns: mgmtClassesOption, - PackagePatterns: packagesOption, - FilePatterns: filesOption, - Prune: pruneOption, - OmitData: omitDataOption, - SyncAll: syncAllOption, - UseSsl: useSslOption, + Master: masterOption, + Port: portOption, + DistroPatterns: distrosOption, + ProfilePatterns: profilesOption, + SystemPatterns: systemsOption, + RepoPatterns: reposOption, + Imagepatterns: imagesOption, + Prune: pruneOption, + OmitData: omitDataOption, + SyncAll: syncAllOption, + UseSsl: useSslOption, } eventId, err := Client.BackgroundReplicate(replicateOptions) if err != nil { @@ -108,12 +93,9 @@ See https://cobbler.readthedocs.io/en/latest/cobbler.html#cobbler-replicate for }, } replicateCmd.Flags().String("distros", "", "patterns of distros to replicate") - replicateCmd.Flags().String("files", "", "patterns of files to replicate") replicateCmd.Flags().String("image", "", "patterns of images to replicate") replicateCmd.Flags().String("master", "", "Cobbler server to replicate from") - replicateCmd.Flags().String("mgmtclasses", "", "patterns of mgmtclasses to replicate") replicateCmd.Flags().Bool("omit-data", false, "do not rsync data") - replicateCmd.Flags().String("packages", "", "patterns of packages to replicate") replicateCmd.Flags().String("port", "", "remote port") replicateCmd.Flags().String("profiles", "", "patterns of profiles to replicate") replicateCmd.Flags().Bool("prune", false, "remove objects (of all types) not found on the master") diff --git a/cmd/repo.go b/cmd/repo.go index 0bb54ee..6f5392d 100644 --- a/cmd/repo.go +++ b/cmd/repo.go @@ -248,7 +248,8 @@ func NewRepoAutoAddCmd() *cobra.Command { return err } - return Client.AutoAddRepos() + _, err = Client.AutoAddRepos() + return err }, } return repoAutoAddCmd @@ -368,6 +369,7 @@ func NewRepoFindCmd() *cobra.Command { addIntFlags(repoFindCmd, findIntFlagMetadata) addFloatFlags(repoFindCmd, findFloatFlagMetadata) repoFindCmd.Flags().String("parent", "", "") + addPaginationFlags(repoFindCmd) return repoFindCmd } diff --git a/cmd/report.go b/cmd/report.go index a54355f..687a694 100644 --- a/cmd/report.go +++ b/cmd/report.go @@ -15,7 +15,7 @@ func NewReportCmd() *cobra.Command { Use: "report", Short: "List configuration in detail", Long: `Lists all configuration which Cobbler can obtain from the saved data. There are also report subcommands for -most of the other Cobbler commands (currently: distro, profile, system, repo, image, mgmtclass, package, file, menu). +most of the other Cobbler commands (currently: distro, profile, system, repo, image, menu). Identical to 'cobbler list'`, RunE: func(cmd *cobra.Command, args []string) error { @@ -89,45 +89,6 @@ Identical to 'cobbler list'`, } fmt.Fprintln(cmd.OutOrStdout(), "") - // Mgmtclass - fmt.Fprintln(cmd.OutOrStdout(), "mgmtclasses:") - fmt.Fprintln(cmd.OutOrStdout(), "==========") - mgmtClassNames, err := Client.ListMgmtClassNames() - if err != nil { - return err - } - err = reportMgmtClasses(cmd, mgmtClassNames) - if err != nil { - return err - } - fmt.Fprintln(cmd.OutOrStdout(), "") - - // Package - fmt.Fprintln(cmd.OutOrStdout(), "packages:") - fmt.Fprintln(cmd.OutOrStdout(), "==========") - packageNames, err := Client.ListPackageNames() - if err != nil { - return err - } - err = reportPackages(cmd, packageNames) - if err != nil { - return err - } - fmt.Fprintln(cmd.OutOrStdout(), "") - - // File - fmt.Fprintln(cmd.OutOrStdout(), "files:") - fmt.Fprintln(cmd.OutOrStdout(), "==========") - fileNames, err := Client.ListFileNames() - if err != nil { - return err - } - err = reportFiles(cmd, fileNames) - if err != nil { - return err - } - fmt.Fprintln(cmd.OutOrStdout(), "") - // Menu fmt.Fprintln(cmd.OutOrStdout(), "menus:") fmt.Fprintln(cmd.OutOrStdout(), "==========") diff --git a/cmd/root.go b/cmd/root.go index 263efde..9664b6f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -46,9 +46,6 @@ func NewRootCmd() *cobra.Command { cobra.CheckErr(err) rootCmd.AddCommand(distroCmd) rootCmd.AddCommand(NewEventCmd()) - fileCmd, err := NewFileCmd() - cobra.CheckErr(err) - rootCmd.AddCommand(fileCmd) rootCmd.AddCommand(NewHardlinkCmd()) imageCmd, err := NewImageCmd() cobra.CheckErr(err) @@ -61,13 +58,7 @@ func NewRootCmd() *cobra.Command { menuCmd, err := NewMenuCmd() cobra.CheckErr(err) rootCmd.AddCommand(menuCmd) - mgmtClassCmd, err := NewMgmtClassCmd() - cobra.CheckErr(err) - rootCmd.AddCommand(mgmtClassCmd) rootCmd.AddCommand(NewMkLoadersCmd()) - packageCmd, err := NewPackageCmd() - cobra.CheckErr(err) - rootCmd.AddCommand(packageCmd) profileCmd, err := NewProfileCmd() cobra.CheckErr(err) rootCmd.AddCommand(profileCmd) @@ -84,6 +75,18 @@ func NewRootCmd() *cobra.Command { systemCmd, err := NewSystemCmd() cobra.CheckErr(err) rootCmd.AddCommand(systemCmd) + templateCmd, err := NewTemplateCmd() + cobra.CheckErr(err) + rootCmd.AddCommand(templateCmd) + distroGroupCmd, err := NewDistroGroupCmd() + cobra.CheckErr(err) + rootCmd.AddCommand(distroGroupCmd) + profileGroupCmd, err := NewProfileGroupCmd() + cobra.CheckErr(err) + rootCmd.AddCommand(profileGroupCmd) + systemGroupCmd, err := NewSystemGroupCmd() + cobra.CheckErr(err) + rootCmd.AddCommand(systemGroupCmd) rootCmd.AddCommand(NewValidateAutoinstallsCmd()) rootCmd.AddCommand(NewVersionCmd()) return rootCmd @@ -174,11 +177,6 @@ func printStructured(cmd *cobra.Command, dataStruct interface{}) { printStructured(cmd, &baseItem) continue } - if fieldName == "Resource" { - baseResource := f.Interface().(cobbler.Resource) - printStructured(cmd, &baseResource) - continue - } if fieldName == "Interfaces" { // Skip and print at the end continue @@ -195,8 +193,8 @@ func printStructured(cmd *cobra.Command, dataStruct interface{}) { // Print interfaces at the end of the output networkInterfacesField := s.FieldByName("Interfaces") if networkInterfacesField != (reflect.Value{}) { - networkInterfaces := networkInterfacesField.Interface().(cobbler.Interfaces) - printNetworkInterface(cmd, networkInterfaces) + networkInterfaces := networkInterfacesField.Interface().(map[string]*cobbler.NetworkInterface) + printNetworkInterfaces(cmd, networkInterfaces) } } @@ -211,10 +209,10 @@ func printValueStructured(cmd *cobra.Command, name string, value reflect.Value) } } -func printNetworkInterface(cmd *cobra.Command, networkInterface cobbler.Interfaces) { - for interfaceName, interfaceStruct := range networkInterface { +func printNetworkInterfaces(cmd *cobra.Command, networkInterfaces map[string]*cobbler.NetworkInterface) { + for interfaceName, interfaceStruct := range networkInterfaces { fmt.Fprintf(cmd.OutOrStdout(), "%-40s: %s\n", "Interface =====", interfaceName) - printStructured(cmd, &interfaceStruct) + printStructured(cmd, interfaceStruct) } } diff --git a/cmd/setting.go b/cmd/setting.go index b28d640..bed383e 100644 --- a/cmd/setting.go +++ b/cmd/setting.go @@ -5,9 +5,17 @@ package cmd import ( + "encoding/json" "errors" "fmt" + "reflect" + "sort" + "strconv" + "strings" + + cobbler "github.com/cobbler/cobblerclient" "github.com/spf13/cobra" + "gopkg.in/yaml.v3" ) // NewSettingCmd builds a new command that represents the setting action @@ -22,20 +30,95 @@ func NewSettingCmd() *cobra.Command { } settingCmd.AddCommand(NewSettingEditCmd()) settingCmd.AddCommand(NewSettingReportCmd()) + settingCmd.AddCommand(NewSettingExportCmd()) return settingCmd } +// settingsFieldByMapstructureTag walks the Settings struct and returns the +// reflect.Value of the field whose `mapstructure` tag matches name (or the +// zero Value if not found). +func settingsFieldByMapstructureTag(settings *cobbler.Settings, name string) reflect.Value { + v := reflect.ValueOf(settings).Elem() + t := v.Type() + for i := 0; i < t.NumField(); i++ { + if t.Field(i).Tag.Get("mapstructure") == name { + return v.Field(i) + } + } + return reflect.Value{} +} + +// listSettingNames returns the known setting names sorted alphabetically. +func listSettingNames(settings *cobbler.Settings) []string { + t := reflect.TypeOf(settings).Elem() + out := make([]string, 0, t.NumField()) + for i := 0; i < t.NumField(); i++ { + if tag := t.Field(i).Tag.Get("mapstructure"); tag != "" { + out = append(out, tag) + } + } + sort.Strings(out) + return out +} + +// parseSettingValue coerces the raw string the user passed via --value into +// the Go-typed value the corresponding Settings field expects. Cobbler 4.0.0 +// is strict about types on the wire, so coercion happens here, not on the +// server. +func parseSettingValue(field reflect.Value, raw string) (interface{}, error) { + switch field.Kind() { + case reflect.Bool: + return strconv.ParseBool(raw) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + v, err := strconv.ParseInt(raw, 10, 64) + if err != nil { + return nil, err + } + return int(v), nil + case reflect.Float32, reflect.Float64: + return strconv.ParseFloat(raw, 64) + case reflect.String: + return raw, nil + case reflect.Slice: + // Accept comma-separated list for []string and similar. + if field.Type().Elem().Kind() == reflect.String { + if raw == "" { + return []string{}, nil + } + parts := strings.Split(raw, ",") + out := make([]string, len(parts)) + for i, p := range parts { + out[i] = strings.TrimSpace(p) + } + return out, nil + } + case reflect.Map: + // Accept key=value,key=value form for map[string]interface{}. + out := map[string]interface{}{} + if raw == "" { + return out, nil + } + for _, pair := range strings.Split(raw, ",") { + kv := strings.SplitN(pair, "=", 2) + if len(kv) != 2 { + return nil, fmt.Errorf("malformed map entry %q (expected key=value)", pair) + } + out[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1]) + } + return out, nil + } + return nil, fmt.Errorf("unsupported setting type %s", field.Kind()) +} + func NewSettingEditCmd() *cobra.Command { settingEditCmd := &cobra.Command{ Use: "edit", Short: "edit settings", - Long: `Edits the settings.`, + Long: `Edits a setting. The value is parsed into the same type the existing setting has, so --value=42 sends an int when the server-side type is int.`, RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { + if err := generateCobblerClient(); err != nil { return err } - settings, err := Client.GetSettings() if err != nil { return err @@ -43,16 +126,23 @@ func NewSettingEditCmd() *cobra.Command { if !settings.AllowDynamicSettings { return errors.New("dynamic settings are turned off server-side") } - - settingName, err := cmd.Flags().GetString("name") + name, err := cmd.Flags().GetString("name") if err != nil { return err } - settingValue, err := cmd.Flags().GetString("value") + raw, err := cmd.Flags().GetString("value") if err != nil { return err } - result, err := Client.ModifySetting(settingName, settingValue) + field := settingsFieldByMapstructureTag(settings, name) + if !field.IsValid() { + return fmt.Errorf("unknown setting %q; run `cobbler setting report` to list valid names", name) + } + typed, err := parseSettingValue(field, raw) + if err != nil { + return fmt.Errorf("cannot parse --value %q as %s: %w", raw, field.Kind(), err) + } + result, err := Client.ModifySetting(name, typed) if err != nil { return err } @@ -65,7 +155,7 @@ func NewSettingEditCmd() *cobra.Command { }, } settingEditCmd.Flags().String("name", "", "the settings name to edit (e.g. server)") - settingEditCmd.Flags().String("value", "", "the new value (e.g. 127.0.0.1)") + settingEditCmd.Flags().String("value", "", "the new value (parsed according to the setting's type)") return settingEditCmd } @@ -75,16 +165,13 @@ func NewSettingReportCmd() *cobra.Command { Short: "list settings", Long: `Prints settings to stdout.`, RunE: func(cmd *cobra.Command, args []string) error { - err := generateCobblerClient() - if err != nil { + if err := generateCobblerClient(); err != nil { return err } - settings, err := Client.GetSettings() if err != nil { return err } - printStructured(cmd, settings) return nil }, @@ -92,3 +179,51 @@ func NewSettingReportCmd() *cobra.Command { settingReportCmd.Flags().String("name", "", "the settings name to show") return settingReportCmd } + +func NewSettingExportCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "export", + Short: "export settings", + PreRunE: func(cmd *cobra.Command, args []string) error { + format, err := cmd.Flags().GetString("format") + if err != nil { + return err + } + if format != "json" && format != "yaml" { + return fmt.Errorf("format must be json or yaml") + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + settings, err := Client.GetSettings() + if err != nil { + return err + } + format, err := cmd.Flags().GetString("format") + if err != nil { + return err + } + switch format { + case "json": + out, err := json.Marshal(settings) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), string(out)) + case "yaml": + out, err := yaml.Marshal(settings) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), "---") + fmt.Fprintln(cmd.OutOrStdout(), string(out)) + } + return nil + }, + } + cmd.Flags().String(exportStringMetadata["format"].Name, exportStringMetadata["format"].DefaultValue, exportStringMetadata["format"].Usage) + return cmd +} diff --git a/cmd/system.go b/cmd/system.go index 0f2aeb1..f27fa67 100644 --- a/cmd/system.go +++ b/cmd/system.go @@ -14,34 +14,10 @@ import ( ) func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { - // TODO: Implementation for more interfaces - // See https://github.com/cobbler/cli/issues/38 - systemNewInterface, err := cmd.Flags().GetString("interface") - if err != nil { - return err - } - deleteInterfaceFlag := cmd.Flags().Lookup("delete-interface") - renameInterfaceFlag := cmd.Flags().Lookup("rename-interface") - deleteInterface := deleteInterfaceFlag != nil && deleteInterfaceFlag.Changed - renameInterface := renameInterfaceFlag != nil && renameInterfaceFlag.Changed - systemInterface, keyInMap := system.Interfaces[systemNewInterface] - if systemNewInterface != "" && !keyInMap { - // Interface doesn't exist and non-empty string, so add a new one. - // We cannot call CreateInterface because the system might not exist. - system.Interfaces[systemNewInterface] = cobbler.Interface{} - systemInterface = system.Interfaces[systemNewInterface] - } - if renameInterface { - var systemNewInterfaceName string - systemNewInterfaceName, err = cmd.Flags().GetString("rename-interface") - if err != nil { - return err - } - err = system.RenameInterface(systemNewInterface, systemNewInterfaceName) - if err != nil { - return err - } - } + // Network interfaces are first-class items in Cobbler 4.0.0 and managed via + // the dedicated `cobbler interface` command. The legacy interface flags on + // `cobbler system add/edit/copy/rename/find` have been removed in v1.0.0. + var err error cmd.Flags().Visit(func(flag *pflag.Flag) { if err != nil { // If one of the previous flags has had an error just directly return. @@ -172,24 +148,6 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { system.KernelOptionsPost.IsInherited = false system.KernelOptionsPost.Data = convertMapStringToMapInterface(systemNewKernelOptionsPost) } - case "mgmt-classes": - fallthrough - case "mgmt-classes-inherit": - if cmd.Flags().Lookup("mgmt-classes-inherit").Changed { - system.MgmtClasses.Data = []string{} - system.MgmtClasses.IsInherited, err = cmd.Flags().GetBool("mgmt-classes-inherit") - if err != nil { - return - } - } else { - var systemNewMgmtClasses []string - systemNewMgmtClasses, err = cmd.Flags().GetStringSlice("mgmt-classes") - if err != nil { - return - } - system.MgmtClasses.IsInherited = false - system.MgmtClasses.Data = systemNewMgmtClasses - } case "owners": fallthrough case "owners-inherit": @@ -251,24 +209,6 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { system.EnableIPXE.IsInherited = false system.EnableIPXE.Data = systemNewEnableIpxe } - case "mgmt-parameters": - fallthrough - case "mgmt-parameters-inherit": - if cmd.Flags().Lookup("mgmt-parameters-inherit").Changed { - system.MgmtParameters.Data = make(map[string]interface{}) - system.MgmtParameters.IsInherited, err = cmd.Flags().GetBool("mgmt-parameters-inherit") - if err != nil { - return - } - } else { - var systemNewMgmtParameters map[string]string - systemNewMgmtParameters, err = cmd.Flags().GetStringToString("mgmt-parameters") - if err != nil { - return - } - system.MgmtParameters.IsInherited = false - system.MgmtParameters.Data = convertMapStringToMapInterface(systemNewMgmtParameters) - } case "name-servers": var systemNewNameServers []string systemNewNameServers, err = cmd.Flags().GetStringSlice("name-servers") @@ -522,243 +462,6 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { return } system.SerialBaudRate = systemNewSerialBaudRate - case "bonding-opts": - if renameInterface || deleteInterface || systemNewInterface == "" { - return - } - var systemNewBondingOpts string - systemNewBondingOpts, err = cmd.Flags().GetString("bonding-opts") - if err != nil { - return - } - systemInterface.BondingOpts = systemNewBondingOpts - case "bridge-opts": - if renameInterface || deleteInterface || systemNewInterface == "" { - return - } - var systemNewBridgeOpts string - systemNewBridgeOpts, err = cmd.Flags().GetString("bridge-opts") - if err != nil { - return - } - systemInterface.BridgeOpts = systemNewBridgeOpts - case "cnames": - if renameInterface || deleteInterface || systemNewInterface == "" { - return - } - var systemNewCNames []string - systemNewCNames, err = cmd.Flags().GetStringSlice("cnames") - if err != nil { - return - } - systemInterface.CNAMEs = systemNewCNames - case "connected-mode": - if renameInterface || deleteInterface || systemNewInterface == "" { - return - } - var systemNewConnectedMode bool - systemNewConnectedMode, err = cmd.Flags().GetBool("connected-mode") - if err != nil { - return - } - systemInterface.ConnectedMode = systemNewConnectedMode - case "dhcp-tag": - if renameInterface || deleteInterface || systemNewInterface == "" { - return - } - var systemNewDhcpTag string - systemNewDhcpTag, err = cmd.Flags().GetString("dhcp-tag") - if err != nil { - return - } - systemInterface.DHCPTag = systemNewDhcpTag - case "dns-name": - if renameInterface || deleteInterface || systemNewInterface == "" { - return - } - var systemNewDnsName string - systemNewDnsName, err = cmd.Flags().GetString("dns-name") - if err != nil { - return - } - systemInterface.DNSName = systemNewDnsName - case "if-gateway": - if renameInterface || deleteInterface || systemNewInterface == "" { - return - } - var systemNewIfGateway string - systemNewIfGateway, err = cmd.Flags().GetString("if-gateway") - if err != nil { - return - } - systemInterface.Gateway = systemNewIfGateway - case "interface-master": - if renameInterface || deleteInterface || systemNewInterface == "" { - return - } - var systemNewInterfaceMaster string - systemNewInterfaceMaster, err = cmd.Flags().GetString("interface-master") - if err != nil { - return - } - systemInterface.InterfaceMaster = systemNewInterfaceMaster - case "interface-type": - if renameInterface || deleteInterface || systemNewInterface == "" { - return - } - var systemNewInterfaceType string - systemNewInterfaceType, err = cmd.Flags().GetString("interface-type") - if err != nil { - return - } - systemInterface.InterfaceType = systemNewInterfaceType - case "ip-address": - if renameInterface || deleteInterface || systemNewInterface == "" { - return - } - var systemNewIpAddress string - systemNewIpAddress, err = cmd.Flags().GetString("ip-address") - if err != nil { - return - } - systemInterface.IPAddress = systemNewIpAddress - case "ipv6-address": - if renameInterface || deleteInterface || systemNewInterface == "" { - return - } - var systemNewIpv6Address string - systemNewIpv6Address, err = cmd.Flags().GetString("ipv6-address") - if err != nil { - return - } - systemInterface.IPv6Address = systemNewIpv6Address - case "ipv6-default-gateway": - if renameInterface || deleteInterface || systemNewInterface == "" { - return - } - var systemNewIpv6DefaultGateway string - systemNewIpv6DefaultGateway, err = cmd.Flags().GetString("ipv6-default-gateway") - if err != nil { - return - } - systemInterface.IPv6DefaultGateway = systemNewIpv6DefaultGateway - case "ipv6-mtu": - if renameInterface || deleteInterface || systemNewInterface == "" { - return - } - var systemNewIpv6Mtu string - systemNewIpv6Mtu, err = cmd.Flags().GetString("ipv6-mtu") - if err != nil { - return - } - systemInterface.IPv6MTU = systemNewIpv6Mtu - case "ipv6-prefix": - if renameInterface || deleteInterface || systemNewInterface == "" { - return - } - var systemNewIpv6Prefix string - systemNewIpv6Prefix, err = cmd.Flags().GetString("ipv6-prefix") - if err != nil { - return - } - systemInterface.IPv6Prefix = systemNewIpv6Prefix - case "ipv6-secondaries": - if renameInterface || deleteInterface || systemNewInterface == "" { - return - } - var systemNewIpv6Secondaries []string - systemNewIpv6Secondaries, err = cmd.Flags().GetStringSlice("ipv6-secondaries") - if err != nil { - return - } - systemInterface.IPv6Secondaries = systemNewIpv6Secondaries - case "ipv6-static-routes": - if renameInterface || deleteInterface || systemNewInterface == "" { - return - } - var systemNewIpv6StaticRoutes []string - systemNewIpv6StaticRoutes, err = cmd.Flags().GetStringSlice("ipv6-static-routes") - if err != nil { - return - } - systemInterface.IPv6StaticRoutes = systemNewIpv6StaticRoutes - case "mac-address": - if renameInterface || deleteInterface || systemNewInterface == "" { - return - } - var systemNewMacAddress string - systemNewMacAddress, err = cmd.Flags().GetString("mac-address") - if err != nil { - return - } - systemInterface.MACAddress = systemNewMacAddress - case "management": - if renameInterface || deleteInterface || systemNewInterface == "" { - return - } - var systemNewManagement bool - systemNewManagement, err = cmd.Flags().GetBool("management") - if err != nil { - return - } - systemInterface.Management = systemNewManagement - case "mtu": - if renameInterface || deleteInterface || systemNewInterface == "" { - return - } - var systemNewMtu string - systemNewMtu, err = cmd.Flags().GetString("mtu") - if err != nil { - return - } - systemInterface.MTU = systemNewMtu - case "netmask": - if renameInterface || deleteInterface || systemNewInterface == "" { - return - } - var systemNewNetmask string - systemNewNetmask, err = cmd.Flags().GetString("netmask") - if err != nil { - return - } - systemInterface.Netmask = systemNewNetmask - case "static": - if renameInterface || deleteInterface || systemNewInterface == "" { - return - } - var systemNewStatic bool - systemNewStatic, err = cmd.Flags().GetBool("static") - if err != nil { - return - } - systemInterface.Static = systemNewStatic - case "static-routes": - if renameInterface || deleteInterface || systemNewInterface == "" { - return - } - var systemNewStaticRoutes []string - systemNewStaticRoutes, err = cmd.Flags().GetStringSlice("static-routes") - if err != nil { - return - } - systemInterface.StaticRoutes = systemNewStaticRoutes - case "virt-bridge": - if renameInterface || deleteInterface || systemNewInterface == "" { - return - } - var systemNewVirtBridge string - systemNewVirtBridge, err = cmd.Flags().GetString("virt-bridge") - if err != nil { - return - } - systemInterface.VirtBridge = systemNewVirtBridge - case "delete-interface": - err = system.DeleteInterface(systemNewInterface) - if err != nil { - return - } - delete(system.Interfaces, systemNewInterface) - system.Meta.IsDirty = true } }) // Don't blindly return nil because maybe one of the flags had an issue retrieving an argument. @@ -834,13 +537,7 @@ func NewSystemAddCmd() *cobra.Command { addFloatFlags(systemAddCmd, systemFloatFlagMetadata) addStringSliceFlags(systemAddCmd, systemStringSliceFlagMetadata) addMapFlags(systemAddCmd, systemMapFlagMetadata) - // Network interface flags - addStringFlags(systemAddCmd, interfaceStringFlagMetadata) - addBoolFlags(systemAddCmd, interfaceBoolFlagMetadata) - addStringSliceFlags(systemAddCmd, interfaceStringSliceFlagMetadata) - // Other systemAddCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") - systemAddCmd.Flags().String("interface", "", "the interface to operate on") return systemAddCmd } @@ -904,15 +601,8 @@ func NewSystemCopyCmd() *cobra.Command { addStringSliceFlags(systemCopyCmd, systemStringSliceFlagMetadata) addMapFlags(systemCopyCmd, systemMapFlagMetadata) // Network interface flags - addStringFlags(systemCopyCmd, interfaceStringFlagMetadata) - addBoolFlags(systemCopyCmd, interfaceBoolFlagMetadata) - addStringSliceFlags(systemCopyCmd, interfaceStringSliceFlagMetadata) - // Other addStringFlags(systemCopyCmd, copyRenameStringFlagMetadata) systemCopyCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") - systemCopyCmd.Flags().String("interface", "", "the interface to operate on") - systemCopyCmd.Flags().Bool("delete-interface", false, "delete the given interface (should be used with --interface)") - systemCopyCmd.Flags().String("rename-interface", "", "rename the given interface (should be used with --interface)") return systemCopyCmd } @@ -927,18 +617,18 @@ func NewSystemDumpVarsCmd() *cobra.Command { return err } - // Get CLI flags systemName, err := cmd.Flags().GetString("name") if err != nil { return err } - - // Now retrieve data - blendedData, err := Client.GetBlendedData("", systemName) + system, err := Client.GetSystem(systemName, false, false) + if err != nil { + return err + } + blendedData, err := Client.DumpVars(system.Uid, false, false) if err != nil { return err } - // Print data printDumpVars(cmd, blendedData) return err }, @@ -996,14 +686,7 @@ func NewSystemEditCmd() *cobra.Command { addStringSliceFlags(systemEditCmd, systemStringSliceFlagMetadata) addMapFlags(systemEditCmd, systemMapFlagMetadata) // Network interface flags - addStringFlags(systemEditCmd, interfaceStringFlagMetadata) - addBoolFlags(systemEditCmd, interfaceBoolFlagMetadata) - addStringSliceFlags(systemEditCmd, interfaceStringSliceFlagMetadata) - // Other systemEditCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") - systemEditCmd.Flags().String("interface", "", "the interface to operate on") - systemEditCmd.Flags().Bool("delete-interface", false, "delete the given interface (should be used with --interface)") - systemEditCmd.Flags().String("rename-interface", "", "rename the given interface (should be used with --interface)") return systemEditCmd } @@ -1030,14 +713,10 @@ func NewSystemFindCmd() *cobra.Command { addStringSliceFlags(systemFindCmd, systemStringSliceFlagMetadata) addMapFlags(systemFindCmd, systemMapFlagMetadata) // Network interface flags - addStringFlags(systemFindCmd, interfaceStringFlagMetadata) - addBoolFlags(systemFindCmd, interfaceBoolFlagMetadata) - addStringSliceFlags(systemFindCmd, interfaceStringSliceFlagMetadata) - // Other addStringFlags(systemFindCmd, findStringFlagMetadata) addIntFlags(systemFindCmd, findIntFlagMetadata) addFloatFlags(systemFindCmd, findFloatFlagMetadata) - systemFindCmd.Flags().String("interface", "", "the interface to operate on") + addPaginationFlags(systemFindCmd) return systemFindCmd } @@ -1064,7 +743,7 @@ func NewSystemGetAutoinstallCmd() *cobra.Command { //goland:noinspection GoErrorStringFormat return fmt.Errorf("System does not exist") } - autoinstallRendered, err := Client.GenerateAutoinstall("", systemName) + autoinstallRendered, err := Client.GenerateAutoinstall(systemName, "system", "name", "", "") if err != nil { return err } @@ -1297,15 +976,8 @@ func NewSystemRenameCmd() *cobra.Command { addStringSliceFlags(systemRenameCmd, systemStringSliceFlagMetadata) addMapFlags(systemRenameCmd, systemMapFlagMetadata) // Network interface flags - addStringFlags(systemRenameCmd, interfaceStringFlagMetadata) - addBoolFlags(systemRenameCmd, interfaceBoolFlagMetadata) - addStringSliceFlags(systemRenameCmd, interfaceStringSliceFlagMetadata) - // Other addStringFlags(systemRenameCmd, copyRenameStringFlagMetadata) systemRenameCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") - systemRenameCmd.Flags().String("interface", "", "the interface to operate on") - systemRenameCmd.Flags().Bool("delete-interface", false, "delete the given interface (should be used with --interface)") - systemRenameCmd.Flags().String("rename-interface", "", "rename the given interface (should be used with --interface)") return systemRenameCmd } diff --git a/cmd/system_group.go b/cmd/system_group.go new file mode 100644 index 0000000..82f9f4f --- /dev/null +++ b/cmd/system_group.go @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright SUSE LLC + +package cmd + +import ( + "fmt" + + cobbler "github.com/cobbler/cobblerclient" + "github.com/spf13/cobra" +) + +// NewSystemGroupCmd builds the `cobbler system-group` command tree. +func NewSystemGroupCmd() (*cobra.Command, error) { + cmd := &cobra.Command{ + Use: "system-group", + Short: "Manage system groups", + Long: `Manage Cobbler 4.0.0 SystemGroup items.`, + } + cmd.AddCommand(newSystemGroupAddCmd()) + cmd.AddCommand(newSystemGroupCopyCmd()) + cmd.AddCommand(newSystemGroupEditCmd()) + cmd.AddCommand(newSystemGroupFindCmd()) + cmd.AddCommand(newSystemGroupListCmd()) + cmd.AddCommand(newSystemGroupRemoveCmd()) + cmd.AddCommand(newSystemGroupRenameCmd()) + cmd.AddCommand(newSystemGroupReportCmd()) + cmd.AddCommand(newSystemGroupExportCmd()) + return cmd, nil +} + +func newSystemGroupAddCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "add", + Short: "add a system group", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + g := cobbler.NewSystemGroup() + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + g.Name = name + comment, commentSet, items, itemsSet, err := extractGroupFlags(cmd) + if err != nil { + return err + } + if commentSet { + g.Comment = comment + } + if itemsSet { + g.Items = items + } + created, err := Client.CreateSystemGroup(g) + if err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "System group %s created\n", created.Name) + return nil + }, + } + addGroupFlagSet(cmd) + return cmd +} + +func newSystemGroupEditCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "edit", + Short: "edit a system group", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + g, err := Client.GetSystemGroup(name, false, false) + if err != nil { + return err + } + comment, commentSet, items, itemsSet, err := extractGroupFlags(cmd) + if err != nil { + return err + } + if commentSet { + g.Comment = comment + } + if itemsSet { + g.Items = items + } + return Client.UpdateSystemGroup(g) + }, + } + addGroupFlagSet(cmd) + return cmd +} + +func newSystemGroupCopyCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "copy", + Short: "copy a system group", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + newName, err := cmd.Flags().GetString("newname") + if err != nil { + return err + } + handle, err := Client.GetSystemGroupHandle(name) + if err != nil { + return err + } + return Client.CopySystemGroup(handle, newName) + }, + } + addStringFlags(cmd, commonStringFlagMetadata) + addStringFlags(cmd, copyRenameStringFlagMetadata) + return cmd +} + +func newSystemGroupRenameCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "rename", + Short: "rename a system group", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + newName, err := cmd.Flags().GetString("newname") + if err != nil { + return err + } + handle, err := Client.GetSystemGroupHandle(name) + if err != nil { + return err + } + return Client.RenameSystemGroup(handle, newName) + }, + } + addStringFlags(cmd, commonStringFlagMetadata) + addStringFlags(cmd, copyRenameStringFlagMetadata) + return cmd +} + +func newSystemGroupRemoveCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "remove", + Short: "remove a system group", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + return RemoveItemRecursive(cmd, args, "system_group") + }, + } + cmd.Flags().String("name", "", "the system group name") + cmd.Flags().Bool("recursive", false, "also delete child objects") + return cmd +} + +func newSystemGroupFindCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "find", + Short: "find system groups", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + return FindItemNames(cmd, args, "system_group") + }, + } + addStringFlags(cmd, commonStringFlagMetadata) + addStringSliceFlags(cmd, groupStringSliceFlagMetadata) + addPaginationFlags(cmd) + return cmd +} + +func newSystemGroupListCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "list all system groups", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + names, err := Client.ListSystemGroupNames() + if err != nil { + return err + } + listItems(cmd, "system_groups", names) + return nil + }, + } + return cmd +} + +func newSystemGroupReportCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "report", + Short: "show system group details", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + names := make([]string, 0) + if name == "" { + names, err = Client.ListSystemGroupNames() + if err != nil { + return err + } + } else { + names = append(names, name) + } + for _, n := range names { + g, err := Client.GetSystemGroup(n, false, false) + if err != nil { + return err + } + printStructured(cmd, g) + fmt.Fprintln(cmd.OutOrStdout(), "") + } + return nil + }, + } + cmd.Flags().String("name", "", "the system group name") + return cmd +} + +func newSystemGroupExportCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "export", + Short: "export system groups", + PreRunE: func(cmd *cobra.Command, args []string) error { + format, err := cmd.Flags().GetString("format") + if err != nil { + return err + } + if format != "json" && format != "yaml" { + return fmt.Errorf("format must be json or yaml") + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + format, err := cmd.Flags().GetString("format") + if err != nil { + return err + } + names := make([]string, 0) + if name == "" { + names, err = Client.ListSystemGroupNames() + if err != nil { + return err + } + } else { + names = append(names, name) + } + for _, n := range names { + g, err := Client.GetSystemGroup(n, false, false) + if err != nil { + return err + } + if err := writeExport(cmd, format, g); err != nil { + return err + } + } + return nil + }, + } + cmd.Flags().String("name", "", "the system group name") + cmd.Flags().String(exportStringMetadata["format"].Name, exportStringMetadata["format"].DefaultValue, exportStringMetadata["format"].Usage) + return cmd +} diff --git a/cmd/template.go b/cmd/template.go new file mode 100644 index 0000000..44cac57 --- /dev/null +++ b/cmd/template.go @@ -0,0 +1,451 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright SUSE LLC + +package cmd + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + cobbler "github.com/cobbler/cobblerclient" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "gopkg.in/yaml.v3" +) + +var templateStringFlagMetadata = map[string]FlagMetadata[string]{ + "template-type": { + Name: "template-type", + DefaultValue: "jinja2", + Usage: "template engine (jinja2, cheetah)", + }, + "uri-schema": { + Name: "uri-schema", + DefaultValue: "file", + Usage: "URI schema (file, importlib, environment)", + }, + "uri-path": { + Name: "uri-path", + DefaultValue: "", + Usage: "URI path (file path, importlib path, or env var name)", + }, + "content": { + Name: "content", + DefaultValue: "", + Usage: "inline template content (mutually exclusive with --content-file)", + }, + "content-file": { + Name: "content-file", + DefaultValue: "", + Usage: "path to a file whose contents become the template content", + }, +} + +var templateStringSliceFlagMetadata = map[string]FlagMetadata[[]string]{ + "tags": { + Name: "tags", + DefaultValue: []string{}, + Usage: "template tags (comma delimited)", + }, +} + +// parseTemplateSchema converts the CLI string form to the typed enum. +func parseTemplateSchema(s string) (cobbler.TemplateSchema, error) { + switch strings.ToLower(s) { + case "", "file": + return cobbler.TemplateSchemaFile, nil + case "importlib": + return cobbler.TemplateSchemaImportlib, nil + case "environment": + return cobbler.TemplateSchemaEnvironment, nil + } + return cobbler.TemplateSchemaFile, fmt.Errorf("unknown URI schema %q", s) +} + +// updateTemplateFromFlags applies any set --flags to t. +func updateTemplateFromFlags(cmd *cobra.Command, t *cobbler.Template) error { + var err error + cmd.Flags().Visit(func(flag *pflag.Flag) { + if err != nil { + return + } + switch flag.Name { + case "comment": + t.Comment, err = cmd.Flags().GetString("comment") + case "template-type": + t.TemplateType, err = cmd.Flags().GetString("template-type") + case "uri-schema": + var v string + v, err = cmd.Flags().GetString("uri-schema") + if err != nil { + return + } + t.URI.Schema, err = parseTemplateSchema(v) + case "uri-path": + t.URI.Path, err = cmd.Flags().GetString("uri-path") + case "content": + t.Content, err = cmd.Flags().GetString("content") + case "content-file": + var path string + path, err = cmd.Flags().GetString("content-file") + if err != nil || path == "" { + return + } + var data []byte + data, err = os.ReadFile(path) + if err != nil { + return + } + t.Content = string(data) + case "tags": + t.Tags, err = cmd.Flags().GetStringSlice("tags") + } + }) + return err +} + +func addTemplateFlagSet(cmd *cobra.Command) { + addCommonArgs(cmd) + addStringFlags(cmd, templateStringFlagMetadata) + addStringSliceFlags(cmd, templateStringSliceFlagMetadata) +} + +// NewTemplateCmd builds the `cobbler template` command tree. +func NewTemplateCmd() (*cobra.Command, error) { + templateCmd := &cobra.Command{ + Use: "template", + Short: "Manage autoinstall templates", + Long: `Manage Cobbler 4.0.0 Template items.`, + } + templateCmd.AddCommand(NewTemplateAddCmd()) + templateCmd.AddCommand(NewTemplateCopyCmd()) + templateCmd.AddCommand(NewTemplateEditCmd()) + templateCmd.AddCommand(NewTemplateFindCmd()) + templateCmd.AddCommand(NewTemplateListCmd()) + templateCmd.AddCommand(NewTemplateRemoveCmd()) + templateCmd.AddCommand(NewTemplateRenameCmd()) + templateCmd.AddCommand(NewTemplateReportCmd()) + templateCmd.AddCommand(NewTemplateExportCmd()) + templateCmd.AddCommand(NewTemplateContentCmd()) + templateCmd.AddCommand(NewTemplateRefreshCmd()) + return templateCmd, nil +} + +func NewTemplateAddCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "add", + Short: "add a template", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + t := cobbler.NewTemplate() + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + t.Name = name + if err := updateTemplateFromFlags(cmd, &t); err != nil { + return err + } + created, err := Client.CreateTemplate(t) + if err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "Template %s created\n", created.Name) + return nil + }, + } + addTemplateFlagSet(cmd) + return cmd +} + +func NewTemplateEditCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "edit", + Short: "edit a template", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + t, err := Client.GetTemplate(name, false, false) + if err != nil { + return err + } + if err := updateTemplateFromFlags(cmd, t); err != nil { + return err + } + return Client.UpdateTemplate(t) + }, + } + addTemplateFlagSet(cmd) + return cmd +} + +func NewTemplateCopyCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "copy", + Short: "copy a template", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + newName, err := cmd.Flags().GetString("newname") + if err != nil { + return err + } + handle, err := Client.GetTemplateHandle(name) + if err != nil { + return err + } + return Client.CopyTemplate(handle, newName) + }, + } + addStringFlags(cmd, commonStringFlagMetadata) + addStringFlags(cmd, copyRenameStringFlagMetadata) + return cmd +} + +func NewTemplateRenameCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "rename", + Short: "rename a template", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + newName, err := cmd.Flags().GetString("newname") + if err != nil { + return err + } + handle, err := Client.GetTemplateHandle(name) + if err != nil { + return err + } + return Client.RenameTemplate(handle, newName) + }, + } + addStringFlags(cmd, commonStringFlagMetadata) + addStringFlags(cmd, copyRenameStringFlagMetadata) + return cmd +} + +func NewTemplateRemoveCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "remove", + Short: "remove a template", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + return RemoveItemRecursive(cmd, args, "template") + }, + } + cmd.Flags().String("name", "", "the template name") + cmd.Flags().Bool("recursive", false, "also delete child objects") + return cmd +} + +func NewTemplateFindCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "find", + Short: "find templates", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + return FindItemNames(cmd, args, "template") + }, + } + addStringFlags(cmd, commonStringFlagMetadata) + addStringFlags(cmd, templateStringFlagMetadata) + addStringSliceFlags(cmd, templateStringSliceFlagMetadata) + addPaginationFlags(cmd) + return cmd +} + +func NewTemplateListCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "list all templates", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + names, err := Client.ListTemplateNames() + if err != nil { + return err + } + listItems(cmd, "templates", names) + return nil + }, + } + return cmd +} + +func NewTemplateReportCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "report", + Short: "show template details", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + names := make([]string, 0) + if name == "" { + names, err = Client.ListTemplateNames() + if err != nil { + return err + } + } else { + names = append(names, name) + } + for _, n := range names { + t, err := Client.GetTemplate(n, false, false) + if err != nil { + return err + } + printStructured(cmd, t) + fmt.Fprintln(cmd.OutOrStdout(), "") + } + return nil + }, + } + cmd.Flags().String("name", "", "the template name") + return cmd +} + +func NewTemplateExportCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "export", + Short: "export templates", + PreRunE: func(cmd *cobra.Command, args []string) error { + format, err := cmd.Flags().GetString("format") + if err != nil { + return err + } + if format != "json" && format != "yaml" { + return fmt.Errorf("format must be json or yaml") + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + format, err := cmd.Flags().GetString("format") + if err != nil { + return err + } + names := make([]string, 0) + if name == "" { + names, err = Client.ListTemplateNames() + if err != nil { + return err + } + } else { + names = append(names, name) + } + for _, n := range names { + t, err := Client.GetTemplate(n, false, false) + if err != nil { + return err + } + switch format { + case "json": + out, err := json.Marshal(t) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), string(out)) + case "yaml": + out, err := yaml.Marshal(t) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), "---") + fmt.Fprintln(cmd.OutOrStdout(), string(out)) + } + } + return nil + }, + } + cmd.Flags().String("name", "", "the template name") + cmd.Flags().String(exportStringMetadata["format"].Name, exportStringMetadata["format"].DefaultValue, exportStringMetadata["format"].Usage) + return cmd +} + +// NewTemplateContentCmd dumps the resolved template content to stdout. +func NewTemplateContentCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "content", + Short: "print resolved template content", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + if name == "" { + return fmt.Errorf("--name is required") + } + t, err := Client.GetTemplate(name, false, false) + if err != nil { + return err + } + content, err := Client.GetTemplateContent(t.Uid) + if err != nil { + return err + } + fmt.Fprint(cmd.OutOrStdout(), content) + return nil + }, + } + cmd.Flags().String("name", "", "the template name") + return cmd +} + +// NewTemplateRefreshCmd forces a backend re-read of one or more templates. +// With no --name flag, all templates are refreshed. +func NewTemplateRefreshCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "refresh", + Short: "refresh template content from disk", + RunE: func(cmd *cobra.Command, args []string) error { + if err := generateCobblerClient(); err != nil { + return err + } + names, err := cmd.Flags().GetStringSlice("name") + if err != nil { + return err + } + return Client.TemplatesRefreshContent(names) + }, + } + cmd.Flags().StringSlice("name", []string{}, "the template name(s) to refresh; omit for all") + return cmd +} diff --git a/go.mod b/go.mod index b616681..f32c142 100644 --- a/go.mod +++ b/go.mod @@ -3,18 +3,21 @@ module github.com/cobbler/cli go 1.22 require ( - github.com/cobbler/cobblerclient v0.5.8 + github.com/cobbler/cobblerclient v1.0.0-rc1 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.19.0 gopkg.in/yaml.v3 v3.0.1 ) +// Local development against the in-progress 4.0.0 client. +// Remove this before tagging v1.0.0-rc1. +replace github.com/cobbler/cobblerclient => /home/enno/Sources/GolandProjects/cobblerclient + require ( - github.com/fatih/structs v1.1.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-test/deep v1.1.1 // indirect - github.com/go-viper/mapstructure/v2 v2.0.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect diff --git a/go.sum b/go.sum index b483321..46db7a4 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,16 @@ -github.com/cobbler/cobblerclient v0.5.8 h1:4P5x3M0sy0PBgBY7uULI70FXUQqLC2RyTyFhVWusH8o= -github.com/cobbler/cobblerclient v0.5.8/go.mod h1:n6b8fTUOlg7BdMl6FeifUm4Uk1JY6/tlTlOClV4x2Wc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= -github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= -github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= diff --git a/testing/start.sh b/testing/start.sh index 1d75463..3110535 100755 --- a/testing/start.sh +++ b/testing/start.sh @@ -6,25 +6,28 @@ if [ -z "$1" ] echo "No cobbler server url supplied" fi -cobbler_commit=df356046f3cf27be62a61001b982d5983800cfd9 # 3.3.6 as of 2024-10-09 -cobbler_branch=release33 +# Cobbler 4.0.0 RC. Pin to a specific main-branch SHA until a 4.0.0 tag exists. +cobbler_commit=fa0610b28554f3ae073880aad362d4f04837d334 # 4.0.0 dev as of 2026-06-09 +cobbler_branch=main iso_url=https://cdimage.ubuntu.com/ubuntu-legacy-server/releases/20.04/release/ubuntu-20.04.1-legacy-server-amd64.iso iso_os=ubuntu valid_iso_checksum=00a9d46306fbe9beb3581853a289490bc231c51f iso_filename=$(echo ${iso_url##*/}) valid_extracted_iso_checksum=dd0b3148e1f071fb86aee4b0395fd63b -valid_git_checksum=6c9511b26946dd3f1f072b9f40eaeccf # master as of 4/2/2022 -[ -d "./testing/cobbler_source" ] && git_checksum=$(find ./testing/cobbler_source/ -type f -exec md5sum {} \; | sort -k 2 | md5sum | awk '{print $1}') -if [ -d "./testing/cobbler_source" ] && [ $git_checksum == $valid_git_checksum ]; then - echo "Cobbler code already cloned and the correct version is checked out" -else - rm -rf ./testing/cobbler_source - git clone --shallow-since="2021-09-01" https://github.com/cobbler/cobbler.git -b $cobbler_branch testing/cobbler_source +if [ -d "./testing/cobbler_source" ]; then + current_commit=$(cd ./testing/cobbler_source && git rev-parse HEAD 2>/dev/null || echo unknown) + if [ "$current_commit" = "$cobbler_commit" ]; then + echo "Cobbler code already cloned and the correct commit is checked out" + else + rm -rf ./testing/cobbler_source + fi +fi +if [ ! -d "./testing/cobbler_source" ]; then + git clone https://github.com/cobbler/cobbler.git -b $cobbler_branch testing/cobbler_source cd ./testing/cobbler_source printf "Changing to version of Cobbler being tested.\n\n" git checkout $cobbler_commit > /dev/null 2>&1 - rm -rf .git # remove .git dir so the checksum is consistent cd - fi