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/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/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 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. 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= diff --git a/testing/compose.yml b/testing/compose.yml index b59f02e..72521b2 100644 --- a/testing/compose.yml +++ b/testing/compose.yml @@ -3,9 +3,11 @@ 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: - 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" 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