From 6f13903e0b8a3f4a1f5126f64f29714be86a07f5 Mon Sep 17 00:00:00 2001 From: Enno Gotthold Date: Mon, 19 Oct 2026 11:22:46 +0200 Subject: [PATCH 1/6] feat(cmd): add cobbler check command Expose cobblerclient's Check() action as a new top-level, zero-flag command that reports potential Cobbler server configuration problems in the classic numbered-list format. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 1 + cmd/check.go | 46 +++++++++++++++++++++++++++++++++++++++++ cmd/check_test.go | 37 +++++++++++++++++++++++++++++++++ cmd/root.go | 1 + docs/commands.rst | 1 + docs/commands/check.rst | 15 ++++++++++++++ 6 files changed, 101 insertions(+) create mode 100644 cmd/check.go create mode 100644 cmd/check_test.go create mode 100644 docs/commands/check.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index 2162e60..957b735 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ Clean break — talks only to Cobbler 4.0.0+. For 3.3.x servers, stay on the - `--page` and `--items-per-page` flags on every `find` subcommand (routes through `find_items_paged`). - `cobbler setting export`. +- `cobbler check` — reports potential Cobbler server configuration problems. ### Removed (no longer accepted) diff --git a/cmd/check.go b/cmd/check.go new file mode 100644 index 0000000..183bf67 --- /dev/null +++ b/cmd/check.go @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-FileCopyrightText: Copyright SUSE LLC + +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// NewCheckCmd builds a new command that represents the check action +func NewCheckCmd() *cobra.Command { + checkCmd := &cobra.Command{ + Use: "check", + Short: "Check the Cobbler server configuration for common problems.", + Long: `This command asks the Cobbler server to inspect its own configuration (settings, installed +dependencies, boot-loader files, services, etc.) and reports suggestions for items you may want to fix. It +does not change anything on the server; it only lists potential problems for the administrator to review.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + checks, err := Client.Check() + if err != nil { + return err + } + + if len(*checks) == 0 { + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "No configuration problems found.") + return nil + } + + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "The following are potential configuration items that you may want to fix:") + for i, check := range *checks { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%d : %s\n", i+1, check) + } + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Restart cobblerd and then run 'cobbler sync' to apply changes.") + + return nil + }, + } + return checkCmd +} diff --git a/cmd/check_test.go b/cmd/check_test.go new file mode 100644 index 0000000..865fed8 --- /dev/null +++ b/cmd/check_test.go @@ -0,0 +1,37 @@ +package cmd + +import ( + "bytes" + "github.com/cobbler/cobblerclient" + "github.com/spf13/cobra" + "io" + "strings" + "testing" +) + +func Test_CheckCmd(t *testing.T) { + // Arrange + cobra.OnInitialize(initConfig, setupLogger) + rootCmd := NewRootCmd() + rootCmd.SetArgs([]string{"--config", "../testing/.cobbler.yaml", "check"}) + stdout := bytes.NewBufferString("") + stderr := bytes.NewBufferString("") + rootCmd.SetOut(stdout) + rootCmd.SetErr(stderr) + + // Act + err := rootCmd.Execute() + + // Assert + cobblerclient.FailOnError(t, err) + FailOnNonEmptyStream(t, stderr) + stdoutBytes, err := io.ReadAll(stdout) + if err != nil { + t.Fatal(err) + } + stdoutString := string(stdoutBytes) + if !strings.Contains(stdoutString, "No configuration problems found.") && + !strings.Contains(stdoutString, "The following are potential configuration items") { + t.Fatalf("expected one of the known check-command headers in output, got: %q", stdoutString) + } +} diff --git a/cmd/root.go b/cmd/root.go index 0ceefd1..f138fd8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -42,6 +42,7 @@ func NewRootCmd() *cobra.Command { // Add sub commands rootCmd.AddCommand(NewAclSetupCmd()) rootCmd.AddCommand(NewBuildisoCmd()) + rootCmd.AddCommand(NewCheckCmd()) distroCmd, err := NewDistroCmd() cobra.CheckErr(err) rootCmd.AddCommand(distroCmd) diff --git a/docs/commands.rst b/docs/commands.rst index d5be847..dbc8b89 100644 --- a/docs/commands.rst +++ b/docs/commands.rst @@ -16,6 +16,7 @@ Long Usage: cobbler aclsetup cobbler buildiso + cobbler check cobbler distro cobbler hardlink cobbler image diff --git a/docs/commands/check.rst b/docs/commands/check.rst new file mode 100644 index 0000000..2d87065 --- /dev/null +++ b/docs/commands/check.rst @@ -0,0 +1,15 @@ +************* +cobbler check +************* + +Check the Cobbler server configuration for common problems. + +Example: + +.. code-block:: shell + + $ cobbler check + +This command asks the Cobbler server to inspect its own configuration (settings, installed dependencies, +boot-loader files, services, etc.) and reports suggestions for items you may want to fix. It does not change +anything on the server; it only lists potential problems for the administrator to review. From b14d7f930be88ab95fd7a7a40c20c252d90f6291 Mon Sep 17 00:00:00 2001 From: Enno Gotthold Date: Mon, 19 Oct 2026 16:05:02 +0200 Subject: [PATCH 2/6] fix(testing): install wget in the dev-stack container The published cobblerd test image omits wget, which the server's check action shells out to probe. Without it, cobbler check surfaces as an XML-RPC fault instead of a reported suggestion, which broke Test_CheckCmd in CI. Install wget on container startup via zypper. Co-Authored-By: Claude Sonnet 5 --- testing/compose.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/testing/compose.yml b/testing/compose.yml index b59f02e..c087f4b 100644 --- a/testing/compose.yml +++ b/testing/compose.yml @@ -8,4 +8,6 @@ services: - ../extracted_iso_image:/extracted_iso_image:ro,z ports: - 25151:25151 - command: cobblerd -F + # The published cobblerd image is missing `wget`, which the server's `check` action shells + # out to probe; without it, `cobbler check` faults instead of reporting it as a suggestion. + command: sh -c "zypper --non-interactive install wget && cobblerd -F" From a1076c577f1fc0eff1fa1e31f407de430f005719 Mon Sep 17 00:00:00 2001 From: Enno Gotthold Date: Tue, 20 Oct 2026 15:34:49 +0200 Subject: [PATCH 3/6] fix(cmd): accept JSON-string values in --autoinstall-meta convertMapStringToMapInterface, the shared helper that every distro/profile/system/image add/copy/edit command uses to build the XML-RPC payload for stringToString flags like --autoinstall-meta, copied every value through as a flat string. Autoinstall-meta keys that need a list or dict value (e.g. Agama's agama_modules, or cloud-init's per-module settings like cloud_init_set_hostname) had no way to be set correctly: the literal string got sent instead, which built-in Jinja templates then misinterpreted (e.g. iterating a string character-by-character where a list was expected). Values starting with '[' or '{' are now decoded as JSON and sent as the real nested value; anything that fails to parse, or doesn't look like JSON, is left as a flat string exactly as before, so existing usage is unaffected. Co-Authored-By: Claude Sonnet 5 --- cmd/utils.go | 13 +++++++++++++ cmd/utils_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/cmd/utils.go b/cmd/utils.go index 68952b2..873b27e 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -10,9 +10,22 @@ import ( "time" ) +// convertMapStringToMapInterface converts the flat map[string]string produced by a stringToString flag (e.g. +// --autoinstall-meta) into the map[string]interface{} expected by the XML-RPC API. As a fallback for values that +// need to carry a list or dict rather than a flat string (e.g. Agama's agama_modules or cloud-init's per-module +// settings), any value that looks like a JSON array or object (starts with '[' or '{') is decoded as JSON; if it +// fails to parse, the original string is used unchanged so existing flat-string usage is unaffected. func convertMapStringToMapInterface(m map[string]string) map[string]interface{} { result := make(map[string]interface{}) for k, v := range m { + trimmed := strings.TrimSpace(v) + if len(trimmed) > 0 && (trimmed[0] == '[' || trimmed[0] == '{') { + var decoded interface{} + if err := json.Unmarshal([]byte(trimmed), &decoded); err == nil { + result[k] = decoded + continue + } + } result[k] = v } return result diff --git a/cmd/utils_test.go b/cmd/utils_test.go index a82235f..4364acd 100644 --- a/cmd/utils_test.go +++ b/cmd/utils_test.go @@ -6,6 +6,50 @@ import ( "time" ) +func Test_convertMapStringToMapInterface(t *testing.T) { + tests := []struct { + name string + args map[string]string + want map[string]interface{} + }{ + { + name: "flat string value is left unchanged", + args: map[string]string{"foo": "bar"}, + want: map[string]interface{}{"foo": "bar"}, + }, + { + name: "json list value is decoded", + args: map[string]string{"agama_modules": `["root", "user"]`}, + want: map[string]interface{}{"agama_modules": []interface{}{"root", "user"}}, + }, + { + name: "json object value is decoded", + args: map[string]string{"cloud_init_set_hostname": `{"hostname": "example-host"}`}, + want: map[string]interface{}{ + "cloud_init_set_hostname": map[string]interface{}{"hostname": "example-host"}, + }, + }, + { + name: "leading whitespace is tolerated", + args: map[string]string{"agama_modules": ` ["root"]`}, + want: map[string]interface{}{"agama_modules": []interface{}{"root"}}, + }, + { + name: "malformed json-looking value falls back to the raw string", + args: map[string]string{"agama_modules": `[root`}, + want: map[string]interface{}{"agama_modules": `[root`}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := convertMapStringToMapInterface(tt.args) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("convertMapStringToMapInterface() got = %v, want %v", got, tt.want) + } + }) + } +} + func Test_covertFloatToTime(t *testing.T) { type args struct { t float64 From 87aab7e22cd1387e61deb11129b6b98498b14d3b Mon Sep 17 00:00:00 2001 From: Enno Gotthold Date: Tue, 27 Oct 2026 12:33:58 +0100 Subject: [PATCH 4/6] chore(testing): pin dev-stack Cobbler image to v4.0.0b7 Also fix the coverage-upload step's version guard, which had been left pointing at v4.0.0b5 since the previous bump to b6 and so never fired. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/testing.yml | 4 ++-- testing/start.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index b733057..24ff998 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -18,7 +18,7 @@ jobs: strategy: matrix: cobbler_version: - - v4.0.0b6 + - v4.0.0b7 fail-fast: false steps: - name: Check out code into the Go module directory @@ -59,7 +59,7 @@ jobs: *.iso key: ${{ steps.cache-iso-restore.outputs.cache-primary-key }} - name: Upload coverage report to GH artifacts - if: matrix.cobbler_version == 'v4.0.0b5' + if: matrix.cobbler_version == 'v4.0.0b7' # https://github.com/actions/upload-artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: diff --git a/testing/start.sh b/testing/start.sh index 618fe61..679fc83 100755 --- a/testing/start.sh +++ b/testing/start.sh @@ -6,7 +6,7 @@ if [ -z "$1" ] echo "No cobbler server url supplied" fi -cobbler_image_tag=v4.0.0b6 +cobbler_image_tag=v4.0.0b7 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 From ec9128e11d0939a42f8dc9fc809af9940d0adccb Mon Sep 17 00:00:00 2001 From: Enno Gotthold Date: Tue, 27 Oct 2026 12:46:06 +0100 Subject: [PATCH 5/6] fix(docker): use COBBLER_XMLRPC_BIND_ADDRESS_V4 in the dev stack Cobbler 4.0.0b7 split the XML-RPC bind-address env var into separate IPv4/IPv6 variants and dropped the old combined COBBLER_XMLRPC_BIND_ADDRESS name. Without this, cobblerd silently falls back to binding 127.0.0.1 only, which breaks the published 25151 port the dev-stack script waits on. Co-Authored-By: Claude Sonnet 5 --- testing/compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/compose.yml b/testing/compose.yml index c087f4b..72521b2 100644 --- a/testing/compose.yml +++ b/testing/compose.yml @@ -3,7 +3,7 @@ services: image: ghcr.io/cobbler/cobblerd:${COBBLER_IMAGE_TAG:-latest} container_name: cobbler-dev environment: - - COBBLER_XMLRPC_BIND_ADDRESS=0.0.0.0 + - COBBLER_XMLRPC_BIND_ADDRESS_V4=0.0.0.0 volumes: - ../extracted_iso_image:/extracted_iso_image:ro,z ports: From 674997a6f8423d3beebd9b244bacc19d120da622 Mon Sep 17 00:00:00 2001 From: Enno Gotthold Date: Tue, 27 Oct 2026 13:02:55 +0100 Subject: [PATCH 6/6] fix(deps): bump cobblerclient to v1.0.0-rc7 v1.0.0-rc6's Settings.ConvertServerToIp (bool) can't decode Cobbler 4.0.0b7's reshaped convert_server_to_ip ({enabled, protocol}), which made `cobbler setting edit`/`setting report` fail outright. v1.0.0-rc7 fixes the field's type; verified `setting report`/`setting edit` and the full test suite pass against a live v4.0.0b7 server. Note: requires the cobbler/cobblerclient v1.0.0-rc7 tag to be pushed before `go mod tidy`/CI will resolve it. Co-Authored-By: Claude Sonnet 5 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7f7b28d..aa712c6 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ module github.com/cobbler/cli go 1.24 require ( - github.com/cobbler/cobblerclient v1.0.0-rc6 + github.com/cobbler/cobblerclient v1.0.0-rc7 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.19.0 diff --git a/go.sum b/go.sum index 662514d..d3e879e 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/cobbler/cobblerclient v1.0.0-rc6 h1:rLwH0DUB3H0PKcE+wxLyiWz1DWmH7UMMd4LSBqFwhjA= -github.com/cobbler/cobblerclient v1.0.0-rc6/go.mod h1:BoS4GMBvwnypRHcOMedyipm+0Pu7IP5pSX6A/DqU6mo= +github.com/cobbler/cobblerclient v1.0.0-rc7 h1:Qey8hX5GeLNIWWBACPSpE2ZPNYZ5putpzbO6weMQx0M= +github.com/cobbler/cobblerclient v1.0.0-rc7/go.mod h1:BoS4GMBvwnypRHcOMedyipm+0Pu7IP5pSX6A/DqU6mo= 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=