diff --git a/.github/workflows/coverage-upload.yml b/.github/workflows/coverage-upload.yml new file mode 100644 index 0000000..0ea36f0 --- /dev/null +++ b/.github/workflows/coverage-upload.yml @@ -0,0 +1,55 @@ +name: Coverage Upload + +on: + workflow_run: + workflows: [testing] + types: + - completed + +jobs: + run_tests: + runs-on: ubuntu-latest + steps: + # https://github.com/actions/github-script + # Based on: https://github.com/orgs/community/discussions/34652 + - name: 'Download artifact' + uses: actions/github-script@v7 + with: + script: | + let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { + return artifact.name == "coverage-report" + })[0]; + let download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + let fs = require('fs'); + fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/coverage-report.zip`, Buffer.from(download.data)); + - name: 'Unzip artifact' + run: unzip coverage-report.zip + # https://github.com/actions/download-artifact + # - name: Download artifact + # id: download-artifact + # uses: actions/download-artifact@v4 + # with: + # run-id: ${{ github.event.workflow_run.id }} + # https://github.com/codacy/codacy-coverage-reporter-action + # - name: Run codacy-coverage-reporter + # uses: codacy/codacy-coverage-reporter-action@v1 + # with: + # project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} + # coverage-reports: coverage.xml + - name: Publish Code Coverage Results + run: | + auth="--project-token ${{ secrets.CODACY_PROJECT_TOKEN }}" + commit_uuid="--commit-uuid ${{ github.event.workflow_run.head_sha }}" + + bash <(curl -Ls https://coverage.codacy.com/get.sh) report $auth $commit_uuid --force-coverage-parser go -r coverage.out --partial &&\ + bash <(curl -Ls https://coverage.codacy.com/get.sh) final $auth $commit_uuid \ No newline at end of file diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 0000000..a3fce4c --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,70 @@ +name: testing + +on: + push: + branches: + - 'main' + tags: + - 'v*' + pull_request: + +permissions: + contents: read + +jobs: + test: + name: Integration Tests (Cobbler ${{ matrix.cobbler_version }}) + runs-on: ubuntu-latest + strategy: + matrix: + cobbler_version: + # - d8f60bbf14a838c8c8a1dba98086b223e35fe70a # 3.3.0 - TypeError during import + - f5b0599acce32de4288c76e4f601aece0c664fed # 3.3.1 + # - 9044aa990a94752fa5bd5a24051adde099280bfa # 3.3.2 - Testing Docker Image broken + # - 5c498dbf2af6e3782b37605a477759e1aacc16b2 # 3.3.3 - Testing Docker Image broken + - 3ed865b79ce69fca7464e0957f4bcadcc9917a9d # 3.3.4 + - 718e3256a5989941e8a678404fdea07364255637 # 3.3.5 + - df356046f3cf27be62a61001b982d5983800cfd9 # 3.3.6 + fail-fast: false + steps: + - name: Check out code into the Go module directory + uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + id: go + - name: Install system dependencies + run: | + sudo apt-get install -y xorriso + - name: Get dependencies + run: | + go mod download + - name: Replace git version hash + run: | + sed -i "s/cobbler_commit=.*/cobbler_commit=${{ matrix.cobbler_version }}/" testing/start.sh + - name: Restore OS ISO + id: cache-iso-restore + uses: actions/cache/restore@v4 + with: + path: | + *.iso + key: ${{ runner.os }}-${{ matrix.cobbler_version }}-iso + - name: Make Test + run: | + make test + - name: Save OS ISO + id: cache-iso-save + uses: actions/cache/save@v4 + with: + path: | + *.iso + key: ${{ steps.cache-iso-restore.outputs.cache-primary-key }} + # https://github.com/actions/upload-artifact + - name: Upload coverage report to GH artifacts + if: matrix.cobbler_version == 'df356046f3cf27be62a61001b982d5983800cfd9' + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage.out + if-no-files-found: error diff --git a/.gitignore b/.gitignore index 654c4c8..96c5ae2 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,9 @@ docs/_build # goreleaser dist/ + +# Tests +testing/cobbler_source/ +extracted_iso_image/ +*.iso +coverage.out diff --git a/Makefile b/Makefile index 75d4845..916f247 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,7 @@ BINARY_NAME=cobbler EXECUTOR?=docker +COBBLER_SERVER_URL=http://localhost:8081/cobbler_api +TEST?=$$(go list ./... |grep -v 'vendor') build: @echo "building package" @@ -28,6 +30,10 @@ run: go build -o ${BINARY_NAME} main.go ./${BINARY_NAME} +test: + @./testing/start.sh ${COBBLER_SERVER_URL} + go test -v -coverprofile="coverage.out" -covermode="atomic" $(TEST) + shell_completions: @mkdir -p config/completions/bash @mkdir -p config/completions/fish diff --git a/cmd/aclsetup.go b/cmd/aclsetup.go index b55ae69..1a89076 100644 --- a/cmd/aclsetup.go +++ b/cmd/aclsetup.go @@ -10,51 +10,52 @@ import ( "github.com/spf13/cobra" ) -// aclsetupCmd represents the aclsetup command -var aclsetupCmd = &cobra.Command{ - Use: "aclsetup", - Short: "Adjust the access control list", - Long: "Configures users/groups to run the Cobbler CLI as non-root.", - Args: cobra.MinimumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - addUserOption, err := cmd.Flags().GetString("adduser") - if err != nil { - return err - } - addGroupOption, err := cmd.Flags().GetString("addgroup") - if err != nil { - return err - } - removeUserOption, err := cmd.Flags().GetString("removeuser") - if err != nil { - return err - } - removeGroupOption, err := cmd.Flags().GetString("removegroup") - if err != nil { - return err - } - aclSetupOptions := cobblerclient.AclSetupOptions{ - AddUser: addUserOption, - AddGroup: addGroupOption, - RemoveUser: removeUserOption, - RemoveGroup: removeGroupOption, - } - eventId, err := Client.BackgroundAclSetup(aclSetupOptions) - if err != nil { - return err - } - fmt.Println("Event ID: ", eventId) - return nil - }, -} - -func init() { - rootCmd.AddCommand(aclsetupCmd) - - //local flags +// NewAclSetupCmd builds a new command that represent the aclsetup action. +func NewAclSetupCmd() *cobra.Command { + aclsetupCmd := &cobra.Command{ + Use: "aclsetup", + Short: "Adjust the access control list", + Long: "Configures users/groups to run the Cobbler CLI as non-root.", + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + addUserOption, err := cmd.Flags().GetString("adduser") + if err != nil { + return err + } + addGroupOption, err := cmd.Flags().GetString("addgroup") + if err != nil { + return err + } + removeUserOption, err := cmd.Flags().GetString("removeuser") + if err != nil { + return err + } + removeGroupOption, err := cmd.Flags().GetString("removegroup") + if err != nil { + return err + } + aclSetupOptions := cobblerclient.AclSetupOptions{ + AddUser: addUserOption, + AddGroup: addGroupOption, + RemoveUser: removeUserOption, + RemoveGroup: removeGroupOption, + } + eventId, err := Client.BackgroundAclSetup(aclSetupOptions) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), "Event ID: ", eventId) + return nil + }, + } aclsetupCmd.Flags().String("adduser", "", "give acls to this user") aclsetupCmd.Flags().String("addgroup", "", "give acls to this group") aclsetupCmd.Flags().String("removeuser", "", "remove acls from this user") aclsetupCmd.Flags().String("removegroup", "", "remove acls from this user") + aclsetupCmd.MarkFlagsMutuallyExclusive("adduser", "addgroup", "removeuser", "removegroup") + aclsetupCmd.MarkFlagsOneRequired("adduser", "addgroup", "removeuser", "removegroup") + return aclsetupCmd } diff --git a/cmd/aclsetup_test.go b/cmd/aclsetup_test.go new file mode 100644 index 0000000..89d06ef --- /dev/null +++ b/cmd/aclsetup_test.go @@ -0,0 +1,76 @@ +package cmd + +import ( + "bytes" + "fmt" + "github.com/cobbler/cobblerclient" + "github.com/spf13/cobra" + "io" + "strings" + "testing" +) + +func Test_AclSetupCmd(t *testing.T) { + type args struct { + command []string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "adduser", + args: args{command: []string{"--config", "../testing/.cobbler.yaml", "aclsetup", "--adduser", "cobbler"}}, + want: "Event ID:", + wantErr: false, + }, + { + name: "addgroup", + args: args{command: []string{"--config", "../testing/.cobbler.yaml", "aclsetup", "--addgroup", "cobbler"}}, + want: "Event ID:", + wantErr: false, + }, + { + name: "removeuser", + args: args{command: []string{"--config", "../testing/.cobbler.yaml", "aclsetup", "--removeuser", "cobbler"}}, + want: "Event ID:", + wantErr: false, + }, + { + name: "removegroup", + args: args{command: []string{"--config", "../testing/.cobbler.yaml", "aclsetup", "--removegroup", "cobbler"}}, + want: "Event ID:", + 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 + 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, tt.want) { + fmt.Println(stdoutString) + t.Fatal("No Event ID present") + } + }) + } +} diff --git a/cmd/buildiso.go b/cmd/buildiso.go index 5ce6054..8a5d924 100644 --- a/cmd/buildiso.go +++ b/cmd/buildiso.go @@ -10,78 +10,77 @@ import ( "github.com/spf13/cobra" ) -// buildisoCmd represents the buildiso command -var buildisoCmd = &cobra.Command{ - Use: "buildiso", - Short: "Build an ISO", - Long: "Build all profiles into a bootable CD image. All flags are optional.", - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - isoOption, err := cmd.Flags().GetString("iso") - if err != nil { - return err - } - distroOption, err := cmd.Flags().GetString("distro") - if err != nil { - return err - } - xorrisofsOption, err := cmd.Flags().GetString("mkisofs-opts") - if err != nil { - return err - } - profilesOption, err := cmd.Flags().GetStringSlice("profiles") - if err != nil { - return err - } - sourceOption, err := cmd.Flags().GetString("source") - if err != nil { - return err - } - systemsOption, err := cmd.Flags().GetStringSlice("systems") - if err != nil { - return err - } - tempdirOption, err := cmd.Flags().GetString("tempdir") - if err != nil { - return err - } - standaloneOption, err := cmd.Flags().GetBool("standalone") - if err != nil { - return err - } - excludeDnsOption, err := cmd.Flags().GetBool("exclude-dns") - if err != nil { - return err - } - airgappedOption, err := cmd.Flags().GetBool("airgapped") - if err != nil { - return err - } - buildisoOptions := cobblerclient.BuildisoOptions{ - Iso: isoOption, - Profiles: profilesOption, - Systems: systemsOption, - BuildisoDir: tempdirOption, - Distro: distroOption, - Standalone: standaloneOption, - Airgapped: airgappedOption, - Source: sourceOption, - ExcludeDns: excludeDnsOption, - XorrisofsOpts: xorrisofsOption, - } - eventId, err := Client.BackgroundBuildiso(buildisoOptions) - if err != nil { - return err - } - fmt.Printf("Event ID: %s\n", eventId) - return nil - }, -} - -func init() { - rootCmd.AddCommand(buildisoCmd) - - //local flags +// NewBuildisoCmd builds a new command that represents the buildiso action +func NewBuildisoCmd() *cobra.Command { + buildisoCmd := &cobra.Command{ + Use: "buildiso", + Short: "Build an ISO", + Long: "Build all profiles into a bootable CD image. All flags are optional.", + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + isoOption, err := cmd.Flags().GetString("iso") + if err != nil { + return err + } + distroOption, err := cmd.Flags().GetString("distro") + if err != nil { + return err + } + xorrisofsOption, err := cmd.Flags().GetString("mkisofs-opts") + if err != nil { + return err + } + profilesOption, err := cmd.Flags().GetStringSlice("profiles") + if err != nil { + return err + } + sourceOption, err := cmd.Flags().GetString("source") + if err != nil { + return err + } + systemsOption, err := cmd.Flags().GetStringSlice("systems") + if err != nil { + return err + } + tempdirOption, err := cmd.Flags().GetString("tempdir") + if err != nil { + return err + } + standaloneOption, err := cmd.Flags().GetBool("standalone") + if err != nil { + return err + } + excludeDnsOption, err := cmd.Flags().GetBool("exclude-dns") + if err != nil { + return err + } + airgappedOption, err := cmd.Flags().GetBool("airgapped") + if err != nil { + return err + } + buildisoOptions := cobblerclient.BuildisoOptions{ + Iso: isoOption, + Profiles: profilesOption, + Systems: systemsOption, + BuildisoDir: tempdirOption, + Distro: distroOption, + Standalone: standaloneOption, + Airgapped: airgappedOption, + Source: sourceOption, + ExcludeDns: excludeDnsOption, + XorrisofsOpts: xorrisofsOption, + } + eventId, err := Client.BackgroundBuildiso(buildisoOptions) + if err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "Event ID: %s\n", eventId) + return nil + }, + } buildisoCmd.Flags().Bool("airgapped", false, "creates a standalone ISO with all distro and repo files for disconnected system installation") buildisoCmd.Flags().String("distro", "", "used with --standalone and --airgapped to create a distro-based ISO including all associated profiles/systems") buildisoCmd.Flags().Bool("exclude-dns", false, "prevents addition of name server addresses to the kernel boot options") @@ -92,4 +91,5 @@ func init() { buildisoCmd.Flags().Bool("standalone", false, "creates a standalone ISO with all required distro files, but without any added repos") buildisoCmd.Flags().StringSlice("systems", []string{}, "use these systems only") buildisoCmd.Flags().String("tempdir", "", "working directory") + return buildisoCmd } diff --git a/cmd/buildiso_test.go b/cmd/buildiso_test.go new file mode 100644 index 0000000..d8d7f3c --- /dev/null +++ b/cmd/buildiso_test.go @@ -0,0 +1,117 @@ +package cmd + +import ( + "bytes" + "fmt" + "github.com/cobbler/cobblerclient" + "github.com/spf13/cobra" + "io" + "strings" + "testing" +) + +func Test_BuildisoStandaloneCmd(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", "buildiso", "--standalone"}}, + want: "Event ID:", + wantErr: false, + }, + { + name: "airgapped", + args: args{command: []string{"--config", "../testing/.cobbler.yaml", "buildiso", "--airgapped"}}, + want: "Event ID:", + 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 + 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, tt.want) { + fmt.Println(stdoutString) + t.Fatal("No Event ID present") + } + }) + } +} + +func Test_BuildisoNetinstallCmd(t *testing.T) { + type args struct { + command []string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "systems", + args: args{command: []string{"--config", "../testing/.cobbler.yaml", "buildiso", "--systems", "test"}}, + want: "Event ID:", + wantErr: false, + }, + { + name: "nodns", + args: args{command: []string{"--config", "../testing/.cobbler.yaml", "buildiso", "--systems", "test", "--exclude-dns"}}, + want: "Event ID:", + 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 + 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, tt.want) { + fmt.Println(stdoutString) + t.Fatal("No Event ID present") + } + }) + } +} diff --git a/cmd/distro.go b/cmd/distro.go index ef1ffa3..edd908d 100644 --- a/cmd/distro.go +++ b/cmd/distro.go @@ -315,253 +315,91 @@ func updateDistroFromFlags(cmd *cobra.Command, distro *cobbler.Distro) error { return err } -// distroCmd represents the distro command -var distroCmd = &cobra.Command{ - Use: "distro", - Short: "Distribution management", - Long: `Let you manage distributions. +// NewDistroCmd builds a new command that represents the distro action +func NewDistroCmd() (*cobra.Command, error) { + distroCmd := &cobra.Command{ + Use: "distro", + Short: "Distribution management", + Long: `Let you manage distributions. See https://cobbler.readthedocs.io/en/latest/cobbler.html#cobbler-distro for more information.`, - Run: func(cmd *cobra.Command, args []string) { - _ = cmd.Help() - }, -} - -var distroAddCmd = &cobra.Command{ - Use: "add", - Short: "add distribution", - Long: `Adds a distribution.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - newDistro := cobbler.NewDistro() - var err error - - // internal fields (ctime, mtime, depth, uid, source-repos, tree-build-time) cannot be modified - newDistro.Name, err = cmd.Flags().GetString("name") - if err != nil { - return err - } - // Update distro in-memory - err = updateDistroFromFlags(cmd, &newDistro) - if err != nil { - return err - } - // Now create the distro via XML-RPC - distro, err := Client.CreateDistro(newDistro) - if err != nil { - return err - } - fmt.Printf("Distro %s created\n", distro.Name) - return nil - }, -} - -var distroCopyCmd = &cobra.Command{ - Use: "copy", - Short: "copy distribution", - Long: `Copies a distribution.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - dname, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - distroNewName, err := cmd.Flags().GetString("newname") - if err != nil { - return err - } - - dhandle, err := Client.GetDistroHandle(dname) - if err != nil { - return err - } - err = Client.CopyDistro(dhandle, distroNewName) - if err != nil { - return err - } - newDistro, err := Client.GetDistro(distroNewName, false, false) - if err != nil { - return err - } - // Update distro in-memory - err = updateDistroFromFlags(cmd, newDistro) - if err != nil { - return err - } - // Update the distro via XML-RPC - return Client.UpdateDistro(newDistro) - }, -} - -var distroEditCmd = &cobra.Command{ - Use: "edit", - Short: "edit distribution", - Long: `Edits a distribution.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - // find distro through its name - dname, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - // Get distro from the API - updateDistro, err := Client.GetDistro(dname, false, false) - if err != nil { - return err - } - // Update distro in-memory - err = updateDistroFromFlags(cmd, updateDistro) - if err != nil { - return err - } - if updateDistro.Meta.IsDirty { - updateDistro, err = Client.GetDistro( - updateDistro.Name, - updateDistro.Meta.IsFlattened, - updateDistro.Meta.IsResolved, - ) - if err != nil { - return err - } - } - // Now update distro via XML-RPC - return Client.UpdateDistro(updateDistro) - }, -} - -var distroFindCmd = &cobra.Command{ - Use: "find", - Short: "find distribution", - Long: `Finds a given distribution.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - return FindItemNames(cmd, args, "distro") - }, -} - -var distroListCmd = &cobra.Command{ - Use: "list", - Short: "list all distributions", - Long: `Lists all available distributions.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - distroNames, err := Client.ListDistroNames() - if err != nil { - return err - } - listItems("distros", distroNames) - return nil - }, -} - -var distroRemoveCmd = &cobra.Command{ - Use: "remove", - Short: "remove distribution", - Long: `Removes a given distribution.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - dname, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - recursiveDelete, err := cmd.Flags().GetBool("recursive") - if err != nil { - return err - } - return Client.DeleteDistroRecursive(dname, recursiveDelete) - }, -} - -var distroRenameCmd = &cobra.Command{ - Use: "rename", - Short: "rename distribution", - Long: `Renames a given distribution.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - // Get the name and newname flags - distroName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - distroNewName, err := cmd.Flags().GetString("newname") - if err != nil { - return err - } - - // Get the distro API handle - distroHandle, err := Client.GetDistroHandle(distroName) - if err != nil { - return err - } - // Perform the rename operation server-side - err = Client.RenameDistro(distroHandle, distroNewName) - if err != nil { - return err - } - // Retrieve the renamed distro from the API - newDistro, err := Client.GetDistro(distroNewName, false, false) - if err != nil { - return err - } - // Now edit the distro in-memory - err = updateDistroFromFlags(cmd, newDistro) - if err != nil { - return err - } - // Now update the distro via XML-RPC - return Client.UpdateDistro(newDistro) - }, -} - -func reportDistros(distroNames []string) error { - for _, itemName := range distroNames { - distro, err := Client.GetDistro(itemName, false, false) - if err != nil { - return err - } - printStructured(distro) - fmt.Println("") + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, + } + distroAddCmd, err := NewDistroAddCmd() + if err != nil { + return nil, err } - return nil -} - -var distroReportCmd = &cobra.Command{ - Use: "report", - Short: "list all distributions in detail", - Long: `Shows detailed information about all distributions.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - name, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - itemNames := make([]string, 0) - if name == "" { - itemNames, err = Client.ListDistroNames() - if err != nil { - return err - } - } else { - itemNames = append(itemNames, name) - } - return reportDistros(itemNames) - }, -} - -func init() { - rootCmd.AddCommand(distroCmd) distroCmd.AddCommand(distroAddCmd) + distroCopyCmd, err := NewDistroCopyCmd() + if err != nil { + return nil, err + } distroCmd.AddCommand(distroCopyCmd) + distroEditCmd, err := NewDistroEditCmd() + if err != nil { + return nil, err + } distroCmd.AddCommand(distroEditCmd) + distroFindCmd, err := NewDistroFindCmd() + if err != nil { + return nil, err + } distroCmd.AddCommand(distroFindCmd) + distroListCmd, err := NewDistroListCmd() + if err != nil { + return nil, err + } distroCmd.AddCommand(distroListCmd) + distroRemoveCmd, err := NewDistroRemoveCmd() + if err != nil { + return nil, err + } distroCmd.AddCommand(distroRemoveCmd) + distroRenameCmd, err := NewDistroRenameCmd() + if err != nil { + return nil, err + } distroCmd.AddCommand(distroRenameCmd) + distroReportCmd, err := NewDistroReportCmd() + if err != nil { + return nil, err + } distroCmd.AddCommand(distroReportCmd) + return distroCmd, nil +} + +func NewDistroAddCmd() (*cobra.Command, error) { + distroAddCmd := &cobra.Command{ + Use: "add", + Short: "add distribution", + Long: `Adds a distribution.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + newDistro := cobbler.NewDistro() - // local flags for distro add + // internal fields (ctime, mtime, depth, uid, source-repos, tree-build-time) cannot be modified + newDistro.Name, err = cmd.Flags().GetString("name") + if err != nil { + return err + } + // Update distro in-memory + err = updateDistroFromFlags(cmd, &newDistro) + if err != nil { + return err + } + // Now create the distro via XML-RPC + distro, err := Client.CreateDistro(newDistro) + if err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "Distro %s created\n", distro.Name) + return nil + }, + } addCommonArgs(distroAddCmd) addStringFlags(distroAddCmd, distroStringFlagMetadata) addStringSliceFlags(distroAddCmd, distroStringSliceFlagMetadata) @@ -569,25 +407,121 @@ func init() { // Required Flags err := distroAddCmd.MarkFlagRequired("name") if err != nil { - panic(err) + return nil, err } + return distroAddCmd, nil +} + +func NewDistroCopyCmd() (*cobra.Command, error) { + distroCopyCmd := &cobra.Command{ + Use: "copy", + Short: "copy distribution", + Long: `Copies a distribution.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + dname, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + distroNewName, err := cmd.Flags().GetString("newname") + if err != nil { + return err + } - // local flags for distro copy + dhandle, err := Client.GetDistroHandle(dname) + if err != nil { + return err + } + err = Client.CopyDistro(dhandle, distroNewName) + if err != nil { + return err + } + newDistro, err := Client.GetDistro(distroNewName, false, false) + if err != nil { + return err + } + // Update distro in-memory + err = updateDistroFromFlags(cmd, newDistro) + if err != nil { + return err + } + // Update the distro via XML-RPC + return Client.UpdateDistro(newDistro) + }, + } addCommonArgs(distroCopyCmd) addStringFlags(distroCopyCmd, distroStringFlagMetadata) addStringSliceFlags(distroCopyCmd, distroStringSliceFlagMetadata) addMapFlags(distroCopyCmd, distroMapFlagMetadata) distroCopyCmd.Flags().String("newname", "", "the new distro name") distroCopyCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") + return distroCopyCmd, nil +} + +func NewDistroEditCmd() (*cobra.Command, error) { + distroEditCmd := &cobra.Command{ + Use: "edit", + Short: "edit distribution", + Long: `Edits a distribution.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - // local flags for distro edit + // find distro through its name + dname, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + // Get distro from the API + updateDistro, err := Client.GetDistro(dname, false, false) + if err != nil { + return err + } + // Update distro in-memory + err = updateDistroFromFlags(cmd, updateDistro) + if err != nil { + return err + } + if updateDistro.Meta.IsDirty { + updateDistro, err = Client.GetDistro( + updateDistro.Name, + updateDistro.Meta.IsFlattened, + updateDistro.Meta.IsResolved, + ) + if err != nil { + return err + } + } + // Now update distro via XML-RPC + return Client.UpdateDistro(updateDistro) + }, + } addCommonArgs(distroEditCmd) addStringFlags(distroEditCmd, distroStringFlagMetadata) addStringSliceFlags(distroEditCmd, distroStringSliceFlagMetadata) addMapFlags(distroEditCmd, distroMapFlagMetadata) distroEditCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") + return distroEditCmd, nil +} - // local flags for distro find +func NewDistroFindCmd() (*cobra.Command, error) { + distroFindCmd := &cobra.Command{ + Use: "find", + Short: "find distribution", + Long: `Finds a given distribution.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + return FindItemNames(cmd, args, "distro") + }, + } addCommonArgs(distroFindCmd) addStringFlags(distroFindCmd, distroStringFlagMetadata) addStringSliceFlags(distroFindCmd, distroStringSliceFlagMetadata) @@ -597,19 +531,150 @@ func init() { addFloatFlags(distroFindCmd, findFloatFlagMetadata) distroFindCmd.Flags().String("source-repos", "", "source repositories") distroFindCmd.Flags().String("tree-build-time", "", "tree build time") + return distroFindCmd, nil +} + +func NewDistroListCmd() (*cobra.Command, error) { + distroListCmd := &cobra.Command{ + Use: "list", + Short: "list all distributions", + Long: `Lists all available distributions.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + distroNames, err := Client.ListDistroNames() + if err != nil { + return err + } + listItems(cmd, "distros", distroNames) + return nil + }, + } + return distroListCmd, nil +} + +func NewDistroRemoveCmd() (*cobra.Command, error) { + distroRemoveCmd := &cobra.Command{ + Use: "remove", + Short: "remove distribution", + Long: `Removes a given distribution.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - // local flags for distro remove + dname, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + recursiveDelete, err := cmd.Flags().GetBool("recursive") + if err != nil { + return err + } + return Client.DeleteDistroRecursive(dname, recursiveDelete) + }, + } distroRemoveCmd.Flags().String("name", "", "the distro name") distroRemoveCmd.Flags().Bool("recursive", false, "also delete child objects") + return distroRemoveCmd, nil +} + +func NewDistroRenameCmd() (*cobra.Command, error) { + distroRenameCmd := &cobra.Command{ + Use: "rename", + Short: "rename distribution", + Long: `Renames a given distribution.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + // Get the name and newname flags + distroName, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + distroNewName, err := cmd.Flags().GetString("newname") + if err != nil { + return err + } - // local flags for distro rename + // Get the distro API handle + distroHandle, err := Client.GetDistroHandle(distroName) + if err != nil { + return err + } + // Perform the rename operation server-side + err = Client.RenameDistro(distroHandle, distroNewName) + if err != nil { + return err + } + // Retrieve the renamed distro from the API + newDistro, err := Client.GetDistro(distroNewName, false, false) + if err != nil { + return err + } + // Now edit the distro in-memory + err = updateDistroFromFlags(cmd, newDistro) + if err != nil { + return err + } + // Now update the distro via XML-RPC + return Client.UpdateDistro(newDistro) + }, + } addCommonArgs(distroRenameCmd) addStringFlags(distroRenameCmd, distroStringFlagMetadata) addStringSliceFlags(distroRenameCmd, distroStringSliceFlagMetadata) addMapFlags(distroRenameCmd, distroMapFlagMetadata) distroRenameCmd.Flags().String("newname", "", "the new distro name") distroRenameCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") + return distroRenameCmd, nil +} - // local flags for distro report +func reportDistros(cmd *cobra.Command, distroNames []string) error { + for _, itemName := range distroNames { + distro, err := Client.GetDistro(itemName, false, false) + if err != nil { + return err + } + printStructured(cmd, distro) + fmt.Fprintln(cmd.OutOrStdout(), "") + } + return nil +} + +func NewDistroReportCmd() (*cobra.Command, error) { + distroReportCmd := &cobra.Command{ + Use: "report", + Short: "list all distributions in detail", + Long: `Shows detailed information about all distributions.`, + 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.ListDistroNames() + if err != nil { + return err + } + } else { + itemNames = append(itemNames, name) + } + return reportDistros(cmd, itemNames) + }, + } distroReportCmd.Flags().String("name", "", "the distro name") + return distroReportCmd, nil } diff --git a/cmd/distro_test.go b/cmd/distro_test.go new file mode 100644 index 0000000..6ec50ea --- /dev/null +++ b/cmd/distro_test.go @@ -0,0 +1,456 @@ +package cmd + +import ( + "bytes" + "fmt" + cobbler "github.com/cobbler/cobblerclient" + "github.com/spf13/cobra" + "io" + "strings" + "testing" +) + +func createDistro(client cobbler.Client, name string) (*cobbler.Distro, error) { + distro := cobbler.NewDistro() + distro.Name = name + distro.Kernel = "/extracted_iso_image/install/vmlinuz" + distro.Initrd = "/extracted_iso_image/install/initrd.gz" + return client.CreateDistro(distro) +} + +func removeDistro(client cobbler.Client, name string) error { + return client.DeleteDistro(name) +} + +func Test_DistroAddCmd(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", "distro", "add", "--name", "test-plain", "--kernel", "/extracted_iso_image/install/vmlinuz", "--initrd", "/extracted_iso_image/install/initrd.gz"}}, + want: "Distro 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 := removeDistro(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_DistroCopyCmd(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", "distro", "copy", "--name", "distro-to-copy", "--newname", "copied-distro"}}, + 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 := removeDistro(Client, tt.args.command[5]) + cobbler.FailOnError(t, cleanupErr) + cleanupErr = removeDistro(Client, tt.args.command[7]) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createDistro(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.GetDistro(tt.args.command[7], false, false) + cobbler.FailOnError(t, err) + }) + } +} + +func Test_DistroEditCmd(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", "distro", "edit", "--name", "test-distro-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 := removeDistro(Client, tt.args.command[5]) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createDistro(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) + updatedDistro, err := Client.GetDistro(tt.args.command[5], false, false) + cobbler.FailOnError(t, err) + if updatedDistro.Comment != "testcomment" { + t.Fatal("distro update wasn't successful") + } + }) + } +} + +func Test_DistroFindCmd(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", "distro", "find", "--name", "test-distro-find"}}, + want: "Event ID:", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Cleanup + distroName := "test-distro-find" + var err error + defer func() { + // Client is initialized since this is the cleanup + cleanupErr := removeDistro(Client, distroName) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createDistro(Client, distroName) + 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, distroName) { + fmt.Println(stdoutString) + t.Fatal("distro not successfully found") + } + }) + } +} + +func Test_DistroListCmd(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", "distro", "list"}}, + want: "distros:", + 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("distro list marker not located in output") + } + }) + } +} + +func Test_DistroRemoveCmd(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", "distro", "remove", "--name", "test-distro-remove"}}, + want: "Event ID:", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Arrange + setupClient(t) + _, err := createDistro(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("distro", 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("distro not successfully removed") + } + }) + } +} + +func Test_DistroRenameCmd(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", "distro", "rename", "--name", "test-distro-rename", "--newname", "test-distro-renamed"}}, + want: "Event ID:", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Cleanup + distroName := "test-distro-rename" + newDistroName := "test-distro-renamed" + var err error + defer func() { + // Client is initialized since this is the cleanup + cleanupErr := removeDistro(Client, newDistroName) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createDistro(Client, distroName) + 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("distro", distroName) + cobbler.FailOnError(t, err) + if resultOldName { + t.Fatal("distro not successfully renamed (old name present)") + } + resultNewName, err := Client.HasItem("distro", newDistroName) + cobbler.FailOnError(t, err) + if !resultNewName { + t.Fatal("distro not successfully renamed (new name not present)") + } + }) + } +} + +func Test_DistroReportCmd(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", "distro", "report", "--name", "test-distro-report"}}, + want: ": test-distro-report", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Cleanup + distroName := "test-distro-report" + var err error + defer func() { + // Client is initialized since this is the cleanup + cleanupErr := removeDistro(Client, distroName) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createDistro(Client, distroName) + 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/event.go b/cmd/event.go index 8a4882a..2d9741b 100644 --- a/cmd/event.go +++ b/cmd/event.go @@ -10,108 +10,118 @@ import ( "time" ) -var eventCmd = &cobra.Command{ - Use: "event", - Short: "Show and query events for their status.", - Run: func(cmd *cobra.Command, args []string) { - _ = cmd.Help() - }, +func NewEventCmd() *cobra.Command { + eventCmd := &cobra.Command{ + Use: "event", + Short: "Show and query events for their status.", + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, + } + eventCmd.AddCommand(NewEventStatusCmd()) + eventCmd.AddCommand(NewEventListCmd()) + eventCmd.AddCommand(NewEventLogCmd()) + return eventCmd } -var eventStatusCmd = &cobra.Command{ - Use: "status", - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() +func NewEventStatusCmd() *cobra.Command { + eventStatusCmd := &cobra.Command{ + Use: "status", + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - eventId, err := cmd.Flags().GetString("event-id") - if err != nil { - return err - } + eventId, err := cmd.Flags().GetString("event-id") + if err != nil { + return err + } - event, err := Client.GetTaskStatus(eventId) - if err != nil { - return err - } - fmt.Println(event.State) - return nil - }, + event, err := Client.GetTaskStatus(eventId) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), event.State) + return nil + }, + } + eventStatusCmd.Flags().String("event-id", "", "the event ID of the background task") + _ = eventStatusCmd.MarkFlagRequired("event-id") + return eventStatusCmd } -var eventListCmd = &cobra.Command{ - Use: "list", - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() +func NewEventListCmd() *cobra.Command { + eventListCmd := &cobra.Command{ + Use: "list", + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - user, err := cmd.Flags().GetString("user") - if err != nil { - return err - } + user, err := cmd.Flags().GetString("user") + if err != nil { + return err + } - events, err := Client.GetEvents(user) - if err != nil { - return err - } - idWidth := 0 - stateWidth := 10 - stateTimeWidth := 24 // Fixed width - nameWidth := 0 - for _, event := range events { - if len(event.ID) > idWidth { - idWidth = len(event.ID) + events, err := Client.GetEvents(user) + if err != nil { + return err } - if len(event.Name) > nameWidth { - nameWidth = len(event.Name) + idWidth := 0 + stateWidth := 10 + stateTimeWidth := 24 // Fixed width + nameWidth := 0 + for _, event := range events { + if len(event.ID) > idWidth { + idWidth = len(event.ID) + } + if len(event.Name) > nameWidth { + nameWidth = len(event.Name) + } + if len(event.State) > stateWidth { + stateWidth = len(event.State) + } } - if len(event.State) > stateWidth { - stateWidth = len(event.State) + fmt.Fprintf(cmd.OutOrStdout(), "%*s | %*s | %*s | %*s | %s \n", idWidth, "ID", nameWidth, "Name", stateWidth, "Task State", stateTimeWidth, "Time (last transitioned)", "Read by Who") + for _, event := range events { + stateTimeStruct, err := covertFloatToUtcTime(event.StateTime) + if err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "%*s | %*s | %*s | %*s | %s \n", idWidth, event.ID, nameWidth, event.Name, stateWidth, event.State, stateTimeWidth, stateTimeStruct.Format(time.DateTime), event.ReadByWho) } - } - fmt.Printf("%*s | %*s | %*s | %*s | %s \n", idWidth, "ID", nameWidth, "Name", stateWidth, "Task State", stateTimeWidth, "Time (last transitioned)", "Read by Who") - for _, event := range events { - stateTimeStruct, err := covertFloatToUtcTime(event.StateTime) + return nil + }, + } + eventListCmd.Flags().String("user", "", "giving this parameter will show only events the user hasn't seen yet") + return eventListCmd +} + +func NewEventLogCmd() *cobra.Command { + eventLogCmd := &cobra.Command{ + Use: "log", + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() if err != nil { return err } - fmt.Printf("%*s | %*s | %*s | %*s | %s \n", idWidth, event.ID, nameWidth, event.Name, stateWidth, event.State, stateTimeWidth, stateTimeStruct.Format(time.DateTime), event.ReadByWho) - } - return nil - }, -} - -var eventLogCmd = &cobra.Command{ - Use: "log", - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - eventId, err := cmd.Flags().GetString("event-id") - if err != nil { - return err - } - - eventLog, err := Client.GetEventLog(eventId) - if err != nil { - return err - } - fmt.Println(eventLog) - return nil - }, -} - -func init() { - rootCmd.AddCommand(eventCmd) - - eventCmd.AddCommand(eventStatusCmd) - eventCmd.AddCommand(eventListCmd) - eventCmd.AddCommand(eventLogCmd) - - // local flags for status - eventStatusCmd.Flags().String("event-id", "", "the event ID of the background task") - _ = eventStatusCmd.MarkFlagRequired("event-id") - // local flags for list - eventListCmd.Flags().String("user", "", "giving this parameter will show only events the user hasn't seen yet") + eventId, err := cmd.Flags().GetString("event-id") + if err != nil { + return err + } - // local flags for log + eventLog, err := Client.GetEventLog(eventId) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), eventLog) + return nil + }, + } eventLogCmd.Flags().String("event-id", "", "the event ID of the background task") - _ = eventStatusCmd.MarkFlagRequired("event-id") + _ = eventLogCmd.MarkFlagRequired("event-id") + return eventLogCmd } diff --git a/cmd/event_test.go b/cmd/event_test.go new file mode 100644 index 0000000..12bb1ab --- /dev/null +++ b/cmd/event_test.go @@ -0,0 +1,87 @@ +package cmd + +import ( + "bytes" + "fmt" + "github.com/cobbler/cobblerclient" + "github.com/spf13/cobra" + "io" + "strings" + "testing" +) + +func Test_EventStatusCmd(t *testing.T) { + // Arrange + cobra.OnInitialize(initConfig, setupLogger) + rootCmd := NewRootCmd() + rootCmd.SetArgs([]string{"--config", "../testing/.cobbler.yaml", "event", "status", "--event-id", "garbage"}) + stdout := bytes.NewBufferString("") + stderr := bytes.NewBufferString("") + rootCmd.SetOut(stdout) + rootCmd.SetErr(stderr) + + // Act + err := rootCmd.Execute() + + // Assert + if err == nil { + t.Fatal("expected error, got none") + } + if !strings.Contains(err.Error(), "no event with that id") { + t.Fatal("server didn't complain about garbage event id") + } +} + +func Test_EventListCmd(t *testing.T) { + // Arrange + cobra.OnInitialize(initConfig, setupLogger) + rootCmd := NewRootCmd() + rootCmd.SetArgs([]string{"--config", "../testing/.cobbler.yaml", "event", "list"}) + 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, "Time (last transitioned)") { + fmt.Println(stdoutString) + t.Fatal("no table header with time present") + } +} + +func Test_EventLogCmd(t *testing.T) { + // Arrange + cobra.OnInitialize(initConfig, setupLogger) + rootCmd := NewRootCmd() + rootCmd.SetArgs([]string{"--config", "../testing/.cobbler.yaml", "event", "log", "--event-id", "garbage"}) + 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, "?") { + fmt.Println(stdoutString) + t.Fatal("server didn't complain about garbage event id") + } +} diff --git a/cmd/file.go b/cmd/file.go index b379a63..06e090f 100644 --- a/cmd/file.go +++ b/cmd/file.go @@ -83,17 +83,12 @@ func updateFileFromFlags(cmd *cobra.Command, file *cobbler.File) error { } file.Group = fileNewGroup case "owner": - if cmd.Flags().Lookup("owners-inherit").Changed { - file.Owners.IsInherited, err = cmd.Flags().GetBool("owners-inherit") - if err != nil { - return - } - } else { - file.Owners.Data, err = cmd.Flags().GetStringSlice("owners") - if err != nil { - return - } + 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") @@ -107,268 +102,306 @@ func updateFileFromFlags(cmd *cobra.Command, file *cobbler.File) error { return err } -// fileCmd represents the file command -var fileCmd = &cobra.Command{ - Use: "file", - Short: "File management", - Long: `Let you manage files. +// NewFileCmd builds a new command that represents the file action +func NewFileCmd() *cobra.Command { + 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() - }, -} - -var fileAddCmd = &cobra.Command{ - Use: "add", - Short: "add file", - Long: `Adds a file.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - newFile := cobbler.NewFile() - var err error - - // 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.Printf("File %s created\n", file.Name) - return nil - }, -} - -var fileCopyCmd = &cobra.Command{ - Use: "copy", - Short: "copy file", - Long: `Copies a file.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - // 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) - }, -} - -var fileEditCmd = &cobra.Command{ - Use: "edit", - Short: "edit file", - Long: `Edits a file.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - // 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) - }, -} - -var fileFindCmd = &cobra.Command{ - Use: "find", - Short: "find file", - Long: `Finds a given file.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - return FindItemNames(cmd, args, "file") - }, -} - -var fileListCmd = &cobra.Command{ - Use: "list", - Short: "list all files", - Long: `Lists all available files.`, - Run: func(cmd *cobra.Command, args []string) { - generateCobblerClient() - fileNames, err := Client.ListFileNames() - if err != nil { - fmt.Println(err) - } - listItems("files", fileNames) - }, -} - -var fileRemoveCmd = &cobra.Command{ - Use: "remove", - Short: "remove file", - Long: `Removes a given file.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - return RemoveItemRecursive(cmd, args, "file") - }, -} - -var fileRenameCmd = &cobra.Command{ - Use: "rename", - Short: "rename file", - Long: `Renames a given file.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - // 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) - }, -} - -func reportFiles(fileNames []string) error { - for _, itemName := range fileNames { - file, err := Client.GetFile(itemName, false, false) - if err != nil { - return err - } - printStructured(file) - fmt.Println("") + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, } - return nil + fileCmd.AddCommand(NewFileAddCmd()) + fileCmd.AddCommand(NewFileCopyCmd()) + fileCmd.AddCommand(NewFileEditCmd()) + fileCmd.AddCommand(NewFileFindCmd()) + fileCmd.AddCommand(NewFileListCmd()) + fileCmd.AddCommand(NewFileRemoveCmd()) + fileCmd.AddCommand(NewFileRenameCmd()) + fileCmd.AddCommand(NewFileReportCmd()) + return fileCmd } -var 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 { - generateCobblerClient() - name, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - itemNames := make([]string, 0) - if name == "" { - itemNames, err = Client.ListFileNames() +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 } - } else { - itemNames = append(itemNames, name) - } - return reportFiles(itemNames) - }, -} -func init() { - rootCmd.AddCommand(fileCmd) - fileCmd.AddCommand(fileAddCmd) - fileCmd.AddCommand(fileCopyCmd) - fileCmd.AddCommand(fileEditCmd) - fileCmd.AddCommand(fileFindCmd) - fileCmd.AddCommand(fileListCmd) - fileCmd.AddCommand(fileRemoveCmd) - fileCmd.AddCommand(fileRenameCmd) - fileCmd.AddCommand(fileReportCmd) + newFile := cobbler.NewFile() - // local flags for file add + // 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 + } - // local flags for file copy + // 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 +} - // local flags for file edit +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 +} - // local flags for file find +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 + } - // local flags for file remove + 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 + } - // local flags for file rename + // 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 +} - // local flags for file report +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 } diff --git a/cmd/file_test.go b/cmd/file_test.go new file mode 100644 index 0000000..c35b3c3 --- /dev/null +++ b/cmd/file_test.go @@ -0,0 +1,459 @@ +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/hardlink.go b/cmd/hardlink.go index f84d447..f5a5a0c 100644 --- a/cmd/hardlink.go +++ b/cmd/hardlink.go @@ -9,22 +9,25 @@ import ( "github.com/spf13/cobra" ) -// hardlinkCmd represents the hardlink command -var hardlinkCmd = &cobra.Command{ - Use: "hardlink", - Short: "Hardlink files", - Long: "Hardlink all files where it is possible to improve performance.", - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - eventId, err := Client.BackgroundHardlink() - if err != nil { - return err - } - fmt.Printf("Event ID: %s\n", eventId) - return nil - }, -} +// NewHardlinkCmd builds a new commdand that represents the hardlink action +func NewHardlinkCmd() *cobra.Command { + hardlinkCmd := &cobra.Command{ + Use: "hardlink", + Short: "Hardlink files", + Long: "Hardlink all files where it is possible to improve performance.", + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } -func init() { - rootCmd.AddCommand(hardlinkCmd) + eventId, err := Client.BackgroundHardlink() + if err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "Event ID: %s\n", eventId) + return nil + }, + } + return hardlinkCmd } diff --git a/cmd/hardlink_test.go b/cmd/hardlink_test.go new file mode 100644 index 0000000..5c491ca --- /dev/null +++ b/cmd/hardlink_test.go @@ -0,0 +1,38 @@ +package cmd + +import ( + "bytes" + "fmt" + "github.com/cobbler/cobblerclient" + "github.com/spf13/cobra" + "io" + "strings" + "testing" +) + +func Test_HardlinkCmd(t *testing.T) { + // Arrange + cobra.OnInitialize(initConfig, setupLogger) + rootCmd := NewRootCmd() + rootCmd.SetArgs([]string{"--config", "../testing/.cobbler.yaml", "hardlink"}) + 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, "Event ID:") { + fmt.Println(stdoutString) + t.Fatal("No Event ID present") + } +} diff --git a/cmd/image.go b/cmd/image.go index 0efeda4..4d501bc 100644 --- a/cmd/image.go +++ b/cmd/image.go @@ -347,224 +347,58 @@ func updateImageFromFlags(cmd *cobra.Command, image *cobbler.Image) error { return err } -// imageCmd represents the image command -var imageCmd = &cobra.Command{ - Use: "image", - Short: "Image management", - Long: `Let you manage images. +// NewImageCmd builds a new command that represents the image action +func NewImageCmd() *cobra.Command { + imageCmd := &cobra.Command{ + Use: "image", + Short: "Image management", + Long: `Let you manage images. See https://cobbler.readthedocs.io/en/latest/cobbler.html#cobbler-image for more information.`, - Run: func(cmd *cobra.Command, args []string) { - _ = cmd.Help() - }, -} - -var imageAddCmd = &cobra.Command{ - Use: "add", - Short: "add image", - Long: `Adds a image.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - newImage := cobbler.NewImage() - var err error - newImage.Name, err = cmd.Flags().GetString("name") - if err != nil { - return err - } - // Update image in-memory - err = updateImageFromFlags(cmd, &newImage) - if err != nil { - return err - } - // Now create the image via XML-RPC - system, err := Client.CreateImage(newImage) - if err != nil { - return err - } - fmt.Printf("System %s created\n", system.Name) - return nil - }, -} - -var imageCopyCmd = &cobra.Command{ - Use: "copy", - Short: "copy image", - Long: `Copies a image.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - imageName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - imageNewName, err := cmd.Flags().GetString("newname") - if err != nil { - return err - } - - imageHandle, err := Client.GetImageHandle(imageName) - if err != nil { - return err - } - err = Client.CopyImage(imageHandle, imageNewName) - if err != nil { - return err - } - copiedImage, err := Client.GetImage(imageNewName, false, false) - if err != nil { - return err - } - // Update image in-memory - err = updateImageFromFlags(cmd, copiedImage) - if err != nil { - return err - } - return Client.UpdateImage(copiedImage) - }, -} - -var imageEditCmd = &cobra.Command{ - Use: "edit", - Short: "edit image", - Long: `Edits a image.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - imageName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - - imageToEdit, err := Client.GetImage(imageName, false, false) - if err != nil { - return err - } - // Update image in-memory - err = updateImageFromFlags(cmd, imageToEdit) - if err != nil { - return err - } - return Client.UpdateImage(imageToEdit) - }, -} - -var imageFindCmd = &cobra.Command{ - Use: "find", - Short: "find image", - Long: `Finds a given image.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - return FindItemNames(cmd, args, "image") - }, -} - -var imageListCmd = &cobra.Command{ - Use: "list", - Short: "list all images", - Long: `Lists all available images.`, - Run: func(cmd *cobra.Command, args []string) { - generateCobblerClient() - imageNames, err := Client.ListImageNames() - if err != nil { - fmt.Println(err) - } - listItems("images", imageNames) - }, -} - -var imageRemoveCmd = &cobra.Command{ - Use: "remove", - Short: "remove image", - Long: `Removes a given image.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - return RemoveItemRecursive(cmd, args, "image") - }, -} - -var imageRenameCmd = &cobra.Command{ - Use: "rename", - Short: "rename image", - Long: `Renames a given image.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - imageName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - imageNewName, err := cmd.Flags().GetString("newname") - if err != nil { - return err - } - - imageHandle, err := Client.GetImageHandle(imageName) - if err != nil { - return err - } - err = Client.RenameImage(imageHandle, imageNewName) - if err != nil { - return err - } - renamedImage, err := Client.GetImage(imageNewName, false, false) - if err != nil { - return err - } - // Update image in-memory - err = updateImageFromFlags(cmd, renamedImage) - if err != nil { - return err - } - return Client.UpdateImage(renamedImage) - }, -} - -func reportImages(imageNames []string) error { - for _, itemName := range imageNames { - system, err := Client.GetImage(itemName, false, false) - if err != nil { - return err - } - printStructured(system) - fmt.Println("") + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, } - return nil + imageCmd.AddCommand(NewImageAddCmd()) + imageCmd.AddCommand(NewImageCopyCmd()) + imageCmd.AddCommand(NewImageEditCmd()) + imageCmd.AddCommand(NewImageFindCmd()) + imageCmd.AddCommand(NewImageListCmd()) + imageCmd.AddCommand(NewImageRemoveCmd()) + imageCmd.AddCommand(NewImageRenameCmd()) + imageCmd.AddCommand(NewImageReportCmd()) + return imageCmd } -var imageReportCmd = &cobra.Command{ - Use: "report", - Short: "list all images in detail", - Long: `Shows detailed information about all images.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - name, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - itemNames := make([]string, 0) - if name == "" { - itemNames, err = Client.ListImageNames() +func NewImageAddCmd() *cobra.Command { + imageAddCmd := &cobra.Command{ + Use: "add", + Short: "add image", + Long: `Adds a image.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() if err != nil { return err } - } else { - itemNames = append(itemNames, name) - } - return reportImages(itemNames) - }, -} -func init() { - rootCmd.AddCommand(imageCmd) - imageCmd.AddCommand(imageAddCmd) - imageCmd.AddCommand(imageCopyCmd) - imageCmd.AddCommand(imageEditCmd) - imageCmd.AddCommand(imageFindCmd) - imageCmd.AddCommand(imageListCmd) - imageCmd.AddCommand(imageRemoveCmd) - imageCmd.AddCommand(imageRenameCmd) - imageCmd.AddCommand(imageReportCmd) - - // local flags for image add + newImage := cobbler.NewImage() + newImage.Name, err = cmd.Flags().GetString("name") + if err != nil { + return err + } + // Update image in-memory + err = updateImageFromFlags(cmd, &newImage) + if err != nil { + return err + } + // Now create the image via XML-RPC + system, err := Client.CreateImage(newImage) + if err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "Image %s created\n", system.Name) + return nil + }, + } addCommonArgs(imageAddCmd) addStringFlags(imageAddCmd, imageStringFlagMetadata) addIntFlags(imageAddCmd, imageIntFlagMetadata) @@ -572,8 +406,49 @@ func init() { addBoolFlags(imageAddCmd, imageBoolFlagMetadata) addStringSliceFlags(imageAddCmd, imageStringSliceFlagMetadata) imageAddCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") + return imageAddCmd +} + +func NewImageCopyCmd() *cobra.Command { + imageCopyCmd := &cobra.Command{ + Use: "copy", + Short: "copy image", + Long: `Copies a image.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - // local flags for image copy + imageName, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + imageNewName, err := cmd.Flags().GetString("newname") + if err != nil { + return err + } + + imageHandle, err := Client.GetImageHandle(imageName) + if err != nil { + return err + } + err = Client.CopyImage(imageHandle, imageNewName) + if err != nil { + return err + } + copiedImage, err := Client.GetImage(imageNewName, false, false) + if err != nil { + return err + } + // Update image in-memory + err = updateImageFromFlags(cmd, copiedImage) + if err != nil { + return err + } + return Client.UpdateImage(copiedImage) + }, + } addCommonArgs(imageCopyCmd) addStringFlags(imageCopyCmd, imageStringFlagMetadata) addIntFlags(imageCopyCmd, imageIntFlagMetadata) @@ -582,8 +457,37 @@ func init() { addStringSliceFlags(imageCopyCmd, imageStringSliceFlagMetadata) imageCopyCmd.Flags().String("newname", "", "the new image name") imageCopyCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") + return imageCopyCmd +} + +func NewImageEditCmd() *cobra.Command { + imageEditCmd := &cobra.Command{ + Use: "edit", + Short: "edit image", + Long: `Edits a image.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + imageName, err := cmd.Flags().GetString("name") + if err != nil { + return err + } - // local flags for image edit + imageToEdit, err := Client.GetImage(imageName, false, false) + if err != nil { + return err + } + // Update image in-memory + err = updateImageFromFlags(cmd, imageToEdit) + if err != nil { + return err + } + return Client.UpdateImage(imageToEdit) + }, + } addCommonArgs(imageEditCmd) addStringFlags(imageEditCmd, imageStringFlagMetadata) addIntFlags(imageEditCmd, imageIntFlagMetadata) @@ -591,8 +495,22 @@ func init() { addBoolFlags(imageEditCmd, imageBoolFlagMetadata) addStringSliceFlags(imageEditCmd, imageStringSliceFlagMetadata) imageEditCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") + return imageEditCmd +} - // local flags for image find +func NewImageFindCmd() *cobra.Command { + imageFindCmd := &cobra.Command{ + Use: "find", + Short: "find image", + Long: `Finds a given image.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + return FindItemNames(cmd, args, "image") + }, + } addCommonArgs(imageFindCmd) addStringFlags(imageFindCmd, imageStringFlagMetadata) addIntFlags(imageFindCmd, imageIntFlagMetadata) @@ -602,12 +520,90 @@ func init() { addStringFlags(imageFindCmd, findStringFlagMetadata) addIntFlags(imageFindCmd, findIntFlagMetadata) addFloatFlags(imageFindCmd, findFloatFlagMetadata) + return imageFindCmd +} + +func NewImageListCmd() *cobra.Command { + imageListCmd := &cobra.Command{ + Use: "list", + Short: "list all images", + Long: `Lists all available images.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + imageNames, err := Client.ListImageNames() + if err != nil { + return err + } + listItems(cmd, "images", imageNames) + return nil + }, + } + return imageListCmd +} - // local flags for image remove +func NewImageRemoveCmd() *cobra.Command { + imageRemoveCmd := &cobra.Command{ + Use: "remove", + Short: "remove image", + Long: `Removes a given image.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + return RemoveItemRecursive(cmd, args, "image") + }, + } imageRemoveCmd.Flags().String("name", "", "the image name") imageRemoveCmd.Flags().Bool("recursive", false, "also delete child objects") + return imageRemoveCmd +} + +func NewImageRenameCmd() *cobra.Command { + imageRenameCmd := &cobra.Command{ + Use: "rename", + Short: "rename image", + Long: `Renames a given image.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + imageName, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + imageNewName, err := cmd.Flags().GetString("newname") + if err != nil { + return err + } - // local flags for image rename + imageHandle, err := Client.GetImageHandle(imageName) + if err != nil { + return err + } + err = Client.RenameImage(imageHandle, imageNewName) + if err != nil { + return err + } + renamedImage, err := Client.GetImage(imageNewName, false, false) + if err != nil { + return err + } + // Update image in-memory + err = updateImageFromFlags(cmd, renamedImage) + if err != nil { + return err + } + return Client.UpdateImage(renamedImage) + }, + } addCommonArgs(imageRenameCmd) addStringFlags(imageRenameCmd, imageStringFlagMetadata) addIntFlags(imageRenameCmd, imageIntFlagMetadata) @@ -616,7 +612,48 @@ func init() { addStringSliceFlags(imageRenameCmd, imageStringSliceFlagMetadata) imageRenameCmd.Flags().String("newname", "", "the new image name") imageRenameCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") + return imageRenameCmd +} + +func reportImages(cmd *cobra.Command, imageNames []string) error { + for _, itemName := range imageNames { + system, err := Client.GetImage(itemName, false, false) + if err != nil { + return err + } + printStructured(cmd, system) + fmt.Fprintln(cmd.OutOrStdout(), "") + } + return nil +} - // local flags for image report +func NewImageReportCmd() *cobra.Command { + imageReportCmd := &cobra.Command{ + Use: "report", + Short: "list all images in detail", + Long: `Shows detailed information about all images.`, + 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.ListImageNames() + if err != nil { + return err + } + } else { + itemNames = append(itemNames, name) + } + return reportImages(cmd, itemNames) + }, + } imageReportCmd.Flags().String("name", "", "the image name") + return imageReportCmd } diff --git a/cmd/image_test.go b/cmd/image_test.go new file mode 100644 index 0000000..5959231 --- /dev/null +++ b/cmd/image_test.go @@ -0,0 +1,454 @@ +package cmd + +import ( + "bytes" + "fmt" + cobbler "github.com/cobbler/cobblerclient" + "github.com/spf13/cobra" + "io" + "strings" + "testing" +) + +func createImage(client cobbler.Client, name string) (*cobbler.Image, error) { + image := cobbler.NewImage() + image.Name = name + return client.CreateImage(image) +} + +func removeImage(client cobbler.Client, name string) error { + return client.DeleteImage(name) +} + +func Test_ImageAddCmd(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", "image", "add", "--name", "test-plain"}}, + want: "Image 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 := removeImage(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_ImageCopyCmd(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", "image", "copy", "--name", "image-to-copy", "--newname", "copied-image"}}, + 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 := removeImage(Client, tt.args.command[5]) + cobbler.FailOnError(t, cleanupErr) + cleanupErr = removeImage(Client, tt.args.command[7]) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createImage(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.GetImage(tt.args.command[7], false, false) + cobbler.FailOnError(t, err) + }) + } +} + +func Test_ImageEditCmd(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", "image", "edit", "--name", "test-image-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 := removeImage(Client, tt.args.command[5]) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createImage(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) + updatedImage, err := Client.GetImage(tt.args.command[5], false, false) + cobbler.FailOnError(t, err) + if updatedImage.Comment != "testcomment" { + t.Fatal("image update wasn't successful") + } + }) + } +} + +func Test_ImageFindCmd(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", "image", "find", "--name", "test-image-find"}}, + want: "Event ID:", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Cleanup + imageName := "test-image-find" + var err error + defer func() { + // Client is initialized since this is the cleanup + cleanupErr := removeImage(Client, imageName) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createImage(Client, imageName) + 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, imageName) { + fmt.Println(stdoutString) + t.Fatal("finding the image was unsuccessful") + } + }) + } +} + +func Test_ImageListCmd(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", "image", "list"}}, + want: "images:", + 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("image list marker not located in output") + } + }) + } +} + +func Test_ImageRemoveCmd(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", "image", "remove", "--name", "test-image-remove"}}, + want: "Event ID:", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Arrange + setupClient(t) + _, err := createImage(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("image", 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("image not successfully removed") + } + }) + } +} + +func Test_ImageRenameCmd(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", "image", "rename", "--name", "test-image-rename", "--newname", "test-image-renamed"}}, + want: "Event ID:", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Cleanup + imageName := "test-image-rename" + newImageName := "test-image-renamed" + var err error + defer func() { + // Client is initialized since this is the cleanup + cleanupErr := removeImage(Client, newImageName) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createImage(Client, imageName) + 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("image", imageName) + cobbler.FailOnError(t, err) + if resultOldName { + t.Fatal("image not successfully renamed (old name present)") + } + resultNewName, err := Client.HasItem("image", newImageName) + cobbler.FailOnError(t, err) + if !resultNewName { + t.Fatal("image not successfully renamed (new name not present)") + } + }) + } +} + +func Test_ImageReportCmd(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", "image", "report", "--name", "test-image-report"}}, + want: ": test-image-report", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Cleanup + imageName := "test-image-report" + var err error + defer func() { + // Client is initialized since this is the cleanup + cleanupErr := removeImage(Client, imageName) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createImage(Client, imageName) + 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/import.go b/cmd/import.go index 0cd5c16..e98c567 100644 --- a/cmd/import.go +++ b/cmd/import.go @@ -10,70 +10,70 @@ import ( "github.com/spf13/cobra" ) -// importCmd represents the import command -var importCmd = &cobra.Command{ - Use: "import", - Short: "Import operating system distributions", - Long: `Import operating system distributions into Cobbler. This could be a mounted ISO, network rsync mirror or a tree in the filesystem. +// NewImportCmd builds a new command that represents the import action +func NewImportCmd() *cobra.Command { + importCmd := &cobra.Command{ + Use: "import", + Short: "Import operating system distributions", + Long: `Import operating system distributions into Cobbler. This could be a mounted ISO, network rsync mirror or a tree in the filesystem. See https://cobbler.readthedocs.io/en/latest/quickstart-guide.html#importing-your-first-distribution for more information.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - archOption, err := cmd.Flags().GetString("arch") - if err != nil { - return err - } - autoinstallOption, err := cmd.Flags().GetString("autoinstall") - if err != nil { - return err - } - availableAsOption, err := cmd.Flags().GetString("available-as") - if err != nil { - return err - } - breedOption, err := cmd.Flags().GetString("breed") - if err != nil { - return err - } - nameOption, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - osVersionOption, err := cmd.Flags().GetString("os-version") - if err != nil { - return err - } - pathOption, err := cmd.Flags().GetString("path") - if err != nil { - return err - } - rsyncFlagsOption, err := cmd.Flags().GetString("rsync-flags") - if err != nil { - return err - } - var backgroundOptions = cobblerclient.BackgroundImportOptions{ - Path: pathOption, - Name: nameOption, - AvailableAs: availableAsOption, - AutoinstallFile: autoinstallOption, - RsyncFlags: rsyncFlagsOption, - Arch: archOption, - Breed: breedOption, - OsVersion: osVersionOption, - } - eventId, err := Client.BackgroundImport(backgroundOptions) - if err != nil { - return err - } - fmt.Printf("Event ID: %s\n", eventId) - return nil - }, -} - -func init() { - rootCmd.AddCommand(importCmd) + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - //local flags + archOption, err := cmd.Flags().GetString("arch") + if err != nil { + return err + } + autoinstallOption, err := cmd.Flags().GetString("autoinstall") + if err != nil { + return err + } + availableAsOption, err := cmd.Flags().GetString("available-as") + if err != nil { + return err + } + breedOption, err := cmd.Flags().GetString("breed") + if err != nil { + return err + } + nameOption, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + osVersionOption, err := cmd.Flags().GetString("os-version") + if err != nil { + return err + } + pathOption, err := cmd.Flags().GetString("path") + if err != nil { + return err + } + rsyncFlagsOption, err := cmd.Flags().GetString("rsync-flags") + if err != nil { + return err + } + var backgroundOptions = cobblerclient.BackgroundImportOptions{ + Path: pathOption, + Name: nameOption, + AvailableAs: availableAsOption, + AutoinstallFile: autoinstallOption, + RsyncFlags: rsyncFlagsOption, + Arch: archOption, + Breed: breedOption, + OsVersion: osVersionOption, + } + eventId, err := Client.BackgroundImport(backgroundOptions) + if err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "Event ID: %s\n", eventId) + return nil + }, + } importCmd.Flags().String("arch", "", "the architechture of the OS") importCmd.Flags().String("autoinstall", "", "assign this autoinstall file") importCmd.Flags().String("available-as", "", "do not mirror, the tree is here") @@ -82,4 +82,5 @@ func init() { importCmd.Flags().String("os-version", "", "the version of the OS") importCmd.Flags().String("path", "", "local path or rsync location") importCmd.Flags().String("rsync-flags", "", "pass additional flags to rsync") + return importCmd } diff --git a/cmd/import_test.go b/cmd/import_test.go new file mode 100644 index 0000000..dc87b27 --- /dev/null +++ b/cmd/import_test.go @@ -0,0 +1,58 @@ +package cmd + +import ( + "bytes" + "fmt" + "github.com/cobbler/cobblerclient" + "github.com/spf13/cobra" + "io" + "strings" + "testing" +) + +func Test_ImportCmd(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", "import"}}, + want: "Event ID:", + 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 + 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, tt.want) { + fmt.Println(stdoutString) + t.Fatal("No Event ID present") + } + }) + } +} diff --git a/cmd/item.go b/cmd/item.go index 8e18171..7a8cc53 100644 --- a/cmd/item.go +++ b/cmd/item.go @@ -134,7 +134,7 @@ func FindItemNames(cmd *cobra.Command, args []string, what string) error { return err } for _, distroName := range itemNames { - fmt.Println(distroName) + fmt.Fprintln(cmd.OutOrStdout(), distroName) } return nil } diff --git a/cmd/list.go b/cmd/list.go index 42e3fbc..0a64930 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -10,74 +10,77 @@ import ( "sort" ) -// listCmd represents the list command -var listCmd = &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 +// NewListCmd represents the list command +func NewListCmd() *cobra.Command { + listCmd := &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). Identical to 'cobbler report'`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - distroNames, err := Client.ListDistroNames() - if err != nil { - return err - } - profileNames, err := Client.ListProfileNames() - if err != nil { - return err - } - systemNames, err := Client.ListSystemNames() - if err != nil { - return err - } - repoNames, err := Client.ListRepoNames() - if err != nil { - return err - } - imageNames, err := Client.ListImageNames() - 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 - } - listItems("distros", distroNames) - listItems("profiles", profileNames) - listItems("systems", systemNames) - listItems("repos", repoNames) - listItems("images", imageNames) - listItems("mgmtclasses", mgmtClassNames) - listItems("packages", packageNames) - listItems("files", fileNames) - listItems("menus", menuNames) - return nil - }, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + distroNames, err := Client.ListDistroNames() + if err != nil { + return err + } + profileNames, err := Client.ListProfileNames() + if err != nil { + return err + } + systemNames, err := Client.ListSystemNames() + if err != nil { + return err + } + repoNames, err := Client.ListRepoNames() + if err != nil { + return err + } + imageNames, err := Client.ListImageNames() + 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 + } + listItems(cmd, "distros", distroNames) + listItems(cmd, "profiles", profileNames) + 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 + }, + } + return listCmd } -func listItems(what string, items []string) { - fmt.Printf("%s:\n", what) +func listItems(cmd *cobra.Command, what string, items []string) { + fmt.Fprintf(cmd.OutOrStdout(), "%s:\n", what) sort.Strings(items) for _, item := range items { - fmt.Printf(" %s\n", item) + fmt.Fprintf(cmd.OutOrStdout(), " %s\n", item) } - fmt.Println() -} - -func init() { - rootCmd.AddCommand(listCmd) + fmt.Fprintln(cmd.OutOrStdout(), "") } diff --git a/cmd/list_test.go b/cmd/list_test.go new file mode 100644 index 0000000..1483d1b --- /dev/null +++ b/cmd/list_test.go @@ -0,0 +1,38 @@ +package cmd + +import ( + "bytes" + "fmt" + "github.com/cobbler/cobblerclient" + "github.com/spf13/cobra" + "io" + "strings" + "testing" +) + +func Test_ListCmd(t *testing.T) { + // Arrange + cobra.OnInitialize(initConfig, setupLogger) + rootCmd := NewRootCmd() + rootCmd.SetArgs([]string{"--config", "../testing/.cobbler.yaml", "list"}) + 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, "distros:") && strings.Contains(stdoutString, "profiles")) { + fmt.Println(stdoutString) + t.Fatal("no heading for distros and profiles present") + } +} diff --git a/cmd/menu.go b/cmd/menu.go index 09169eb..3d6ddc3 100644 --- a/cmd/menu.go +++ b/cmd/menu.go @@ -51,250 +51,324 @@ func updateMenuFromFlags(cmd *cobra.Command, menu *cobbler.Menu) error { return err } -// menuCmd represents the menu command -var menuCmd = &cobra.Command{ - Use: "menu", - Short: "Menu management", - Long: `Let you manage menus. +// NewMenuCmd builds a new command that represents the menu action +func NewMenuCmd() (*cobra.Command, error) { + menuCmd := &cobra.Command{ + Use: "menu", + Short: "Menu management", + Long: `Let you manage menus. See https://cobbler.readthedocs.io/en/latest/cobbler.html#cobbler-menu for more information.`, - Run: func(cmd *cobra.Command, args []string) { - _ = cmd.Help() - }, -} - -var menuAddCmd = &cobra.Command{ - Use: "add", - Short: "add menu", - Long: `Adds a menu.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - newMenu := cobbler.NewMenu() - var err error - - // internal fields (ctime, mtime, depth, uid, source-repos, tree-build-time) cannot be modified - newMenu.Name, err = cmd.Flags().GetString("name") - if err != nil { - return err - } - // Update menu in-memory - err = updateMenuFromFlags(cmd, &newMenu) - if err != nil { - return err - } - // Now create the menu via XML-RPC - menu, err := Client.CreateMenu(newMenu) - if err != nil { - return err - } - fmt.Printf("Menu %s created\n", menu.Name) - return nil - }, -} - -var menuCopyCmd = &cobra.Command{ - Use: "copy", - Short: "copy menu", - Long: `Copies a menu.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - menuName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - menuNewName, err := cmd.Flags().GetString("newname") - if err != nil { - return err - } - - menuHandle, err := Client.GetMenuHandle(menuName) - if err != nil { - return err - } - err = Client.CopyMenu(menuHandle, menuNewName) - if err != nil { - return err - } - newMenu, err := Client.GetMenu(menuNewName, false, false) - if err != nil { - return err - } - err = updateMenuFromFlags(cmd, newMenu) - if err != nil { - return err - } - return Client.UpdateMenu(newMenu) - }, -} - -var menuEditCmd = &cobra.Command{ - Use: "edit", - Short: "edit menu", - Long: `Edits a menu.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - menuName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - - menuToEdit, err := Client.GetMenu(menuName, false, false) - if err != nil { - return err - } - err = updateMenuFromFlags(cmd, menuToEdit) - if err != nil { - return err - } - return Client.UpdateMenu(menuToEdit) - }, -} - -var menuFindCmd = &cobra.Command{ - Use: "find", - Short: "find menu", - Long: `Finds a given menu.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - return FindItemNames(cmd, args, "menu") - }, -} - -var menuListCmd = &cobra.Command{ - Use: "list", - Short: "list all menus", - Long: `Lists all available menus.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - menuNames, err := Client.ListMenuNames() - if err != nil { - return err - } - listItems("menus", menuNames) - return nil - }, -} - -var menuRemoveCmd = &cobra.Command{ - Use: "remove", - Short: "remove menu", - Long: `Removes a given menu.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - return RemoveItemRecursive(cmd, args, "menu") - }, + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, + } + menuAddCmd, err := NewMenuAddCmd() + if err != nil { + return nil, err + } + menuCmd.AddCommand(menuAddCmd) + menuCopyCmd, err := NewMenuCopyCmd() + if err != nil { + return nil, err + } + menuCmd.AddCommand(menuCopyCmd) + menuCmd.AddCommand(NewMenuEditCmd()) + menuCmd.AddCommand(NewMenuFindCmd()) + menuCmd.AddCommand(NewMenuListCmd()) + menuCmd.AddCommand(NewMenuRemoveCmd()) + menuRenameCmd, err := NewMenuRenameCmd() + if err != nil { + return nil, err + } + menuCmd.AddCommand(menuRenameCmd) + menuCmd.AddCommand(NewMenuReportCmd()) + return menuCmd, nil } -var menuRenameCmd = &cobra.Command{ - Use: "rename", - Short: "rename menu", - Long: `Renames a given menu.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - menuName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - menuNewName, err := cmd.Flags().GetString("newname") - if err != nil { - return err - } +func NewMenuAddCmd() (*cobra.Command, error) { + menuAddCmd := &cobra.Command{ + Use: "add", + Short: "add menu", + Long: `Adds a menu.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - menuHandle, err := Client.GetMenuHandle(menuName) - if err != nil { - return err - } - err = Client.RenameMenu(menuHandle, menuNewName) - if err != nil { - return err - } - newMenu, err := Client.GetMenu(menuNewName, false, false) - if err != nil { - return err - } - err = updateMenuFromFlags(cmd, newMenu) - if err != nil { - return err - } - return Client.UpdateMenu(newMenu) - }, -} + newMenu := cobbler.NewMenu() -func reportMenus(menuNames []string) error { - for _, itemName := range menuNames { - menu, err := Client.GetMenu(itemName, false, false) - if err != nil { - return err - } - printStructured(menu) - fmt.Println("") + // internal fields (ctime, mtime, depth, uid, source-repos, tree-build-time) cannot be modified + newMenu.Name, err = cmd.Flags().GetString("name") + if err != nil { + return err + } + // Update menu in-memory + err = updateMenuFromFlags(cmd, &newMenu) + if err != nil { + return err + } + // Now create the menu via XML-RPC + menu, err := Client.CreateMenu(newMenu) + if err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "Menu %s created\n", menu.Name) + return nil + }, } - return nil + addCommonArgs(menuAddCmd) + addStringFlags(menuAddCmd, menuStringFlagMetadata) + err := menuAddCmd.MarkFlagRequired("name") + if err != nil { + return nil, err + } + return menuAddCmd, nil } -var menuReportCmd = &cobra.Command{ - Use: "report", - Short: "list all menus in detail", - Long: `Shows detailed information about all menus.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - name, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - itemNames := make([]string, 0) - if name == "" { - itemNames, err = Client.ListMenuNames() +func NewMenuCopyCmd() (*cobra.Command, error) { + menuCopyCmd := &cobra.Command{ + Use: "copy", + Short: "copy menu", + Long: `Copies a menu.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() if err != nil { return err } - } else { - itemNames = append(itemNames, name) - } - return reportMenus(itemNames) - }, -} -func init() { - rootCmd.AddCommand(menuCmd) - menuCmd.AddCommand(menuAddCmd) - menuCmd.AddCommand(menuCopyCmd) - menuCmd.AddCommand(menuEditCmd) - menuCmd.AddCommand(menuFindCmd) - menuCmd.AddCommand(menuListCmd) - menuCmd.AddCommand(menuRemoveCmd) - menuCmd.AddCommand(menuRenameCmd) - menuCmd.AddCommand(menuReportCmd) - - // local flags for menu add - addCommonArgs(menuAddCmd) - addStringFlags(menuAddCmd, menuStringFlagMetadata) + menuName, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + menuNewName, err := cmd.Flags().GetString("newname") + if err != nil { + return err + } - // local flags for menu copy + menuHandle, err := Client.GetMenuHandle(menuName) + if err != nil { + return err + } + err = Client.CopyMenu(menuHandle, menuNewName) + if err != nil { + return err + } + newMenu, err := Client.GetMenu(menuNewName, false, false) + if err != nil { + return err + } + err = updateMenuFromFlags(cmd, newMenu) + if err != nil { + return err + } + return Client.UpdateMenu(newMenu) + }, + } addCommonArgs(menuCopyCmd) addStringFlags(menuCopyCmd, menuStringFlagMetadata) + addStringFlags(menuCopyCmd, copyRenameStringFlagMetadata) menuCopyCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") + err := menuCopyCmd.MarkFlagRequired("name") + if err != nil { + return nil, err + } + err = menuCopyCmd.MarkFlagRequired("newname") + if err != nil { + return nil, err + } + return menuCopyCmd, nil +} + +func NewMenuEditCmd() *cobra.Command { + menuEditCmd := &cobra.Command{ + Use: "edit", + Short: "edit menu", + Long: `Edits a menu.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - // local flags for menu edit + menuName, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + + menuToEdit, err := Client.GetMenu(menuName, false, false) + if err != nil { + return err + } + err = updateMenuFromFlags(cmd, menuToEdit) + if err != nil { + return err + } + return Client.UpdateMenu(menuToEdit) + }, + } addCommonArgs(menuEditCmd) addStringFlags(menuEditCmd, menuStringFlagMetadata) menuEditCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") + return menuEditCmd +} - // local flags for menu find +func NewMenuFindCmd() *cobra.Command { + menuFindCmd := &cobra.Command{ + Use: "find", + Short: "find menu", + Long: `Finds a given menu.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + return FindItemNames(cmd, args, "menu") + }, + } addCommonArgs(menuFindCmd) addStringFlags(menuFindCmd, menuStringFlagMetadata) addStringFlags(menuFindCmd, findStringFlagMetadata) addIntFlags(menuFindCmd, findIntFlagMetadata) addFloatFlags(menuFindCmd, findFloatFlagMetadata) + return menuFindCmd +} + +func NewMenuListCmd() *cobra.Command { + menuListCmd := &cobra.Command{ + Use: "list", + Short: "list all menus", + Long: `Lists all available menus.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + menuNames, err := Client.ListMenuNames() + if err != nil { + return err + } + listItems(cmd, "menus", menuNames) + return nil + }, + } + return menuListCmd +} + +func NewMenuRemoveCmd() *cobra.Command { + menuRemoveCmd := &cobra.Command{ + Use: "remove", + Short: "remove menu", + Long: `Removes a given menu.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - // local flags for menu remove + return RemoveItemRecursive(cmd, args, "menu") + }, + } menuRemoveCmd.Flags().String("name", "", "the menu name") menuRemoveCmd.Flags().Bool("recursive", false, "also delete child objects") + return menuRemoveCmd +} - // local flags for menu rename +func NewMenuRenameCmd() (*cobra.Command, error) { + menuRenameCmd := &cobra.Command{ + Use: "rename", + Short: "rename menu", + Long: `Renames a given menu.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + menuName, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + menuNewName, err := cmd.Flags().GetString("newname") + if err != nil { + return err + } + + menuHandle, err := Client.GetMenuHandle(menuName) + if err != nil { + return err + } + err = Client.RenameMenu(menuHandle, menuNewName) + if err != nil { + return err + } + newMenu, err := Client.GetMenu(menuNewName, false, false) + if err != nil { + return err + } + err = updateMenuFromFlags(cmd, newMenu) + if err != nil { + return err + } + return Client.UpdateMenu(newMenu) + }, + } addCommonArgs(menuRenameCmd) addStringFlags(menuRenameCmd, menuStringFlagMetadata) + addStringFlags(menuRenameCmd, copyRenameStringFlagMetadata) menuRenameCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") + err := menuRenameCmd.MarkFlagRequired("name") + if err != nil { + return nil, err + } + err = menuRenameCmd.MarkFlagRequired("newname") + if err != nil { + return nil, err + } + return menuRenameCmd, nil +} - // local flags for menu report +func reportMenus(cmd *cobra.Command, menuNames []string) error { + for _, itemName := range menuNames { + menu, err := Client.GetMenu(itemName, false, false) + if err != nil { + return err + } + printStructured(cmd, menu) + fmt.Fprintln(cmd.OutOrStdout(), "") + } + return nil +} + +func NewMenuReportCmd() *cobra.Command { + menuReportCmd := &cobra.Command{ + Use: "report", + Short: "list all menus in detail", + Long: `Shows detailed information about all menus.`, + 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.ListMenuNames() + if err != nil { + return err + } + } else { + itemNames = append(itemNames, name) + } + return reportMenus(cmd, itemNames) + }, + } menuReportCmd.Flags().String("name", "", "the menu name") + return menuReportCmd } diff --git a/cmd/menu_test.go b/cmd/menu_test.go new file mode 100644 index 0000000..4f6ad2f --- /dev/null +++ b/cmd/menu_test.go @@ -0,0 +1,454 @@ +package cmd + +import ( + "bytes" + "fmt" + cobbler "github.com/cobbler/cobblerclient" + "github.com/spf13/cobra" + "io" + "strings" + "testing" +) + +func createMenu(client cobbler.Client, name string) (*cobbler.Menu, error) { + menu := cobbler.NewMenu() + menu.Name = name + return client.CreateMenu(menu) +} + +func removeMenu(client cobbler.Client, name string) error { + return client.DeleteMenu(name) +} + +func Test_MenuAddCmd(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", "menu", "add", "--name", "test-plain"}}, + want: "Menu 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 := removeMenu(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_MenuCopyCmd(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", "menu", "copy", "--name", "menu-to-copy", "--newname", "copied-menu"}}, + 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 := removeMenu(Client, tt.args.command[5]) + cobbler.FailOnError(t, cleanupErr) + cleanupErr = removeMenu(Client, tt.args.command[7]) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createMenu(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.GetMenu(tt.args.command[7], false, false) + cobbler.FailOnError(t, err) + }) + } +} + +func Test_MenuEditCmd(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", "menu", "edit", "--name", "test-menu-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 := removeMenu(Client, tt.args.command[5]) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createMenu(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) + updatedMenu, err := Client.GetMenu(tt.args.command[5], false, false) + cobbler.FailOnError(t, err) + if updatedMenu.Comment != "testcomment" { + t.Fatal("menu update wasn't successful") + } + }) + } +} + +func Test_MenuFindCmd(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", "menu", "find", "--name", "test-menu-find"}}, + want: "Event ID:", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Cleanup + menuName := "test-menu-find" + var err error + defer func() { + // Client is initialized since this is the cleanup + cleanupErr := removeMenu(Client, menuName) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createMenu(Client, menuName) + 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, menuName) { + fmt.Println(stdoutString) + t.Fatal("menu not successfully found") + } + }) + } +} + +func Test_MenuListCmd(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", "menu", "list"}}, + want: "menus:", + 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("menu list marker not located in output") + } + }) + } +} + +func Test_MenuRemoveCmd(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", "menu", "remove", "--name", "test-menu-remove"}}, + want: "Event ID:", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Arrange + setupClient(t) + _, err := createMenu(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("menu", 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("menu not successfully removed") + } + }) + } +} + +func Test_MenuRenameCmd(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", "menu", "rename", "--name", "test-menu-rename", "--newname", "test-menu-renamed"}}, + want: "Event ID:", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Cleanup + menuName := "test-menu-rename" + newMenuName := "test-menu-renamed" + var err error + defer func() { + // Client is initialized since this is the cleanup + cleanupErr := removeMenu(Client, newMenuName) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createMenu(Client, menuName) + 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("menu", menuName) + cobbler.FailOnError(t, err) + if resultOldName { + t.Fatal("menu not successfully renamed (old name present)") + } + resultNewName, err := Client.HasItem("menu", newMenuName) + cobbler.FailOnError(t, err) + if !resultNewName { + t.Fatal("menu not successfully renamed (new name not present)") + } + }) + } +} + +func Test_MenuReportCmd(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", "menu", "report", "--name", "test-menu-report"}}, + want: ": test-menu-report", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Cleanup + menuName := "test-menu-report" + var err error + defer func() { + // Client is initialized since this is the cleanup + cleanupErr := removeMenu(Client, menuName) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createMenu(Client, menuName) + 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/metadata.go b/cmd/metadata.go index 92d2428..548d0b6 100644 --- a/cmd/metadata.go +++ b/cmd/metadata.go @@ -29,6 +29,14 @@ var commonStringSliceFlagMetadata = map[string]FlagMetadata[[]string]{ }, } +var copyRenameStringFlagMetadata = map[string]FlagMetadata[string]{ + "newname": { + Name: "newname", + DefaultValue: "", + Usage: "the new item name", + }, +} + var distroStringFlagMetadata = map[string]FlagMetadata[string]{ "kernel": { Name: "kernel", diff --git a/cmd/mgmtclass.go b/cmd/mgmtclass.go index 8091851..d308081 100644 --- a/cmd/mgmtclass.go +++ b/cmd/mgmtclass.go @@ -12,9 +12,13 @@ import ( ) func updateMgmtClassFromFlags(cmd *cobra.Command, mgmtClass *cobbler.MgmtClass) error { - inPlace, err := cmd.Flags().GetBool("in-place") - if err != nil { - return err + 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 { @@ -102,247 +106,113 @@ func updateMgmtClassFromFlags(cmd *cobra.Command, mgmtClass *cobbler.MgmtClass) return err } -// mgmtclassCmd represents the mgmtclass command -var mgmtclassCmd = &cobra.Command{ - Use: "mgmtclass", - Short: "Mgmtclass management", - Long: `Let you manage mgmtclasses. +// NewMgmtClassCmd builds a new command that represents the mgmtclass action +func NewMgmtClassCmd() *cobra.Command { + 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() - }, -} - -var mgmtclassAddCmd = &cobra.Command{ - Use: "add", - Short: "add mgmtclass", - Long: `Adds a mgmtclass.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - newMgmtClass := cobbler.NewMgmtClass() - var err error - - // 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.Printf("Mgmtclass %s created\n", mgmtClass.Name) - return nil - }, -} - -var mgmtclassCopyCmd = &cobra.Command{ - Use: "copy", - Short: "copy mgmtclass", - Long: `Copies a mgmtclass.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - // 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) - }, -} - -var mgmtclassEditCmd = &cobra.Command{ - Use: "edit", - Short: "edit mgmtclass", - Long: `Edits a mgmtclass.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - // 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) - }, -} - -var mgmtclassFindCmd = &cobra.Command{ - Use: "find", - Short: "find mgmtclass", - Long: `Finds a given mgmtclass.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - return FindItemNames(cmd, args, "mgmtclass") - }, -} - -var mgmtclassListCmd = &cobra.Command{ - Use: "list", - Short: "list all mgmtclasses", - Long: `Lists all available mgmtclasses.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - mgmtclassNames, err := Client.ListMgmtClassNames() - if err != nil { - return err - } - listItems("mgmtclasses", mgmtclassNames) - return nil - }, -} - -var mgmtclassRemoveCmd = &cobra.Command{ - Use: "remove", - Short: "remove mgmtclass", - Long: `Removes a given mgmtclass.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - return RemoveItemRecursive(cmd, args, "mgmtclass") - }, -} - -var mgmtclassRenameCmd = &cobra.Command{ - Use: "rename", - Short: "rename mgmtclass", - Long: `Renames a given mgmtclass.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - // 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) - }, -} - -func reportMgmtClasses(mgmtClassNames []string) error { - for _, itemName := range mgmtClassNames { - system, err := Client.GetMgmtClass(itemName, false, false) - if err != nil { - return err - } - printStructured(system) - fmt.Println("") + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, } - return nil + mgmtclassCmd.AddCommand(NewMgmtClassAddCmd()) + mgmtclassCmd.AddCommand(NewMgmtClassCopyCmd()) + mgmtclassCmd.AddCommand(NewMgmtClassEditCmd()) + mgmtclassCmd.AddCommand(NewMgmtClassFindCmd()) + mgmtclassCmd.AddCommand(NewMgmtClassListCmd()) + mgmtclassCmd.AddCommand(NewMgmtClassRemoveCmd()) + mgmtclassCmd.AddCommand(NewMgmtClassRenameCmd()) + mgmtclassCmd.AddCommand(NewMgmtClassReportCmd()) + return mgmtclassCmd } -var 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 { - generateCobblerClient() - name, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - itemNames := make([]string, 0) - if name == "" { - itemNames, err = Client.ListMgmtClassNames() +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 } - } else { - itemNames = append(itemNames, name) - } - return reportMgmtClasses(itemNames) - }, -} -func init() { - rootCmd.AddCommand(mgmtclassCmd) - mgmtclassCmd.AddCommand(mgmtclassAddCmd) - mgmtclassCmd.AddCommand(mgmtclassCopyCmd) - mgmtclassCmd.AddCommand(mgmtclassEditCmd) - mgmtclassCmd.AddCommand(mgmtclassFindCmd) - mgmtclassCmd.AddCommand(mgmtclassListCmd) - mgmtclassCmd.AddCommand(mgmtclassRemoveCmd) - mgmtclassCmd.AddCommand(mgmtclassRenameCmd) - mgmtclassCmd.AddCommand(mgmtclassReportCmd) + newMgmtClass := cobbler.NewMgmtClass() - // local flags for mgmtclass add + // 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 + } - // local flags for mgmtclass copy + // 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) @@ -350,16 +220,63 @@ func init() { 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 + } - // local flags for mgmtclass edit + // 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 +} - // local flags for mgmtclass find +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) @@ -368,12 +285,95 @@ func init() { addStringFlags(mgmtclassFindCmd, findStringFlagMetadata) addIntFlags(mgmtclassFindCmd, findIntFlagMetadata) addFloatFlags(mgmtclassFindCmd, findFloatFlagMetadata) + return mgmtclassFindCmd +} - // local flags for mgmtclass remove +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 +} - // local flags for mgmtclass rename +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) @@ -381,7 +381,48 @@ func init() { 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 +} - // local flags for mgmtclass report +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 } diff --git a/cmd/mgmtclass_test.go b/cmd/mgmtclass_test.go new file mode 100644 index 0000000..e0f71dd --- /dev/null +++ b/cmd/mgmtclass_test.go @@ -0,0 +1,454 @@ +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/mkloaders.go b/cmd/mkloaders.go index 362424c..1144494 100644 --- a/cmd/mkloaders.go +++ b/cmd/mkloaders.go @@ -9,25 +9,28 @@ import ( "github.com/spf13/cobra" ) -// mkloadersCmd represents the mkloaders command -var mkloadersCmd = &cobra.Command{ - Use: "mkloaders", - Short: "Generate GRUB 2 bootloaders", - Long: `Generate UEFI bootable GRUB 2 bootloaders. If available on the operating system Cobbler is running on, +// NewMkLoadersCmd builds a new command that represents the mkloaders action +func NewMkLoadersCmd() *cobra.Command { + mkloadersCmd := &cobra.Command{ + Use: "mkloaders", + Short: "Generate GRUB 2 bootloaders", + Long: `Generate UEFI bootable GRUB 2 bootloaders. If available on the operating system Cobbler is running on, then this also generates bootloaders for different architectures then the one of the system. The options are configured in the Cobbler settings file.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - eventId, err := Client.BackgroundMkLoaders() - if err != nil { - return err - } - fmt.Printf("Event ID: %s\n", eventId) - return nil - }, -} + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } -func init() { - rootCmd.AddCommand(mkloadersCmd) + eventId, err := Client.BackgroundMkLoaders() + if err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "Event ID: %s\n", eventId) + return nil + }, + } + return mkloadersCmd } diff --git a/cmd/mkloaders_test.go b/cmd/mkloaders_test.go new file mode 100644 index 0000000..e330ef5 --- /dev/null +++ b/cmd/mkloaders_test.go @@ -0,0 +1,38 @@ +package cmd + +import ( + "bytes" + "fmt" + "github.com/cobbler/cobblerclient" + "github.com/spf13/cobra" + "io" + "strings" + "testing" +) + +func Test_MkLoaders(t *testing.T) { + // Arrange + cobra.OnInitialize(initConfig, setupLogger) + rootCmd := NewRootCmd() + rootCmd.SetArgs([]string{"--config", "../testing/.cobbler.yaml", "mkloaders"}) + 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, "Event ID:") { + fmt.Println(stdoutString) + t.Fatal("No Event ID present") + } +} diff --git a/cmd/package.go b/cmd/package.go index 6cbae59..b8b80a4 100644 --- a/cmd/package.go +++ b/cmd/package.go @@ -71,267 +71,307 @@ func updatePackageFromFlags(cmd *cobra.Command, p *cobbler.Package) error { return err } -// packageCmd represents the package command -var packageCmd = &cobra.Command{ - Use: "package", - Short: "Package management", - Long: `Let you manage packages. +// NewPackageCmd builds a new command that represents the package action +func NewPackageCmd() *cobra.Command { + 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() - }, -} - -var packageAddCmd = &cobra.Command{ - Use: "add", - Short: "add package", - Long: `Adds a package.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - newPackage := cobbler.NewPackage() - var err error - - // 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.Printf("Package %s created\n", linuxpackage.Name) - return nil - }, -} - -var packageCopyCmd = &cobra.Command{ - Use: "copy", - Short: "copy package", - Long: `Copies a package.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - // 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) - }, -} - -var packageEditCmd = &cobra.Command{ - Use: "edit", - Short: "edit package", - Long: `Edits a package.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - 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) - }, -} - -var packageFindCmd = &cobra.Command{ - Use: "find", - Short: "find package", - Long: `Finds a given package.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - return FindItemNames(cmd, args, "package") - }, -} - -var packageListCmd = &cobra.Command{ - Use: "list", - Short: "list all packages", - Long: `Lists all available packages.`, - Run: func(cmd *cobra.Command, args []string) { - generateCobblerClient() - packageNames, err := Client.ListPackageNames() - if err != nil { - fmt.Println(err) - } - listItems("packages", packageNames) - }, -} - -var packageRemoveCmd = &cobra.Command{ - Use: "remove", - Short: "remove package", - Long: `Removes a given package.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - return RemoveItemRecursive(cmd, args, "package") - }, + 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()) + return packageCmd } -var packageRenameCmd = &cobra.Command{ - Use: "rename", - Short: "rename package", - Long: `Renames a given package.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - // 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 - } +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 + } - // 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) - }, -} + newPackage := cobbler.NewPackage() -func reportPackages(packageNames []string) error { - for _, itemName := range packageNames { - repo, err := Client.GetRepo(itemName, false, false) - if err != nil { - return err - } - printStructured(repo) - fmt.Println("") + // 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 + }, } - return nil + addCommonArgs(packageAddCmd) + addStringFlags(packageAddCmd, packageStringFlagMetadata) + return packageAddCmd } -var 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 { - generateCobblerClient() - name, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - itemNames := make([]string, 0) - if name == "" { - itemNames, err = Client.ListRepoNames() +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 } - } else { - itemNames = append(itemNames, name) - } - return reportPackages(itemNames) - }, -} - -func init() { - rootCmd.AddCommand(packageCmd) - packageCmd.AddCommand(packageAddCmd) - packageCmd.AddCommand(packageCopyCmd) - packageCmd.AddCommand(packageEditCmd) - packageCmd.AddCommand(packageFindCmd) - packageCmd.AddCommand(packageListCmd) - packageCmd.AddCommand(packageRemoveCmd) - packageCmd.AddCommand(packageRenameCmd) - packageCmd.AddCommand(packageReportCmd) - // local flags for package add - addCommonArgs(packageAddCmd) - addStringFlags(packageAddCmd, packageStringFlagMetadata) + // 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 + } - // local flags for package copy + // 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 +} - // local flags for package edit +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 + } - // local flags for package find + 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 +} - // local flags for package remove +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 + } - // local flags for package rename + // 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 +} - // local flags for package report +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 } diff --git a/cmd/package_test.go b/cmd/package_test.go new file mode 100644 index 0000000..3d55d93 --- /dev/null +++ b/cmd/package_test.go @@ -0,0 +1,454 @@ +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 7f70686..452ba11 100644 --- a/cmd/profile.go +++ b/cmd/profile.go @@ -9,7 +9,6 @@ import ( cobbler "github.com/cobbler/cobblerclient" "github.com/spf13/cobra" "github.com/spf13/pflag" - "os" ) func updateProfileFromFlags(cmd *cobra.Command, profile *cobbler.Profile) error { @@ -506,285 +505,59 @@ func updateProfileFromFlags(cmd *cobra.Command, profile *cobbler.Profile) error return nil } -// profileCmd represents the profile command -var profileCmd = &cobra.Command{ - Use: "profile", - Short: "Profile management", - Long: `Let you manage profiles. +// NewProfileCmd builds a new command that represents the profile action +func NewProfileCmd() *cobra.Command { + profileCmd := &cobra.Command{ + Use: "profile", + Short: "Profile management", + Long: `Let you manage profiles. See https://cobbler.readthedocs.io/en/latest/cobbler.html#cobbler-profile for more information.`, - Run: func(cmd *cobra.Command, args []string) { - _ = cmd.Help() - }, -} - -var profileAddCmd = &cobra.Command{ - Use: "add", - Short: "add profile", - Long: `Adds a profile.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - newProfile := cobbler.NewProfile() - var err error - // internal fields (ctime, mtime, uid, depth, repos-enabled) cannot be modified - newProfile.Name, err = cmd.Flags().GetString("name") - if err != nil { - return err - } - err = updateProfileFromFlags(cmd, &newProfile) - if err != nil { - return err - } - profile, err := Client.CreateProfile(newProfile) - if err != nil { - return err - } - fmt.Printf("Profile %s created\n", profile.Name) - return nil - }, -} - -var profileCopyCmd = &cobra.Command{ - Use: "copy", - Short: "copy profile", - Long: `Copies a profile.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - profileName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - profileNewName, err := cmd.Flags().GetString("newname") - if err != nil { - return err - } - - profileHandle, err := Client.GetProfileHandle(profileName) - if err != nil { - return err - } - err = Client.CopyDistro(profileHandle, profileNewName) - if err != nil { - return err - } - newProfile, err := Client.GetProfile(profileNewName, false, false) - if err != nil { - return err - } - err = updateProfileFromFlags(cmd, newProfile) - if err != nil { - return err - } - return Client.UpdateProfile(newProfile) - }, -} - -var profileDumpVarsCmd = &cobra.Command{ - Use: "dumpvars", - Short: "dump profile variables", - Long: `Prints all profile variables to stdout.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - // Get CLI flags - profileName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - - // Now retrieve data - blendedData, err := Client.GetBlendedData(profileName, "") - if err != nil { - return err - } - // Print data - printDumpVars(blendedData) - return err - }, -} - -var profileEditCmd = &cobra.Command{ - Use: "edit", - Short: "edit profile", - Long: `Edits a profile.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - // find profile through its name - pname, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - updateProfile, err := Client.GetProfile(pname, false, false) - if err != nil { - return err - } - - // internal fields (ctime, mtime, uid, depth, repos-enabled) cannot be modified - err = updateProfileFromFlags(cmd, updateProfile) - if err != nil { - return err - } - return Client.UpdateProfile(updateProfile) - }, -} - -var profileFindCmd = &cobra.Command{ - Use: "find", - Short: "find profile", - Long: `Finds a given profile.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - return FindItemNames(cmd, args, "profile") - }, -} - -var profileGetAutoinstallCmd = &cobra.Command{ - Use: "get-autoinstall", - Short: "dump autoinstall XML", - Long: `Prints the autoinstall XML file of the given profile to stdout.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - profileName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - profileExists, err := Client.HasItem("profile", profileName) - if err != nil { - return err - } - if !profileExists { - fmt.Println("Profile does not exist!") - os.Exit(1) - } - autoinstallRendered, err := Client.GenerateAutoinstall(profileName, "") - if err != nil { - return err - } - fmt.Println(autoinstallRendered) - return nil - }, -} - -var profileListCmd = &cobra.Command{ - Use: "list", - Short: "list all profiles", - Long: `Lists all available profiles.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - profileNames, err := Client.ListProfileNames() - if err != nil { - return err - } - listItems("profiles", profileNames) - return nil - }, -} - -var profileRemoveCmd = &cobra.Command{ - Use: "remove", - Short: "remove profile", - Long: `Removes a given profile.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - pname, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - recursiveDelete, err := cmd.Flags().GetBool("recursive") - if err != nil { - return err - } - return Client.DeleteProfileRecursive(pname, recursiveDelete) - }, -} - -var profileRenameCmd = &cobra.Command{ - Use: "rename", - Short: "rename profile", - Long: `Renames a given profile.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - profileName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - profileNewName, err := cmd.Flags().GetString("newname") - if err != nil { - return err - } - - // Now do the real edit - profileHandle, err := Client.GetProfileHandle(profileName) - if err != nil { - return err - } - err = Client.RenameProfile(profileHandle, profileNewName) - if err != nil { - return err - } - newProfile, err := Client.GetProfile(profileNewName, false, false) - if err != nil { - return err - } - err = updateProfileFromFlags(cmd, newProfile) - if err != nil { - return err - } - return Client.UpdateProfile(newProfile) - }, -} - -func reportProfiles(profileNames []string) error { - for _, itemName := range profileNames { - profile, err := Client.GetProfile(itemName, false, false) - if err != nil { - return err - } - printStructured(profile) - fmt.Println("") + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, } - return nil + profileCmd.AddCommand(NewProfileAddCmd()) + profileCmd.AddCommand(NewProfileCopyCmd()) + profileCmd.AddCommand(NewProfileDumpVars()) + profileCmd.AddCommand(NewProfileEditCmd()) + profileCmd.AddCommand(NewProfileFindCmd()) + profileCmd.AddCommand(NewProfileGetAutoinstallCmd()) + profileCmd.AddCommand(NewProfileListCmd()) + profileCmd.AddCommand(NewProfileRemoveCmd()) + profileCmd.AddCommand(NewProfileRenameCmd()) + profileCmd.AddCommand(NewProfileReportCmd()) + return profileCmd } -var profileReportCmd = &cobra.Command{ - Use: "report", - Short: "list all profiles in detail", - Long: `Shows detailed information about all profiles.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - name, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - itemNames := make([]string, 0) - if name == "" { - itemNames, err = Client.ListProfileNames() +func NewProfileAddCmd() *cobra.Command { + profileAddCmd := &cobra.Command{ + Use: "add", + Short: "add profile", + Long: `Adds a profile.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() if err != nil { return err } - } else { - itemNames = append(itemNames, name) - } - return reportProfiles(itemNames) - }, -} - -func init() { - rootCmd.AddCommand(profileCmd) - profileCmd.AddCommand(profileAddCmd) - profileCmd.AddCommand(profileCopyCmd) - profileCmd.AddCommand(profileDumpVarsCmd) - profileCmd.AddCommand(profileEditCmd) - profileCmd.AddCommand(profileFindCmd) - profileCmd.AddCommand(profileGetAutoinstallCmd) - profileCmd.AddCommand(profileListCmd) - profileCmd.AddCommand(profileRemoveCmd) - profileCmd.AddCommand(profileRenameCmd) - profileCmd.AddCommand(profileReportCmd) - // local flags for profile add + newProfile := cobbler.NewProfile() + // internal fields (ctime, mtime, uid, depth, repos-enabled) cannot be modified + newProfile.Name, err = cmd.Flags().GetString("name") + if err != nil { + return err + } + err = updateProfileFromFlags(cmd, &newProfile) + if err != nil { + return err + } + profile, err := Client.CreateProfile(newProfile) + if err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "Profile %s created\n", profile.Name) + return nil + }, + } addCommonArgs(profileAddCmd) addStringFlags(profileAddCmd, profileStringFlagMetadata) addBoolFlags(profileAddCmd, profileBoolFlagMetadata) @@ -795,8 +568,48 @@ func init() { addMapFlags(profileAddCmd, distroMapFlagMetadata) addMapFlags(profileAddCmd, profileMapFlagMetadata) profileAddCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") + return profileAddCmd +} - // local flags for profile copy +func NewProfileCopyCmd() *cobra.Command { + profileCopyCmd := &cobra.Command{ + Use: "copy", + Short: "copy profile", + Long: `Copies a profile.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + profileName, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + profileNewName, err := cmd.Flags().GetString("newname") + if err != nil { + return err + } + + profileHandle, err := Client.GetProfileHandle(profileName) + if err != nil { + return err + } + err = Client.CopyProfile(profileHandle, profileNewName) + if err != nil { + return err + } + newProfile, err := Client.GetProfile(profileNewName, false, false) + if err != nil { + return err + } + err = updateProfileFromFlags(cmd, newProfile) + if err != nil { + return err + } + return Client.UpdateProfile(newProfile) + }, + } addCommonArgs(profileCopyCmd) addStringFlags(profileCopyCmd, profileStringFlagMetadata) addBoolFlags(profileCopyCmd, profileBoolFlagMetadata) @@ -808,11 +621,69 @@ func init() { addMapFlags(profileCopyCmd, profileMapFlagMetadata) profileCopyCmd.Flags().String("newname", "", "the new profile name") profileCopyCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") + return profileCopyCmd +} - // local flags for profile dumpvars +func NewProfileDumpVars() *cobra.Command { + profileDumpVarsCmd := &cobra.Command{ + Use: "dumpvars", + Short: "dump profile variables", + Long: `Prints all profile variables to stdout.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + // Get CLI flags + profileName, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + + // Now retrieve data + blendedData, err := Client.GetBlendedData(profileName, "") + if err != nil { + return err + } + // Print data + printDumpVars(cmd, blendedData) + return err + }, + } profileDumpVarsCmd.Flags().String("name", "", "the profile name") + return profileDumpVarsCmd +} + +func NewProfileEditCmd() *cobra.Command { + profileEditCmd := &cobra.Command{ + Use: "edit", + Short: "edit profile", + Long: `Edits a profile.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - // local flags for profile edit + // find profile through its name + pname, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + updateProfile, err := Client.GetProfile(pname, false, false) + if err != nil { + return err + } + + // internal fields (ctime, mtime, uid, depth, repos-enabled) cannot be modified + err = updateProfileFromFlags(cmd, updateProfile) + if err != nil { + return err + } + return Client.UpdateProfile(updateProfile) + }, + } addCommonArgs(profileEditCmd) addStringFlags(profileEditCmd, profileStringFlagMetadata) addBoolFlags(profileEditCmd, profileBoolFlagMetadata) @@ -823,8 +694,23 @@ func init() { addMapFlags(profileEditCmd, distroMapFlagMetadata) addMapFlags(profileEditCmd, profileMapFlagMetadata) profileEditCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") + return profileEditCmd +} + +func NewProfileFindCmd() *cobra.Command { + profileFindCmd := &cobra.Command{ + Use: "find", + Short: "find profile", + Long: `Finds a given profile.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - // local flags for profile find + return FindItemNames(cmd, args, "profile") + }, + } addCommonArgs(profileFindCmd) addStringFlags(profileFindCmd, profileStringFlagMetadata) addBoolFlags(profileFindCmd, profileBoolFlagMetadata) @@ -837,15 +723,133 @@ func init() { addStringFlags(profileFindCmd, findStringFlagMetadata) addIntFlags(profileFindCmd, findIntFlagMetadata) addFloatFlags(profileFindCmd, findFloatFlagMetadata) + return profileFindCmd +} + +func NewProfileGetAutoinstallCmd() *cobra.Command { + profileGetAutoinstallCmd := &cobra.Command{ + Use: "get-autoinstall", + Short: "dump autoinstall XML", + Long: `Prints the autoinstall XML file of the given profile to stdout.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - // local flags for profile get-autoinstall + profileName, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + profileExists, err := Client.HasItem("profile", profileName) + if err != nil { + return err + } + if !profileExists { + return fmt.Errorf("Profile does not exist!") + + } + autoinstallRendered, err := Client.GenerateAutoinstall(profileName, "") + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), autoinstallRendered) + return nil + }, + } profileGetAutoinstallCmd.Flags().String("name", "", "the profile name") + return profileGetAutoinstallCmd +} + +func NewProfileListCmd() *cobra.Command { + profileListCmd := &cobra.Command{ + Use: "list", + Short: "list all profiles", + Long: `Lists all available profiles.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + profileNames, err := Client.ListProfileNames() + if err != nil { + return err + } + listItems(cmd, "profiles", profileNames) + return nil + }, + } + return profileListCmd +} - // local flags for profile remove +func NewProfileRemoveCmd() *cobra.Command { + profileRemoveCmd := &cobra.Command{ + Use: "remove", + Short: "remove profile", + Long: `Removes a given profile.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + pname, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + recursiveDelete, err := cmd.Flags().GetBool("recursive") + if err != nil { + return err + } + return Client.DeleteProfileRecursive(pname, recursiveDelete) + }, + } profileRemoveCmd.Flags().String("name", "", "the profile name") profileRemoveCmd.Flags().Bool("recursive", false, "also delete child objects") + return profileRemoveCmd +} + +func NewProfileRenameCmd() *cobra.Command { + profileRenameCmd := &cobra.Command{ + Use: "rename", + Short: "rename profile", + Long: `Renames a given profile.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - // local flags for profile rename + profileName, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + profileNewName, err := cmd.Flags().GetString("newname") + if err != nil { + return err + } + + // Now do the real edit + profileHandle, err := Client.GetProfileHandle(profileName) + if err != nil { + return err + } + err = Client.RenameProfile(profileHandle, profileNewName) + if err != nil { + return err + } + newProfile, err := Client.GetProfile(profileNewName, false, false) + if err != nil { + return err + } + err = updateProfileFromFlags(cmd, newProfile) + if err != nil { + return err + } + return Client.UpdateProfile(newProfile) + }, + } addCommonArgs(profileRenameCmd) addStringFlags(profileRenameCmd, profileStringFlagMetadata) addBoolFlags(profileRenameCmd, profileBoolFlagMetadata) @@ -857,7 +861,48 @@ func init() { addMapFlags(profileRenameCmd, profileMapFlagMetadata) profileRenameCmd.Flags().String("newname", "", "the new profile name") profileRenameCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") + return profileRenameCmd +} + +func reportProfiles(cmd *cobra.Command, profileNames []string) error { + for _, itemName := range profileNames { + profile, err := Client.GetProfile(itemName, false, false) + if err != nil { + return err + } + printStructured(cmd, profile) + fmt.Fprintln(cmd.OutOrStdout(), "") + } + return nil +} + +func NewProfileReportCmd() *cobra.Command { + profileReportCmd := &cobra.Command{ + Use: "report", + Short: "list all profiles in detail", + Long: `Shows detailed information about all profiles.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - // local flags for profile report + name, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + itemNames := make([]string, 0) + if name == "" { + itemNames, err = Client.ListProfileNames() + if err != nil { + return err + } + } else { + itemNames = append(itemNames, name) + } + return reportProfiles(cmd, itemNames) + }, + } profileReportCmd.Flags().String("name", "", "the profile name") + return profileReportCmd } diff --git a/cmd/profile_test.go b/cmd/profile_test.go new file mode 100644 index 0000000..31d7993 --- /dev/null +++ b/cmd/profile_test.go @@ -0,0 +1,455 @@ +package cmd + +import ( + "bytes" + "fmt" + cobbler "github.com/cobbler/cobblerclient" + "github.com/spf13/cobra" + "io" + "strings" + "testing" +) + +func createProfile(client cobbler.Client, name string) (*cobbler.Profile, error) { + profile := cobbler.NewProfile() + profile.Name = name + profile.Distro = "Ubuntu-20.04-x86_64" + return client.CreateProfile(profile) +} + +func removeProfile(client cobbler.Client, name string) error { + return client.DeleteProfile(name) +} + +func Test_ProfileAddCmd(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", "profile", "add", "--name", "test-plain", "--distro", "Ubuntu-20.04-x86_64"}}, + want: "Profile 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 := removeProfile(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_ProfileCopyCmd(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", "profile", "copy", "--name", "profile-to-copy", "--newname", "copied-profile"}}, + 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 := removeProfile(Client, tt.args.command[5]) + cobbler.FailOnError(t, cleanupErr) + cleanupErr = removeProfile(Client, tt.args.command[7]) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createProfile(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.GetProfile(tt.args.command[7], false, false) + cobbler.FailOnError(t, err) + }) + } +} + +func Test_ProfileEditCmd(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", "profile", "edit", "--name", "test-profile-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 := removeProfile(Client, tt.args.command[5]) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createProfile(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) + updatedProfile, err := Client.GetProfile(tt.args.command[5], false, false) + cobbler.FailOnError(t, err) + if updatedProfile.Comment != "testcomment" { + t.Fatal("profile update wasn't successful") + } + }) + } +} + +func Test_ProfileFindCmd(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", "profile", "find", "--name", "test-profile-find"}}, + want: "Event ID:", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Cleanup + profileName := "test-profile-find" + var err error + defer func() { + // Client is initialized since this is the cleanup + cleanupErr := removeProfile(Client, profileName) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createProfile(Client, profileName) + 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, profileName) { + fmt.Println(stdoutString) + t.Fatal("profile not successfully found") + } + }) + } +} + +func Test_ProfileListCmd(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", "profile", "list"}}, + want: "profiles:", + 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("profile list marker not located in output") + } + }) + } +} + +func Test_ProfileRemoveCmd(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", "profile", "remove", "--name", "test-profile-remove"}}, + want: "Event ID:", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Arrange + setupClient(t) + _, err := createProfile(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("profile", 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("profile not successfully removed") + } + }) + } +} + +func Test_ProfileRenameCmd(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", "profile", "rename", "--name", "test-profile-rename", "--newname", "test-profile-renamed"}}, + want: "Event ID:", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Cleanup + profileName := "test-profile-rename" + newProfileName := "test-profile-renamed" + var err error + defer func() { + // Client is initialized since this is the cleanup + cleanupErr := removeProfile(Client, newProfileName) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createProfile(Client, profileName) + 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("profile", profileName) + cobbler.FailOnError(t, err) + if resultOldName { + t.Fatal("profile not successfully renamed (old name present)") + } + resultNewName, err := Client.HasItem("profile", newProfileName) + cobbler.FailOnError(t, err) + if !resultNewName { + t.Fatal("profile not successfully renamed (new name not present)") + } + }) + } +} + +func Test_ProfileReportCmd(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", "profile", "report", "--name", "test-profile-report"}}, + want: ": test-profile-report", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Cleanup + profileName := "test-profile-report" + var err error + defer func() { + // Client is initialized since this is the cleanup + cleanupErr := removeProfile(Client, profileName) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createProfile(Client, profileName) + 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/replicate.go b/cmd/replicate.go index b19b28e..a995754 100644 --- a/cmd/replicate.go +++ b/cmd/replicate.go @@ -10,103 +10,103 @@ import ( "github.com/spf13/cobra" ) -// replicateCmd represents the replicate command -var replicateCmd = &cobra.Command{ - Use: "replicate", - Short: "Replicate data", - Long: `Replicate configurations from a master Cobbler server. This feature is intended for load-balancing, +// NewReplicateCmd builds a new command that represents the replicate action +func NewReplicateCmd() *cobra.Command { + replicateCmd := &cobra.Command{ + Use: "replicate", + Short: "Replicate data", + Long: `Replicate configurations from a master Cobbler server. This feature is intended for load-balancing, disaster-recovery, backup, or multiple geography support. Each Cobbler server is still expected to have a locally relevant cobbler.conf and modules.conf, as these files are not synced. See https://cobbler.readthedocs.io/en/latest/cobbler.html#cobbler-replicate for more information.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - distrosOption, err := cmd.Flags().GetString("distros") - if err != nil { - return err - } - profilesOption, err := cmd.Flags().GetString("profiles") - if err != nil { - return err - } - systemsOption, err := cmd.Flags().GetString("systems") - if err != nil { - return err - } - reposOption, err := cmd.Flags().GetString("repos") - if err != nil { - return err - } - imagesOption, err := cmd.Flags().GetString("image") - 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 - } - masterOption, err := cmd.Flags().GetString("master") - if err != nil { - return err - } - pruneOption, err := cmd.Flags().GetBool("prune") - if err != nil { - return err - } - omitDataOption, err := cmd.Flags().GetBool("omit-data") - if err != nil { - return err - } - syncAllOption, err := cmd.Flags().GetBool("sync-all") - if err != nil { - return err - } - useSslOption, err := cmd.Flags().GetBool("use-ssl") - if err != nil { - 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, - } - eventId, err := Client.BackgroundReplicate(replicateOptions) - if err != nil { - return err - } - fmt.Printf("EventID: %s\n", eventId) - return nil - }, -} - -func init() { - rootCmd.AddCommand(replicateCmd) + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - //local flags + distrosOption, err := cmd.Flags().GetString("distros") + if err != nil { + return err + } + profilesOption, err := cmd.Flags().GetString("profiles") + if err != nil { + return err + } + systemsOption, err := cmd.Flags().GetString("systems") + if err != nil { + return err + } + reposOption, err := cmd.Flags().GetString("repos") + if err != nil { + return err + } + imagesOption, err := cmd.Flags().GetString("image") + 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 + } + masterOption, err := cmd.Flags().GetString("master") + if err != nil { + return err + } + pruneOption, err := cmd.Flags().GetBool("prune") + if err != nil { + return err + } + omitDataOption, err := cmd.Flags().GetBool("omit-data") + if err != nil { + return err + } + syncAllOption, err := cmd.Flags().GetBool("sync-all") + if err != nil { + return err + } + useSslOption, err := cmd.Flags().GetBool("use-ssl") + if err != nil { + 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, + } + eventId, err := Client.BackgroundReplicate(replicateOptions) + if err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "Event ID: %s\n", eventId) + return nil + }, + } 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") @@ -121,4 +121,5 @@ func init() { replicateCmd.Flags().Bool("sync-all", false, "sync all data") replicateCmd.Flags().String("systems", "", "patterns of systems to replicate") replicateCmd.Flags().Bool("use-ssl", false, "use SSL to access the Cobbler master server API") + return replicateCmd } diff --git a/cmd/replicate_test.go b/cmd/replicate_test.go new file mode 100644 index 0000000..2992646 --- /dev/null +++ b/cmd/replicate_test.go @@ -0,0 +1,58 @@ +package cmd + +import ( + "bytes" + "fmt" + "github.com/cobbler/cobblerclient" + "github.com/spf13/cobra" + "io" + "strings" + "testing" +) + +func Test_ReplicateCmd(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", "replicate"}}, + want: "Event ID:", + 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 + 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, tt.want) { + fmt.Println(stdoutString) + t.Fatal("No Event ID present") + } + }) + } +} diff --git a/cmd/repo.go b/cmd/repo.go index 59c09af..8a4d698 100644 --- a/cmd/repo.go +++ b/cmd/repo.go @@ -170,254 +170,126 @@ func updateRepoFromFlags(cmd *cobra.Command, repo *cobbler.Repo) error { return err } -// repoCmd represents the repo command -var repoCmd = &cobra.Command{ - Use: "repo", - Short: "Repository management", - Long: `Let you manage repositories. +// NewRepoCmd builds a new command that represents the repo action +func NewRepoCmd() *cobra.Command { + repoCmd := &cobra.Command{ + Use: "repo", + Short: "Repository management", + Long: `Let you manage repositories. See https://cobbler.readthedocs.io/en/latest/cobbler.html#cobbler-repo for more information.`, - Run: func(cmd *cobra.Command, args []string) { - _ = cmd.Help() - }, -} - -var repoAddCmd = &cobra.Command{ - Use: "add", - Short: "add repository", - Long: `Adds a repository.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - newRepo := cobbler.NewRepo() - var err error - - // internal fields (ctime, mtime, depth, uid, parent, tree-build-time) cannot be modified - newRepo.Name, err = cmd.Flags().GetString("name") - if err != nil { - return err - } - // Update repo in-memory - err = updateRepoFromFlags(cmd, &newRepo) - if err != nil { - return err - } - // Now create via XML-RPC - repo, err := Client.CreateRepo(newRepo) - if err != nil { - return err - } - fmt.Printf("Repo %s created\n", repo.Name) - return nil - }, -} - -var repoAutoAddCmd = &cobra.Command{ - Use: "autoadd", - Short: "add repository automatically", - Long: `Automatically adds a repository.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - return Client.AutoAddRepos() - }, -} - -var repoCopyCmd = &cobra.Command{ - Use: "copy", - Short: "copy repository", - Long: `Copies a repository.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - repoName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - repoNewName, err := cmd.Flags().GetString("newname") - if err != nil { - return err - } - - repoHandle, err := Client.GetRepoHandle(repoName) - if err != nil { - return err - } - err = Client.CopyRepo(repoHandle, repoNewName) - if err != nil { - return err - } - copiedRepo, err := Client.GetRepo(repoNewName, false, false) - if err != nil { - return err - } - err = updateRepoFromFlags(cmd, copiedRepo) - if err != nil { - return err - } - return Client.UpdateRepo(copiedRepo) - }, -} - -var repoEditCmd = &cobra.Command{ - Use: "edit", - Short: "edit repository", - Long: `Edits a repository.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - // find repo through its name - rname, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - // Get repo from API - updateRepo, err := Client.GetRepo(rname, false, false) - if err != nil { - return err - } - // Update repo in-memory - err = updateRepoFromFlags(cmd, updateRepo) - if err != nil { - return err - } - // Update repo via XML-RPC - return Client.UpdateRepo(updateRepo) - }, -} - -var repoFindCmd = &cobra.Command{ - Use: "find", - Short: "find repository", - Long: `Finds a given repository.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - return FindItemNames(cmd, args, "repo") - }, -} - -var repoListCmd = &cobra.Command{ - Use: "list", - Short: "list all repositorys", - Long: `Lists all available repositories.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - repoNames, err := Client.ListRepoNames() - if err != nil { - return err - } - listItems("repos", repoNames) - return nil - }, -} - -var repoRemoveCmd = &cobra.Command{ - Use: "remove", - Short: "remove repository", - Long: `Removes a given repository.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - return RemoveItemRecursive(cmd, args, "repo") - }, -} - -var repoRenameCmd = &cobra.Command{ - Use: "rename", - Short: "rename repository", - Long: `Renames a given repository.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - // Get special name and newname flags - repoName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - repoNewName, err := cmd.Flags().GetString("newname") - if err != nil { - return err - } - // Get repo handle from the API - repoHandle, err := Client.GetMenuHandle(repoName) - if err != nil { - return err - } - // Rename the repo server side - err = Client.RenameRepo(repoHandle, repoNewName) - if err != nil { - return err - } - // Get the renamed repository from the API - newRepository, err := Client.GetRepo(repoNewName, false, false) - if err != nil { - return err - } - // Update the repo in-memory - err = updateRepoFromFlags(cmd, newRepository) - if err != nil { - return err - } - // Update the repo via XML-RPC - return Client.UpdateRepo(newRepository) - }, -} - -func reportRepos(repoNames []string) error { - for _, itemName := range repoNames { - repo, err := Client.GetRepo(itemName, false, false) - if err != nil { - return err - } - printStructured(repo) - fmt.Println("") + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, } - return nil + repoCmd.AddCommand(NewRepoAddCmd()) + repoCmd.AddCommand(NewRepoAutoAddCmd()) + repoCmd.AddCommand(NewRepoCopyCmd()) + repoCmd.AddCommand(NewRepoEditCmd()) + repoCmd.AddCommand(NewRepoFindCmd()) + repoCmd.AddCommand(NewRepoListCmd()) + repoCmd.AddCommand(NewRepoRemoveCmd()) + repoCmd.AddCommand(NewRepoRenameCmd()) + repoCmd.AddCommand(NewRepoReportCmd()) + return repoCmd } -var repoReportCmd = &cobra.Command{ - Use: "report", - Short: "list all repositorys in detail", - Long: `Shows detailed information about all repositories.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - name, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - itemNames := make([]string, 0) - if name == "" { - itemNames, err = Client.ListRepoNames() +func NewRepoAddCmd() *cobra.Command { + repoAddCmd := &cobra.Command{ + Use: "add", + Short: "add repository", + Long: `Adds a repository.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() if err != nil { return err } - } else { - itemNames = append(itemNames, name) - } - return reportRepos(itemNames) - }, -} -func init() { - rootCmd.AddCommand(repoCmd) - repoCmd.AddCommand(repoAddCmd) - repoCmd.AddCommand(repoAutoAddCmd) - repoCmd.AddCommand(repoCopyCmd) - repoCmd.AddCommand(repoEditCmd) - repoCmd.AddCommand(repoFindCmd) - repoCmd.AddCommand(repoListCmd) - repoCmd.AddCommand(repoRemoveCmd) - repoCmd.AddCommand(repoRenameCmd) - repoCmd.AddCommand(repoReportCmd) + newRepo := cobbler.NewRepo() - // local flags for repo add + // internal fields (ctime, mtime, depth, uid, parent, tree-build-time) cannot be modified + newRepo.Name, err = cmd.Flags().GetString("name") + if err != nil { + return err + } + // Update repo in-memory + err = updateRepoFromFlags(cmd, &newRepo) + if err != nil { + return err + } + // Now create via XML-RPC + repo, err := Client.CreateRepo(newRepo) + if err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "Repo %s created\n", repo.Name) + return nil + }, + } addCommonArgs(repoAddCmd) addStringFlags(repoAddCmd, repoStringFlagMetadata) addBoolFlags(repoAddCmd, repoBoolFlagMetadata) addIntFlags(repoAddCmd, repoIntFlagMetadata) addStringSliceFlags(repoAddCmd, repoStringSliceFlagMetadata) addMapFlags(repoAddCmd, repoMapFlagMetadata) + return repoAddCmd +} - // local flags for repo autoadd - // no flags +func NewRepoAutoAddCmd() *cobra.Command { + repoAutoAddCmd := &cobra.Command{ + Use: "autoadd", + Short: "add repository automatically", + Long: `Automatically adds a repository.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + return Client.AutoAddRepos() + }, + } + return repoAutoAddCmd +} - // local flags for repo copy +func NewRepoCopyCmd() *cobra.Command { + repoCopyCmd := &cobra.Command{ + Use: "copy", + Short: "copy repository", + Long: `Copies a repository.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + repoName, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + repoNewName, err := cmd.Flags().GetString("newname") + if err != nil { + return err + } + + repoHandle, err := Client.GetRepoHandle(repoName) + if err != nil { + return err + } + err = Client.CopyRepo(repoHandle, repoNewName) + if err != nil { + return err + } + copiedRepo, err := Client.GetRepo(repoNewName, false, false) + if err != nil { + return err + } + err = updateRepoFromFlags(cmd, copiedRepo) + if err != nil { + return err + } + return Client.UpdateRepo(copiedRepo) + }, + } addCommonArgs(repoCopyCmd) addStringFlags(repoCopyCmd, repoStringFlagMetadata) addBoolFlags(repoCopyCmd, repoBoolFlagMetadata) @@ -426,8 +298,39 @@ func init() { addMapFlags(repoCopyCmd, repoMapFlagMetadata) repoCopyCmd.Flags().String("newname", "", "the new repo name") repoCopyCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") + return repoCopyCmd +} + +func NewRepoEditCmd() *cobra.Command { + repoEditCmd := &cobra.Command{ + Use: "edit", + Short: "edit repository", + Long: `Edits a repository.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - // local flags for repo edit + // find repo through its name + rname, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + // Get repo from API + updateRepo, err := Client.GetRepo(rname, false, false) + if err != nil { + return err + } + // Update repo in-memory + err = updateRepoFromFlags(cmd, updateRepo) + if err != nil { + return err + } + // Update repo via XML-RPC + return Client.UpdateRepo(updateRepo) + }, + } addCommonArgs(repoEditCmd) addStringFlags(repoEditCmd, repoStringFlagMetadata) addBoolFlags(repoEditCmd, repoBoolFlagMetadata) @@ -435,8 +338,23 @@ func init() { addStringSliceFlags(repoEditCmd, repoStringSliceFlagMetadata) addMapFlags(repoEditCmd, repoMapFlagMetadata) repoEditCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") + return repoEditCmd +} - // local flags for repo find +func NewRepoFindCmd() *cobra.Command { + repoFindCmd := &cobra.Command{ + Use: "find", + Short: "find repository", + Long: `Finds a given repository.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + return FindItemNames(cmd, args, "repo") + }, + } addCommonArgs(repoFindCmd) addStringFlags(repoFindCmd, repoStringFlagMetadata) addBoolFlags(repoFindCmd, repoBoolFlagMetadata) @@ -447,12 +365,94 @@ func init() { addIntFlags(repoFindCmd, findIntFlagMetadata) addFloatFlags(repoFindCmd, findFloatFlagMetadata) repoFindCmd.Flags().String("parent", "", "") + return repoFindCmd +} - // local flags for repo remove +func NewRepoListCmd() *cobra.Command { + repoListCmd := &cobra.Command{ + Use: "list", + Short: "list all repositorys", + Long: `Lists all available repositories.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + repoNames, err := Client.ListRepoNames() + if err != nil { + return err + } + listItems(cmd, "repos", repoNames) + return nil + }, + } + return repoListCmd +} + +func NewRepoRemoveCmd() *cobra.Command { + repoRemoveCmd := &cobra.Command{ + Use: "remove", + Short: "remove repository", + Long: `Removes a given repository.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + return RemoveItemRecursive(cmd, args, "repo") + }, + } repoRemoveCmd.Flags().String("name", "", "the repo name") repoRemoveCmd.Flags().Bool("recursive", false, "also delete child objects") + return repoRemoveCmd +} - // local flags for repo rename +func NewRepoRenameCmd() *cobra.Command { + repoRenameCmd := &cobra.Command{ + Use: "rename", + Short: "rename repository", + Long: `Renames a given repository.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + // Get special name and newname flags + repoName, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + repoNewName, err := cmd.Flags().GetString("newname") + if err != nil { + return err + } + // Get repo handle from the API + repoHandle, err := Client.GetRepoHandle(repoName) + if err != nil { + return err + } + // Rename the repo server side + err = Client.RenameRepo(repoHandle, repoNewName) + if err != nil { + return err + } + // Get the renamed repository from the API + newRepository, err := Client.GetRepo(repoNewName, false, false) + if err != nil { + return err + } + // Update the repo in-memory + err = updateRepoFromFlags(cmd, newRepository) + if err != nil { + return err + } + // Update the repo via XML-RPC + return Client.UpdateRepo(newRepository) + }, + } addCommonArgs(repoRenameCmd) addStringFlags(repoRenameCmd, repoStringFlagMetadata) addBoolFlags(repoRenameCmd, repoBoolFlagMetadata) @@ -461,7 +461,48 @@ func init() { addMapFlags(repoRenameCmd, repoMapFlagMetadata) repoRenameCmd.Flags().String("newname", "", "the new repo name") repoRenameCmd.Flags().Bool("in-place", false, "edit items in kopts or autoinstall without clearing the other items") + return repoRenameCmd +} + +func reportRepos(cmd *cobra.Command, repoNames []string) error { + for _, itemName := range repoNames { + repo, err := Client.GetRepo(itemName, false, false) + if err != nil { + return err + } + printStructured(cmd, repo) + fmt.Fprintln(cmd.OutOrStdout(), "") + } + return nil +} - // local flags for repo report +func NewRepoReportCmd() *cobra.Command { + repoReportCmd := &cobra.Command{ + Use: "report", + Short: "list all repositorys in detail", + Long: `Shows detailed information about all repositories.`, + 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 reportRepos(cmd, itemNames) + }, + } repoReportCmd.Flags().String("name", "", "the repo name") + return repoReportCmd } diff --git a/cmd/repo_test.go b/cmd/repo_test.go new file mode 100644 index 0000000..f767d94 --- /dev/null +++ b/cmd/repo_test.go @@ -0,0 +1,454 @@ +package cmd + +import ( + "bytes" + "fmt" + cobbler "github.com/cobbler/cobblerclient" + "github.com/spf13/cobra" + "io" + "strings" + "testing" +) + +func createRepo(client cobbler.Client, name string) (*cobbler.Repo, error) { + repo := cobbler.NewRepo() + repo.Name = name + return client.CreateRepo(repo) +} + +func removeRepo(client cobbler.Client, name string) error { + return client.DeleteRepo(name) +} + +func Test_RepoAddCmd(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", "repo", "add", "--name", "test-plain"}}, + want: "Repo 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 := removeRepo(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_RepoCopyCmd(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", "repo", "copy", "--name", "repo-to-copy", "--newname", "copied-repo"}}, + 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 := removeRepo(Client, tt.args.command[5]) + cobbler.FailOnError(t, cleanupErr) + cleanupErr = removeRepo(Client, tt.args.command[7]) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createRepo(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.GetRepo(tt.args.command[7], false, false) + cobbler.FailOnError(t, err) + }) + } +} + +func Test_RepoEditCmd(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", "repo", "edit", "--name", "test-repo-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 := removeRepo(Client, tt.args.command[5]) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createRepo(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) + updatedRepo, err := Client.GetRepo(tt.args.command[5], false, false) + cobbler.FailOnError(t, err) + if updatedRepo.Comment != "testcomment" { + t.Fatal("repo update wasn't successful") + } + }) + } +} + +func Test_RepoFindCmd(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", "repo", "find", "--name", "test-repo-find"}}, + want: "Event ID:", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Cleanup + repoName := "test-repo-find" + var err error + defer func() { + // Client is initialized since this is the cleanup + cleanupErr := removeRepo(Client, repoName) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createRepo(Client, repoName) + 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, repoName) { + fmt.Println(stdoutString) + t.Fatal("repo not successfully found") + } + }) + } +} + +func Test_RepoListCmd(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", "repo", "list"}}, + want: "repos:", + 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("repo list marker not located in output") + } + }) + } +} + +func Test_RepoRemoveCmd(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", "repo", "remove", "--name", "test-repo-remove"}}, + want: "Event ID:", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Arrange + setupClient(t) + _, err := createRepo(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("repo", 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("repo not successfully removed") + } + }) + } +} + +func Test_RepoRenameCmd(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", "repo", "rename", "--name", "test-repo-rename", "--newname", "test-repo-renamed"}}, + want: "Event ID:", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Cleanup + repoName := "test-repo-rename" + newRepoName := "test-repo-renamed" + var err error + defer func() { + // Client is initialized since this is the cleanup + cleanupErr := removeRepo(Client, newRepoName) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createRepo(Client, repoName) + 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("repo", repoName) + cobbler.FailOnError(t, err) + if resultOldName { + t.Fatal("repo not successfully renamed (old name present)") + } + resultNewName, err := Client.HasItem("repo", newRepoName) + cobbler.FailOnError(t, err) + if !resultNewName { + t.Fatal("repo not successfully renamed (new name not present)") + } + }) + } +} + +func Test_RepoReportCmd(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", "repo", "report", "--name", "test-repo-report"}}, + want: ": test-repo-report", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Cleanup + repoName := "test-repo-report" + var err error + defer func() { + // Client is initialized since this is the cleanup + cleanupErr := removeRepo(Client, repoName) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createRepo(Client, repoName) + 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/report.go b/cmd/report.go index f404642..a54355f 100644 --- a/cmd/report.go +++ b/cmd/report.go @@ -9,136 +9,139 @@ import ( "github.com/spf13/cobra" ) -// reportCmd represents the report command -var reportCmd = &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 +// NewReportCmd builds a new command that represents the report action +func NewReportCmd() *cobra.Command { + reportCmd := &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). Identical to 'cobbler list'`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - // Distro - fmt.Println("distros:") - fmt.Println("==========") - distroNames, err := Client.ListDistroNames() - if err != nil { - return err - } - err = reportDistros(distroNames) - if err != nil { - return err - } - fmt.Println("") + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - // Profile - fmt.Println("profiles:") - fmt.Println("==========") - profileNames, err := Client.ListProfileNames() - if err != nil { - return err - } - err = reportProfiles(profileNames) - if err != nil { - return err - } - fmt.Println("") + // Distro + fmt.Fprintln(cmd.OutOrStdout(), "distros:") + fmt.Fprintln(cmd.OutOrStdout(), "==========") + distroNames, err := Client.ListDistroNames() + if err != nil { + return err + } + err = reportDistros(cmd, distroNames) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), "") - // System - fmt.Println("systems:") - fmt.Println("==========") - systemNames, err := Client.ListSystemNames() - if err != nil { - return err - } - err = reportSystems(systemNames) - if err != nil { - return err - } - fmt.Println("") + // Profile + fmt.Fprintln(cmd.OutOrStdout(), "profiles:") + fmt.Fprintln(cmd.OutOrStdout(), "==========") + profileNames, err := Client.ListProfileNames() + if err != nil { + return err + } + err = reportProfiles(cmd, profileNames) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), "") - // Repository - fmt.Println("repos:") - fmt.Println("==========") - repoNames, err := Client.ListRepoNames() - if err != nil { - return err - } - err = reportRepos(repoNames) - if err != nil { - return err - } - fmt.Println("") + // System + fmt.Fprintln(cmd.OutOrStdout(), "systems:") + fmt.Fprintln(cmd.OutOrStdout(), "==========") + systemNames, err := Client.ListSystemNames() + if err != nil { + return err + } + err = reportSystems(cmd, systemNames) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), "") - // Image - fmt.Println("images:") - fmt.Println("==========") - imageNames, err := Client.ListImageNames() - if err != nil { - return err - } - err = reportImages(imageNames) - if err != nil { - return err - } - fmt.Println("") + // Repository + fmt.Fprintln(cmd.OutOrStdout(), "repos:") + fmt.Fprintln(cmd.OutOrStdout(), "==========") + repoNames, err := Client.ListRepoNames() + if err != nil { + return err + } + err = reportRepos(cmd, repoNames) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), "") - // Mgmtclass - fmt.Println("mgmtclasses:") - fmt.Println("==========") - mgmtClassNames, err := Client.ListMgmtClassNames() - if err != nil { - return err - } - err = reportMgmtClasses(mgmtClassNames) - if err != nil { - return err - } - fmt.Println("") + // Image + fmt.Fprintln(cmd.OutOrStdout(), "images:") + fmt.Fprintln(cmd.OutOrStdout(), "==========") + imageNames, err := Client.ListImageNames() + if err != nil { + return err + } + err = reportImages(cmd, imageNames) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), "") - // Package - fmt.Println("packages:") - fmt.Println("==========") - packageNames, err := Client.ListPackageNames() - if err != nil { - return err - } - err = reportPackages(packageNames) - if err != nil { - return err - } - fmt.Println("") + // 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(), "") - // File - fmt.Println("files:") - fmt.Println("==========") - fileNames, err := Client.ListFileNames() - if err != nil { - return err - } - err = reportFiles(fileNames) - if err != nil { - return err - } - fmt.Println("") + // 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(), "") - // Menu - fmt.Println("menus:") - fmt.Println("==========") - menuNames, err := Client.ListMenuNames() - if err != nil { - return err - } - err = reportMenus(menuNames) - if err != nil { - return err - } - fmt.Println("") - return nil - }, -} + // 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(), "") -func init() { - rootCmd.AddCommand(reportCmd) + // Menu + fmt.Fprintln(cmd.OutOrStdout(), "menus:") + fmt.Fprintln(cmd.OutOrStdout(), "==========") + menuNames, err := Client.ListMenuNames() + if err != nil { + return err + } + err = reportMenus(cmd, menuNames) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), "") + return nil + }, + } + return reportCmd } diff --git a/cmd/report_test.go b/cmd/report_test.go new file mode 100644 index 0000000..0ef2bb3 --- /dev/null +++ b/cmd/report_test.go @@ -0,0 +1,38 @@ +package cmd + +import ( + "bytes" + "fmt" + "github.com/cobbler/cobblerclient" + "github.com/spf13/cobra" + "io" + "strings" + "testing" +) + +func Test_ReportCmd(t *testing.T) { + // Arrange + cobra.OnInitialize(initConfig, setupLogger) + rootCmd := NewRootCmd() + rootCmd.SetArgs([]string{"--config", "../testing/.cobbler.yaml", "report"}) + 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, "distros:") && strings.Contains(stdoutString, "profiles")) { + fmt.Println(stdoutString) + t.Fatal("no heading for distros and profiles present") + } +} diff --git a/cmd/reposync.go b/cmd/reposync.go index 1cef32a..a67b6c1 100644 --- a/cmd/reposync.go +++ b/cmd/reposync.go @@ -10,48 +10,49 @@ import ( "github.com/spf13/cobra" ) -// reposyncCmd represents the reposync command -var reposyncCmd = &cobra.Command{ - Use: "reposync", - Short: "Sync repositories", - Long: `Update and sync Cobbler repositories. The repositories have to be added beforehand via 'cobbler repo add'. +// NewRepoSyncCmd builds a new command that represents the reposync action +func NewRepoSyncCmd() *cobra.Command { + reposyncCmd := &cobra.Command{ + Use: "reposync", + Short: "Sync repositories", + Long: `Update and sync Cobbler repositories. The repositories have to be added beforehand via 'cobbler repo add'. See https://cobbler.readthedocs.io/en/latest/cobbler.html#cobbler-reposync for more information.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - noFailOption, err := cmd.Flags().GetBool("no-fail") - if err != nil { - return err - } - onlyOption, err := cmd.Flags().GetString("only") - if err != nil { - return err - } - triesOption, err := cmd.Flags().GetInt("tries") - if err != nil { - return err - } - var reposyncOptions = cobblerclient.BackgroundReposyncOptions{ - Repos: make([]string, 0), - Only: onlyOption, - Nofail: noFailOption, - Tries: triesOption, - } - eventId, err := Client.BackgroundReposync(reposyncOptions) - if err != nil { - return err - } - fmt.Printf("Event ID: %s\n", eventId) - return nil - }, -} - -func init() { - rootCmd.AddCommand(reposyncCmd) + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - //local flags + noFailOption, err := cmd.Flags().GetBool("no-fail") + if err != nil { + return err + } + onlyOption, err := cmd.Flags().GetString("only") + if err != nil { + return err + } + triesOption, err := cmd.Flags().GetInt("tries") + if err != nil { + return err + } + var reposyncOptions = cobblerclient.BackgroundReposyncOptions{ + Repos: make([]string, 0), + Only: onlyOption, + Nofail: noFailOption, + Tries: triesOption, + } + eventId, err := Client.BackgroundReposync(reposyncOptions) + if err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "Event ID: %s\n", eventId) + return nil + }, + } reposyncCmd.Flags().Bool("no-fail", false, "do not stop reposyncing if a failure occurs") reposyncCmd.Flags().String("only", "", "update only this repository name") reposyncCmd.Flags().Int("tries", 3, "try each repo this many times") + return reposyncCmd } diff --git a/cmd/reposync_test.go b/cmd/reposync_test.go new file mode 100644 index 0000000..2e1bdeb --- /dev/null +++ b/cmd/reposync_test.go @@ -0,0 +1,70 @@ +package cmd + +import ( + "bytes" + "fmt" + "github.com/cobbler/cobblerclient" + "github.com/spf13/cobra" + "io" + "strings" + "testing" +) + +func Test_ReposyncCmd(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", "reposync"}}, + want: "Event ID:", + wantErr: false, + }, + { + name: "tries", + args: args{command: []string{"--config", "../testing/.cobbler.yaml", "reposync", "--tries", "3"}}, + want: "Event ID:", + wantErr: false, + }, + { + name: "nofail", + args: args{command: []string{"--config", "../testing/.cobbler.yaml", "reposync", "--no-fail"}}, + want: "Event ID:", + 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 + 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, tt.want) { + fmt.Println(stdoutString) + t.Fatal("No Event ID present") + } + }) + } +} diff --git a/cmd/root.go b/cmd/root.go index 0a58a66..509dc47 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -24,30 +24,64 @@ var conf cobbler.ClientConfig var httpClient = &http.Client{} var verbose bool -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ - Use: "cobbler", - Short: "Cobbler CLI client", - Long: "An independent CLI to manage a Cobbler server.", - Run: func(cmd *cobra.Command, args []string) { - _ = cmd.Help() - }, +// NewRootCmd builds a new command that represents the base action when called without any subcommands +func NewRootCmd() *cobra.Command { + rootCmd := &cobra.Command{ + Use: "cobbler", + Short: "Cobbler CLI client", + Long: "An independent CLI to manage a Cobbler server.", + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, + } + + // global flags + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobbler.yaml)") + rootCmd.PersistentFlags().BoolVar(&verbose, "verbose", false, "Whether or not to print debug messages from the CLI.") + + // Add sub commands + rootCmd.AddCommand(NewAclSetupCmd()) + rootCmd.AddCommand(NewBuildisoCmd()) + distroCmd, err := NewDistroCmd() + cobra.CheckErr(err) + rootCmd.AddCommand(distroCmd) + rootCmd.AddCommand(NewEventCmd()) + rootCmd.AddCommand(NewFileCmd()) + rootCmd.AddCommand(NewHardlinkCmd()) + rootCmd.AddCommand(NewImageCmd()) + rootCmd.AddCommand(NewImportCmd()) + rootCmd.AddCommand(NewListCmd()) + menuCmd, err := NewMenuCmd() + cobra.CheckErr(err) + rootCmd.AddCommand(menuCmd) + rootCmd.AddCommand(NewMgmtClassCmd()) + rootCmd.AddCommand(NewMkLoadersCmd()) + rootCmd.AddCommand(NewPackageCmd()) + rootCmd.AddCommand(NewProfileCmd()) + rootCmd.AddCommand(NewReplicateCmd()) + rootCmd.AddCommand(NewRepoCmd()) + rootCmd.AddCommand(NewReportCmd()) + rootCmd.AddCommand(NewRepoSyncCmd()) + rootCmd.AddCommand(NewSettingCmd()) + rootCmd.AddCommand(NewSignatureCmd()) + rootCmd.AddCommand(NewSyncCmd()) + rootCmd.AddCommand(NewSystemCmd()) + rootCmd.AddCommand(NewValidateAutoinstallsCmd()) + rootCmd.AddCommand(NewVersionCmd()) + return rootCmd } // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. func Execute() { + cobra.OnInitialize(initConfig, setupLogger) + rootCmd := NewRootCmd() + + // Execute root command cobra.CheckErr(rootCmd.Execute()) } -func init() { - cobra.OnInitialize(initConfig) - - // global flags - rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobbler.yaml)") - rootCmd.Flags().BoolVar(&verbose, "verbose", false, "Whether or not to print debug messages from the CLI.") - - // Setup logger +func setupLogger() { if !verbose { slog.SetLogLoggerLevel(slog.LevelWarn) } @@ -78,15 +112,17 @@ func initConfig() { viper.AutomaticEnv() // read in environment variables that match // If a config file is found, read it in. - if err := viper.ReadInConfig(); err == nil { - if verbose { - _, _ = fmt.Fprintln(os.Stdout, "Using config file:", viper.ConfigFileUsed()) - } + err := viper.ReadInConfig() + if cfgFile != "" { + cobra.CheckErr(err) + } + if verbose { + _, _ = fmt.Fprintln(os.Stdout, "Using config file:", viper.ConfigFileUsed()) } } // basic connection to the Cobbler server -func generateCobblerClient() { +func generateCobblerClient() error { // the configuration is done in .cobbler.yaml conf.URL = viper.GetString("server_url") @@ -96,12 +132,13 @@ func generateCobblerClient() { Client = cobbler.NewClient(httpClient, conf) login, err := Client.Login() - if !login || err != nil { - _, _ = fmt.Fprintln(os.Stderr, fmt.Errorf("error! Failed to login: %s", err)) + if !login { + return fmt.Errorf("failed to login") } + return err } -func printStructured(dataStruct interface{}) { +func printStructured(cmd *cobra.Command, dataStruct interface{}) { s := reflect.ValueOf(dataStruct).Elem() typeOfT := s.Type() @@ -111,12 +148,17 @@ func printStructured(dataStruct interface{}) { fieldName := typeOfT.Field(i).Name fieldStructName := typeOfT.Field(i).Type.String() if strings.HasPrefix(fieldStructName, "cobblerclient.Value") { - printValueStructured(mapstructureTag, f) + printValueStructured(cmd, mapstructureTag, f) continue } if fieldName == "Item" { baseItem := f.Interface().(cobbler.Item) - printStructured(&baseItem) + printStructured(cmd, &baseItem) + continue + } + if fieldName == "Resource" { + baseResource := f.Interface().(cobbler.Resource) + printStructured(cmd, &baseResource) continue } if fieldName == "Interfaces" { @@ -129,80 +171,80 @@ func printStructured(dataStruct interface{}) { if fieldName == "Meta" { continue } - printField(f.Kind(), mapstructureTag, f.Interface()) + printField(cmd, f.Kind(), mapstructureTag, f.Interface()) } // Print interfaces at the end of the output networkInterfacesField := s.FieldByName("Interfaces") if networkInterfacesField != (reflect.Value{}) { networkInterfaces := networkInterfacesField.Interface().(cobbler.Interfaces) - printNetworkInterface(networkInterfaces) + printNetworkInterface(cmd, networkInterfaces) } } -func printValueStructured(name string, value reflect.Value) { +func printValueStructured(cmd *cobra.Command, name string, value reflect.Value) { isInherited := value.FieldByName("IsInherited").Bool() data := value.FieldByName("Data").Interface() if isInherited { - printField(reflect.String, name, "<>") + printField(cmd, reflect.String, name, "<>") } else { dataType := value.FieldByName("Data").Kind() - printField(dataType, name, data) + printField(cmd, dataType, name, data) } } -func printNetworkInterface(networkInterface cobbler.Interfaces) { +func printNetworkInterface(cmd *cobra.Command, networkInterface cobbler.Interfaces) { for interfaceName, interfaceStruct := range networkInterface { - fmt.Printf("%-40s: %s\n", "Interface =====", interfaceName) - printStructured(&interfaceStruct) + fmt.Fprintf(cmd.OutOrStdout(), "%-40s: %s\n", "Interface =====", interfaceName) + printStructured(cmd, &interfaceStruct) } } -func printField(valueType reflect.Kind, name string, value interface{}) { +func printField(cmd *cobra.Command, valueType reflect.Kind, name string, value interface{}) { if name == "ctime" || name == "mtime" { time, err := covertFloatToUtcTime(value.(float64)) if err == nil { // If there is an error just show the float - fmt.Printf("%-40s: %s\n", name, time) + fmt.Fprintf(cmd.OutOrStdout(), "%-40s: %s\n", name, time) return } } switch valueType { case reflect.Bool: - fmt.Printf("%-40s: %t\n", name, value.(bool)) + fmt.Fprintf(cmd.OutOrStdout(), "%-40s: %t\n", name, value.(bool)) case reflect.Int64: - fmt.Printf("%-40s: %d\n", name, value.(int64)) + fmt.Fprintf(cmd.OutOrStdout(), "%-40s: %d\n", name, value.(int64)) case reflect.Int32: - fmt.Printf("%-40s: %d\n", name, value.(int32)) + fmt.Fprintf(cmd.OutOrStdout(), "%-40s: %d\n", name, value.(int32)) case reflect.Int16: - fmt.Printf("%-40s: %d\n", name, value.(int16)) + fmt.Fprintf(cmd.OutOrStdout(), "%-40s: %d\n", name, value.(int16)) case reflect.Int8: - fmt.Printf("%-40s: %d\n", name, value.(int8)) + fmt.Fprintf(cmd.OutOrStdout(), "%-40s: %d\n", name, value.(int8)) case reflect.Int: - fmt.Printf("%-40s: %d\n", name, value.(int)) + fmt.Fprintf(cmd.OutOrStdout(), "%-40s: %d\n", name, value.(int)) case reflect.Float32: - fmt.Printf("%-40s: %f\n", name, value.(float32)) + fmt.Fprintf(cmd.OutOrStdout(), "%-40s: %f\n", name, value.(float32)) case reflect.Float64: - fmt.Printf("%-40s: %f\n", name, value.(float64)) + fmt.Fprintf(cmd.OutOrStdout(), "%-40s: %f\n", name, value.(float64)) case reflect.Map: res2B, _ := json.Marshal(value) - fmt.Printf("%-40s: %s\n", name, string(res2B)) + fmt.Fprintf(cmd.OutOrStdout(), "%-40s: %s\n", name, string(res2B)) case reflect.Array, reflect.Slice: arr := reflect.ValueOf(value) - fmt.Printf("%-40s: [", name) + fmt.Fprintf(cmd.OutOrStdout(), "%-40s: [", name) for i := 0; i < arr.Len(); i++ { if i+1 != arr.Len() { - fmt.Printf("'%v', ", arr.Index(i).Interface()) + fmt.Fprintf(cmd.OutOrStdout(), "'%v', ", arr.Index(i).Interface()) } else { - fmt.Printf("'%v'", arr.Index(i).Interface()) + fmt.Fprintf(cmd.OutOrStdout(), "'%v'", arr.Index(i).Interface()) } } - fmt.Printf("]\n") + fmt.Fprintf(cmd.OutOrStdout(), "]\n") default: if value == nil { value = "" } - fmt.Printf("%-40s: %s\n", name, value) - // fmt.Printf("%d: %s %s = %v\n", i, typeOfT.Field(i).Name, f.Type(), f.Interface()) + fmt.Fprintf(cmd.OutOrStdout(), "%-40s: %s\n", name, value) + // fmt.Fprintf(cmd.OutOrStdout(),"%d: %s %s = %v\n", i, typeOfT.Field(i).Name, f.Type(), f.Interface()) } } diff --git a/cmd/setting.go b/cmd/setting.go index 5418b3e..b28d640 100644 --- a/cmd/setting.go +++ b/cmd/setting.go @@ -5,83 +5,90 @@ package cmd import ( + "errors" "fmt" "github.com/spf13/cobra" - "os" ) -// settingCmd represents the setting command -var settingCmd = &cobra.Command{ - Use: "setting", - Short: "Settings management", - Long: `Let you manage settings.`, - Run: func(cmd *cobra.Command, args []string) { - _ = cmd.Help() - }, +// NewSettingCmd builds a new command that represents the setting action +func NewSettingCmd() *cobra.Command { + settingCmd := &cobra.Command{ + Use: "setting", + Short: "Settings management", + Long: `Let you manage settings.`, + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, + } + settingCmd.AddCommand(NewSettingEditCmd()) + settingCmd.AddCommand(NewSettingReportCmd()) + return settingCmd } -var settingEditCmd = &cobra.Command{ - Use: "edit", - Short: "edit settings", - Long: `Edits the settings.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() +func NewSettingEditCmd() *cobra.Command { + settingEditCmd := &cobra.Command{ + Use: "edit", + Short: "edit settings", + Long: `Edits the settings.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - settings, err := Client.GetSettings() - if err != nil { - return err - } - if !settings.AllowDynamicSettings { - fmt.Println("Dynamic settings are turned off server-side!") - os.Exit(1) - } + settings, err := Client.GetSettings() + if err != nil { + return err + } + if !settings.AllowDynamicSettings { + return errors.New("dynamic settings are turned off server-side") + } - settingName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - settingValue, err := cmd.Flags().GetString("value") - if err != nil { - return err - } - result, err := Client.ModifySetting(settingName, settingValue) - if err != nil { - return err - } - if result == 0 { - fmt.Println("Successfully updated!") - } else { - fmt.Println("Updating settings failed!") - } - return nil - }, -} - -var settingReportCmd = &cobra.Command{ - Use: "report", - Short: "list settings", - Long: `Prints settings to stdout.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - settings, err := Client.GetSettings() - if err != nil { - return err - } - - printStructured(settings) - return nil - }, + settingName, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + settingValue, err := cmd.Flags().GetString("value") + if err != nil { + return err + } + result, err := Client.ModifySetting(settingName, settingValue) + if err != nil { + return err + } + if result == 0 { + fmt.Fprintln(cmd.OutOrStdout(), "Successfully updated!") + } else { + fmt.Fprintln(cmd.OutOrStdout(), "Updating settings failed!") + } + return nil + }, + } + 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)") + return settingEditCmd } -func init() { - rootCmd.AddCommand(settingCmd) - settingCmd.AddCommand(settingEditCmd) - settingCmd.AddCommand(settingReportCmd) +func NewSettingReportCmd() *cobra.Command { + settingReportCmd := &cobra.Command{ + Use: "report", + Short: "list settings", + Long: `Prints settings to stdout.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - // local flags for setting edit - 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)") + settings, err := Client.GetSettings() + if err != nil { + return err + } - // local flags for setting report + printStructured(cmd, settings) + return nil + }, + } settingReportCmd.Flags().String("name", "", "the settings name to show") + return settingReportCmd } diff --git a/cmd/setting_test.go b/cmd/setting_test.go new file mode 100644 index 0000000..29744fe --- /dev/null +++ b/cmd/setting_test.go @@ -0,0 +1,60 @@ +package cmd + +import ( + "bytes" + "fmt" + "github.com/cobbler/cobblerclient" + "github.com/spf13/cobra" + "io" + "strings" + "testing" +) + +func Test_SettingEditCmd(t *testing.T) { + // Arrange + cobra.OnInitialize(initConfig, setupLogger) + rootCmd := NewRootCmd() + rootCmd.SetArgs([]string{"--config", "../testing/.cobbler.yaml", "setting", "edit"}) + stdout := bytes.NewBufferString("") + stderr := bytes.NewBufferString("") + rootCmd.SetOut(stdout) + rootCmd.SetErr(stderr) + + // Act + err := rootCmd.Execute() + + // Assert + if err == nil { + t.Fatal("expected error, got none") + } + if err.Error() != "dynamic settings are turned off server-side" { + t.Fatalf("expected dynamic settings are to be turned off server-side, got %s", err.Error()) + } +} + +func Test_SettingReportCmd(t *testing.T) { + // Arrange + cobra.OnInitialize(initConfig, setupLogger) + rootCmd := NewRootCmd() + rootCmd.SetArgs([]string{"--config", "../testing/.cobbler.yaml", "setting", "report"}) + 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, "scm_track_enabled") { + fmt.Println(stdoutString) + t.Fatal("Expected setting couldn't be found") + } +} diff --git a/cmd/signature.go b/cmd/signature.go index 840aad4..49fadc6 100644 --- a/cmd/signature.go +++ b/cmd/signature.go @@ -10,91 +10,111 @@ import ( "sort" ) -// signatureCmd represents the signature command -var signatureCmd = &cobra.Command{ - Use: "signature", - Short: "Signature management", - Long: `Reloads, reports or updates the signatures of the distinct operating system versions.`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("Please use one of the sub commands!") - _ = cmd.Help() - }, +// NewSignatureCmd builds a new command that represents the signature action +func NewSignatureCmd() *cobra.Command { + signatureCmd := &cobra.Command{ + Use: "signature", + Short: "Signature management", + Long: `Reloads, reports or updates the signatures of the distinct operating system versions.`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintln(cmd.OutOrStdout(), "Please use one of the sub commands!") + _ = cmd.Help() + }, + } + signatureCmd.AddCommand(NewSignatureReloadCmd()) + signatureCmd.AddCommand(NewSignatureReportCmd()) + signatureCmd.AddCommand(NewSignatureUpdateCmd()) + return signatureCmd } -var signatureReportCmd = &cobra.Command{ - Use: "report", - Short: "Report the loaded signatures", - Long: `Report the loaded signatures`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() +func NewSignatureReportCmd() *cobra.Command { + signatureReportCmd := &cobra.Command{ + Use: "report", + Short: "Report the loaded signatures", + Long: `Report the loaded signatures`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - // Get signatures - signatures, err := Client.GetSignatures() - if err != nil { - return err - } + // Get signatures + signatures, err := Client.GetSignatures() + if err != nil { + return err + } - if len(signatures.Breeds) > 0 { - // Counters - var totalOsVersions int + if len(signatures.Breeds) > 0 { + // Counters + var totalOsVersions int - // Print signatures - fmt.Println("Currently loaded signatures") - breedNameList := make([]string, 0, len(signatures.Breeds)) - for key := range signatures.Breeds { - breedNameList = append(breedNameList, key) - } - sort.Strings(breedNameList) - for _, breedName := range breedNameList { - fmt.Println(breedName) - totalOsVersions += len(signatures.Breeds[breedName]) - if len(signatures.Breeds[breedName]) > 0 { - osVersionNameList := make([]string, 0, len(signatures.Breeds[breedName])) - for key := range signatures.Breeds[breedName] { - osVersionNameList = append(osVersionNameList, key) - } - sort.Strings(osVersionNameList) - for _, versionName := range osVersionNameList { - fmt.Printf("\t%s\n", versionName) - } - } else { - fmt.Println("\t(none)") + // Print signatures + fmt.Fprintln(cmd.OutOrStdout(), "Currently loaded signatures") + breedNameList := make([]string, 0, len(signatures.Breeds)) + for key := range signatures.Breeds { + breedNameList = append(breedNameList, key) } + sort.Strings(breedNameList) + for _, breedName := range breedNameList { + fmt.Fprintln(cmd.OutOrStdout(), breedName) + totalOsVersions += len(signatures.Breeds[breedName]) + if len(signatures.Breeds[breedName]) > 0 { + osVersionNameList := make([]string, 0, len(signatures.Breeds[breedName])) + for key := range signatures.Breeds[breedName] { + osVersionNameList = append(osVersionNameList, key) + } + sort.Strings(osVersionNameList) + for _, versionName := range osVersionNameList { + fmt.Fprintf(cmd.OutOrStdout(), "\t%s\n", versionName) + } + } else { + fmt.Fprintln(cmd.OutOrStdout(), "\t(none)") + } + } + fmt.Fprintf(cmd.OutOrStdout(), "\n%d breeds with %d total OS versions loaded\n", len(signatures.Breeds), totalOsVersions) + } else { + fmt.Fprintln(cmd.OutOrStdout(), "No breeds found in the signature, a signature update is recommended") } - fmt.Printf("\n%d breeds with %d total OS versions loaded\n", len(signatures.Breeds), totalOsVersions) - } else { - fmt.Println("No breeds found in the signature, a signature update is recommended") - } - return nil - }, -} - -var signatureUpdateCmd = &cobra.Command{ - Use: "update", - Short: "Update the signatures JSON file", - Long: `Retrieve an up-to-date "distro_signatures.json" file from the server-side configured webservice.`, - Run: func(cmd *cobra.Command, args []string) { - generateCobblerClient() - eventId, _ := Client.BackgroundSignatureUpdate() - fmt.Printf("Event ID: %s\n", eventId) - }, + return nil + }, + } + return signatureReportCmd } -var signatureReloadCmd = &cobra.Command{ - Use: "reload", - Short: "Reloads signatures", - Long: `Reloads signatures from the - on the server - local "distro_signatures.json" file.`, - Run: func(cmd *cobra.Command, args []string) { - generateCobblerClient() +func NewSignatureUpdateCmd() *cobra.Command { + signatureUpdateCmd := &cobra.Command{ + Use: "update", + Short: "Update the signatures JSON file", + Long: `Retrieve an up-to-date "distro_signatures.json" file from the server-side configured webservice.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - fmt.Println("This functionality cannot be used in the new CLI until https://github.com/cobbler/cobbler/issues/3791 is implemented!") - }, + eventId, _ := Client.BackgroundSignatureUpdate() + fmt.Fprintf(cmd.OutOrStdout(), "Event ID: %s\n", eventId) + return nil + }, + } + return signatureUpdateCmd } -func init() { - signatureCmd.AddCommand(signatureReloadCmd) - signatureCmd.AddCommand(signatureReportCmd) - signatureCmd.AddCommand(signatureUpdateCmd) - rootCmd.AddCommand(signatureCmd) +func NewSignatureReloadCmd() *cobra.Command { + signatureReloadCmd := &cobra.Command{ + Use: "reload", + Short: "Reloads signatures", + Long: `Reloads signatures from the - on the server - local "distro_signatures.json" file.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + fmt.Fprintln(cmd.OutOrStdout(), "This functionality cannot be used in the new CLI until https://github.com/cobbler/cobbler/issues/3791 is implemented!") + return nil + }, + } + return signatureReloadCmd } diff --git a/cmd/signature_test.go b/cmd/signature_test.go new file mode 100644 index 0000000..fbd07eb --- /dev/null +++ b/cmd/signature_test.go @@ -0,0 +1,92 @@ +package cmd + +import ( + "bytes" + "fmt" + "github.com/cobbler/cobblerclient" + "github.com/spf13/cobra" + "io" + "strings" + "testing" +) + +func Test_SignatureReloadCmd(t *testing.T) { + // Arrange + cobra.OnInitialize(initConfig, setupLogger) + rootCmd := NewRootCmd() + rootCmd.SetArgs([]string{"--config", "../testing/.cobbler.yaml", "signature", "reload"}) + 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, "This functionality cannot be used in the new CLI") { + fmt.Println(stdoutString) + t.Fatal("No missing feature message present") + } +} + +func Test_SignatureReportCmd(t *testing.T) { + // Arrange + cobra.OnInitialize(initConfig, setupLogger) + rootCmd := NewRootCmd() + rootCmd.SetArgs([]string{"--config", "../testing/.cobbler.yaml", "signature", "report"}) + 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, "Currently loaded signatures") { + fmt.Println(stdoutString) + t.Fatal("No report header present") + } +} + +func Test_SignatureUpdateCmd(t *testing.T) { + // Arrange + cobra.OnInitialize(initConfig, setupLogger) + rootCmd := NewRootCmd() + rootCmd.SetArgs([]string{"--config", "../testing/.cobbler.yaml", "signature", "update"}) + 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, "Event ID:") { + fmt.Println(stdoutString) + t.Fatal("No Event ID present") + } +} diff --git a/cmd/sync.go b/cmd/sync.go index 9f7ce54..65a45b6 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -10,66 +10,67 @@ import ( "github.com/spf13/cobra" ) -// syncCmd represents the sync command -var syncCmd = &cobra.Command{ - Use: "sync", - Short: "Sync Cobbler", - Long: `Force a rewrite of all configuration files, distribution files in the TFTP root, and restart managed +// NewSyncCmd builds a command that represents the sync action +func NewSyncCmd() *cobra.Command { + syncCmd := &cobra.Command{ + Use: "sync", + Short: "Sync Cobbler", + Long: `Force a rewrite of all configuration files, distribution files in the TFTP root, and restart managed services. It is used to repair or rebuild the contents of '/tftpboot' or '/var/www/cobbler' or when something has changed behind the scenes. It brings the filesystem up to date with the configuration. See https://cobbler.readthedocs.io/en/latest/cobbler.html#cobbler-sync for more information.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - dhcpOption, err := cmd.Flags().GetBool("dhcp") - if err != nil { - return err - } - dnsOption, err := cmd.Flags().GetBool("dns") - if err != nil { - return err - } - verboseOption, err := cmd.Flags().GetBool("verbose") - if err != nil { - return err - } - systemsOption, err := cmd.Flags().GetStringSlice("systems") - if err != nil { - return err - } + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - var eventId string - if len(systemsOption) > 0 { - backgroundSyncSystemsOptions := cobblerclient.BackgroundSyncSystemsOptions{ - Systems: systemsOption, - Verbose: verboseOption, + dhcpOption, err := cmd.Flags().GetBool("dhcp") + if err != nil { + return err } - eventId, err = Client.BackgroundSyncSystems(backgroundSyncSystemsOptions) - } else { - backgroundSyncOptions := cobblerclient.BackgroundSyncOptions{ - Dhcp: dhcpOption, - Dns: dnsOption, - Verbose: verboseOption, + dnsOption, err := cmd.Flags().GetBool("dns") + if err != nil { + return err + } + verboseOption, err := cmd.Flags().GetBool("verbose") + if err != nil { + return err + } + systemsOption, err := cmd.Flags().GetStringSlice("systems") + if err != nil { + return err } - eventId, err = Client.BackgroundSync(backgroundSyncOptions) - } - - if err != nil { - return err - } - fmt.Printf("Event ID: %s\n", eventId) - return nil - }, -} -func init() { - rootCmd.AddCommand(syncCmd) + var eventId string + if len(systemsOption) > 0 { + backgroundSyncSystemsOptions := cobblerclient.BackgroundSyncSystemsOptions{ + Systems: systemsOption, + Verbose: verboseOption, + } + eventId, err = Client.BackgroundSyncSystems(backgroundSyncSystemsOptions) + } else { + backgroundSyncOptions := cobblerclient.BackgroundSyncOptions{ + Dhcp: dhcpOption, + Dns: dnsOption, + Verbose: verboseOption, + } + eventId, err = Client.BackgroundSync(backgroundSyncOptions) + } - //local flags + if err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "Event ID: %s\n", eventId) + return nil + }, + } syncCmd.Flags().Bool("dhcp", false, "write DHCP config files and restart service") syncCmd.Flags().Bool("dns", false, "write DNS config files and restart service") syncCmd.Flags().StringSlice("systems", []string{}, "run a sync only on specified systems") syncCmd.Flags().Bool("verbose", false, "more verbose output") syncCmd.MarkFlagsMutuallyExclusive("dhcp", "systems") syncCmd.MarkFlagsMutuallyExclusive("dns", "systems") + return syncCmd } diff --git a/cmd/sync_test.go b/cmd/sync_test.go new file mode 100644 index 0000000..44028be --- /dev/null +++ b/cmd/sync_test.go @@ -0,0 +1,86 @@ +package cmd + +import ( + "bytes" + "fmt" + "github.com/cobbler/cobblerclient" + "github.com/spf13/cobra" + "io" + "strings" + "testing" + "time" +) + +func Test_SyncCmd(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", "sync"}}, + want: "Event ID:", + wantErr: false, + }, + { + name: "dns", + args: args{command: []string{"--config", "../testing/.cobbler.yaml", "sync", "--dns"}}, + want: "Event ID:", + wantErr: false, + }, + { + name: "dhcp", + args: args{command: []string{"--config", "../testing/.cobbler.yaml", "sync", "--dhcp"}}, + want: "Event ID:", + wantErr: false, + }, + { + name: "dhcpdns", + args: args{command: []string{"--config", "../testing/.cobbler.yaml", "sync", "--dns", "--dhcp"}}, + want: "Event ID:", + wantErr: false, + }, + { + name: "systems", + args: args{command: []string{"--config", "../testing/.cobbler.yaml", "sync", "--systems", "a.b.c,a.d.c"}}, + want: "Event ID:", + 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 + 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, tt.want) { + fmt.Println(stdoutString) + t.Fatal("No Event ID present") + } + + // Cleanup - Sleep after each test to let dhcpd restart properly + time.Sleep(1 * time.Second) + }) + } +} diff --git a/cmd/system.go b/cmd/system.go index d228292..f4ff70f 100644 --- a/cmd/system.go +++ b/cmd/system.go @@ -9,7 +9,6 @@ import ( cobbler "github.com/cobbler/cobblerclient" "github.com/spf13/cobra" "github.com/spf13/pflag" - "os" ) func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { @@ -24,8 +23,8 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { deleteInterface := deleteInterfaceFlag != nil && deleteInterfaceFlag.Changed renameInterface := renameInterfaceFlag != nil && renameInterfaceFlag.Changed systemInterface, keyInMap := system.Interfaces[systemNewInterface] - if !keyInMap { - // Interface doesn't exist, so add a new one. + 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] @@ -522,7 +521,7 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { } system.SerialBaudRate = systemNewSerialBaudRate case "bonding-opts": - if renameInterface || deleteInterface { + if renameInterface || deleteInterface || systemNewInterface == "" { return } var systemNewBondingOpts string @@ -532,7 +531,7 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { } systemInterface.BondingOpts = systemNewBondingOpts case "bridge-opts": - if renameInterface || deleteInterface { + if renameInterface || deleteInterface || systemNewInterface == "" { return } var systemNewBridgeOpts string @@ -542,7 +541,7 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { } systemInterface.BridgeOpts = systemNewBridgeOpts case "cnames": - if renameInterface || deleteInterface { + if renameInterface || deleteInterface || systemNewInterface == "" { return } var systemNewCNames []string @@ -552,7 +551,7 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { } systemInterface.CNAMEs = systemNewCNames case "connected-mode": - if renameInterface || deleteInterface { + if renameInterface || deleteInterface || systemNewInterface == "" { return } var systemNewConnectedMode bool @@ -562,7 +561,7 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { } systemInterface.ConnectedMode = systemNewConnectedMode case "dhcp-tag": - if renameInterface || deleteInterface { + if renameInterface || deleteInterface || systemNewInterface == "" { return } var systemNewDhcpTag string @@ -572,7 +571,7 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { } systemInterface.DHCPTag = systemNewDhcpTag case "dns-name": - if renameInterface || deleteInterface { + if renameInterface || deleteInterface || systemNewInterface == "" { return } var systemNewDnsName string @@ -582,7 +581,7 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { } systemInterface.DNSName = systemNewDnsName case "if-gateway": - if renameInterface || deleteInterface { + if renameInterface || deleteInterface || systemNewInterface == "" { return } var systemNewIfGateway string @@ -592,7 +591,7 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { } systemInterface.Gateway = systemNewIfGateway case "interface-master": - if renameInterface || deleteInterface { + if renameInterface || deleteInterface || systemNewInterface == "" { return } var systemNewInterfaceMaster string @@ -602,7 +601,7 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { } systemInterface.InterfaceMaster = systemNewInterfaceMaster case "interface-type": - if renameInterface || deleteInterface { + if renameInterface || deleteInterface || systemNewInterface == "" { return } var systemNewInterfaceType string @@ -612,7 +611,7 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { } systemInterface.InterfaceType = systemNewInterfaceType case "ip-address": - if renameInterface || deleteInterface { + if renameInterface || deleteInterface || systemNewInterface == "" { return } var systemNewIpAddress string @@ -622,7 +621,7 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { } systemInterface.IPAddress = systemNewIpAddress case "ipv6-address": - if renameInterface || deleteInterface { + if renameInterface || deleteInterface || systemNewInterface == "" { return } var systemNewIpv6Address string @@ -632,7 +631,7 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { } systemInterface.IPv6Address = systemNewIpv6Address case "ipv6-default-gateway": - if renameInterface || deleteInterface { + if renameInterface || deleteInterface || systemNewInterface == "" { return } var systemNewIpv6DefaultGateway string @@ -642,7 +641,7 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { } systemInterface.IPv6DefaultGateway = systemNewIpv6DefaultGateway case "ipv6-mtu": - if renameInterface || deleteInterface { + if renameInterface || deleteInterface || systemNewInterface == "" { return } var systemNewIpv6Mtu string @@ -652,7 +651,7 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { } systemInterface.IPv6MTU = systemNewIpv6Mtu case "ipv6-prefix": - if renameInterface || deleteInterface { + if renameInterface || deleteInterface || systemNewInterface == "" { return } var systemNewIpv6Prefix string @@ -662,7 +661,7 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { } systemInterface.IPv6Prefix = systemNewIpv6Prefix case "ipv6-secondaries": - if renameInterface || deleteInterface { + if renameInterface || deleteInterface || systemNewInterface == "" { return } var systemNewIpv6Secondaries []string @@ -672,7 +671,7 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { } systemInterface.IPv6Secondaries = systemNewIpv6Secondaries case "ipv6-static-routes": - if renameInterface || deleteInterface { + if renameInterface || deleteInterface || systemNewInterface == "" { return } var systemNewIpv6StaticRoutes []string @@ -682,7 +681,7 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { } systemInterface.IPv6StaticRoutes = systemNewIpv6StaticRoutes case "mac-address": - if renameInterface || deleteInterface { + if renameInterface || deleteInterface || systemNewInterface == "" { return } var systemNewMacAddress string @@ -692,7 +691,7 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { } systemInterface.MACAddress = systemNewMacAddress case "management": - if renameInterface || deleteInterface { + if renameInterface || deleteInterface || systemNewInterface == "" { return } var systemNewManagement bool @@ -702,7 +701,7 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { } systemInterface.Management = systemNewManagement case "mtu": - if renameInterface || deleteInterface { + if renameInterface || deleteInterface || systemNewInterface == "" { return } var systemNewMtu string @@ -712,7 +711,7 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { } systemInterface.MTU = systemNewMtu case "netmask": - if renameInterface || deleteInterface { + if renameInterface || deleteInterface || systemNewInterface == "" { return } var systemNewNetmask string @@ -722,7 +721,7 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { } systemInterface.Netmask = systemNewNetmask case "static": - if renameInterface || deleteInterface { + if renameInterface || deleteInterface || systemNewInterface == "" { return } var systemNewStatic bool @@ -732,7 +731,7 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { } systemInterface.Static = systemNewStatic case "static-routes": - if renameInterface || deleteInterface { + if renameInterface || deleteInterface || systemNewInterface == "" { return } var systemNewStaticRoutes []string @@ -742,7 +741,7 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { } systemInterface.StaticRoutes = systemNewStaticRoutes case "virt-bridge": - if renameInterface || deleteInterface { + if renameInterface || deleteInterface || systemNewInterface == "" { return } var systemNewVirtBridge string @@ -764,412 +763,66 @@ func updateSystemFromFlags(cmd *cobra.Command, system *cobbler.System) error { return err } -// systemCmd represents the system command -var systemCmd = &cobra.Command{ - Use: "system", - Short: "System management", - Long: `Let you manage systems. +// NewSystemCmd builds a new command that represents the system action +func NewSystemCmd() *cobra.Command { + systemCmd := &cobra.Command{ + Use: "system", + Short: "System management", + Long: `Let you manage systems. See https://cobbler.readthedocs.io/en/latest/cobbler.html#cobbler-system for more information.`, - Run: func(cmd *cobra.Command, args []string) { - _ = cmd.Help() - }, -} - -var systemAddCmd = &cobra.Command{ - Use: "add", - Short: "add system", - Long: `Adds a system.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - newSystem := cobbler.NewSystem() - var err error - - // internal fields (ctime, mtime, depth, uid, repos-enabled, ipv6-autoconfiguration) cannot be modified - newSystem.Name, err = cmd.Flags().GetString("name") - if err != nil { - return err - } - // Update system in-memory - err = updateSystemFromFlags(cmd, &newSystem) - if err != nil { - return err - } - // No create the system via XML-RPC - system, err := Client.CreateSystem(newSystem) - if err != nil { - return err - } - fmt.Printf("System %s created\n", system.Name) - return nil - }, + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, + } + systemCmd.AddCommand(NewSystemAddCmd()) + systemCmd.AddCommand(NewSystemCopyCmd()) + systemCmd.AddCommand(NewSystemDumpVarsCmd()) + systemCmd.AddCommand(NewSystemEditCmd()) + systemCmd.AddCommand(NewSystemFindCmd()) + systemCmd.AddCommand(NewSystemGetAutoinstallCmd()) + systemCmd.AddCommand(NewSystemListCmd()) + systemCmd.AddCommand(NewSystemPowerOffCmd()) + systemCmd.AddCommand(NewSystemPowerOnCmd()) + systemCmd.AddCommand(NewSystemPowerStatusCmd()) + systemCmd.AddCommand(NewSystemRebootCmd()) + systemCmd.AddCommand(NewSystemRemoveCmd()) + systemCmd.AddCommand(NewSystemRenameCmd()) + systemCmd.AddCommand(NewSystemReportCmd()) + return systemCmd } -var systemCopyCmd = &cobra.Command{ - Use: "copy", - Short: "copy system", - Long: `Copies a system.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - systemName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - systemNewName, err := cmd.Flags().GetString("newname") - if err != nil { - return err - } - - systemHandle, err := Client.GetSystemHandle(systemName) - if err != nil { - return err - } - err = Client.CopySystem(systemHandle, systemNewName) - if err != nil { - return err - } - newSystem, err := Client.GetSystem(systemNewName, false, false) - if err != nil { - return err - } - // Update the system in-memory - err = updateSystemFromFlags(cmd, newSystem) - if err != nil { - return err - } - if newSystem.Meta.IsDirty { - newSystem, err = Client.GetSystem( - newSystem.Name, - newSystem.Meta.IsFlattened, - newSystem.Meta.IsResolved, - ) +func NewSystemAddCmd() *cobra.Command { + systemAddCmd := &cobra.Command{ + Use: "add", + Short: "add system", + Long: `Adds a system.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() if err != nil { return err } - } - // Update the system via XML-RPC - return Client.UpdateSystem(newSystem) - }, -} -var systemDumpVarsCmd = &cobra.Command{ - Use: "dumpvars", - Short: "dump system variables", - Long: `Prints all system variables to stdout.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() + newSystem := cobbler.NewSystem() - // Get CLI flags - systemName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - - // Now retrieve data - blendedData, err := Client.GetBlendedData("", systemName) - if err != nil { - return err - } - // Print data - printDumpVars(blendedData) - return err - }, -} - -var systemEditCmd = &cobra.Command{ - Use: "edit", - Short: "edit system", - Long: `Edits a system.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - // find profile through its name - systemName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - updateSystem, err := Client.GetSystem(systemName, false, false) - if err != nil { - return err - } - - // Update the system in-memory - err = updateSystemFromFlags(cmd, updateSystem) - if err != nil { - return err - } - fmt.Println(updateSystem.Interfaces) - if updateSystem.Meta.IsDirty { - updateSystem, err = Client.GetSystem( - updateSystem.Name, - updateSystem.Meta.IsFlattened, - updateSystem.Meta.IsResolved, - ) + // internal fields (ctime, mtime, depth, uid, repos-enabled, ipv6-autoconfiguration) cannot be modified + newSystem.Name, err = cmd.Flags().GetString("name") if err != nil { return err } - } - fmt.Println(updateSystem.Interfaces) - // Update the system via XML-RPC - return Client.UpdateSystem(updateSystem) - }, -} - -var systemFindCmd = &cobra.Command{ - Use: "find", - Short: "find system", - Long: `Finds a given system.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - return FindItemNames(cmd, args, "system") - }, -} - -var systemGetAutoinstallCmd = &cobra.Command{ - Use: "get-autoinstall", - Short: "dump autoinstall XML", - Long: `Prints the autoinstall XML file of the given system to stdout.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - systemName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - systemExists, err := Client.HasItem("system", systemName) - if err != nil { - return err - } - if !systemExists { - fmt.Println("System does not exist!") - os.Exit(1) - } - autoinstallRendered, err := Client.GenerateAutoinstall("", systemName) - if err != nil { - return err - } - fmt.Println(autoinstallRendered) - return nil - }, -} - -var systemListCmd = &cobra.Command{ - Use: "list", - Short: "list all systems", - Long: `Lists all available systems.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - systemNames, err := Client.ListSystemNames() - if err != nil { - return err - } - listItems("systems", systemNames) - return nil - }, -} - -var systemPowerOffCmd = &cobra.Command{ - Use: "poweroff", - Short: "power off system", - Long: `Powers off the selected system.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - // Get flags - systemName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - - // Perform action - systemHandle, err := Client.GetSystemHandle(systemName) - if err != nil { - return err - } - _, err = Client.PowerSystem(systemHandle, "off") - return err - }, -} - -var systemPowerOnCmd = &cobra.Command{ - Use: "poweron", - Short: "power on system", - Long: `Powers on the selected system.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - // Get flags - systemName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - - // Perform action - systemHandle, err := Client.GetSystemHandle(systemName) - if err != nil { - return err - } - _, err = Client.PowerSystem(systemHandle, "on") - return err - }, -} - -var systemPowerStatusCmd = &cobra.Command{ - Use: "powerstatus", - Short: "Power status of the system", - Long: `Querys the power status of the selected system.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - // Get flags - systemName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - - // Perform action - systemHandle, err := Client.GetSystemHandle(systemName) - if err != nil { - return err - } - _, err = Client.PowerSystem(systemHandle, "status") - return err - }, -} - -var systemRebootCmd = &cobra.Command{ - Use: "reboot", - Short: "reboot system", - Long: `Reboots the selected system.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - // Get flags - systemName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - - // Perform action - systemHandle, err := Client.GetSystemHandle(systemName) - if err != nil { - return err - } - _, err = Client.PowerSystem(systemHandle, "reboot") - return err - }, -} - -var systemRemoveCmd = &cobra.Command{ - Use: "remove", - Short: "remove system", - Long: `Removes a given system.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - return RemoveItemRecursive(cmd, args, "system") - }, -} - -var systemRenameCmd = &cobra.Command{ - Use: "rename", - Short: "rename system", - Long: `Renames a given system.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - - // Get flags - systemName, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - systemNewName, err := cmd.Flags().GetString("newname") - if err != nil { - return err - } - - // Perform action - systemHandle, err := Client.GetSystemHandle(systemName) - if err != nil { - return err - } - err = Client.RenameSystem(systemHandle, systemNewName) - if err != nil { - return err - } - newSystem, err := Client.GetSystem(systemNewName, false, false) - if err != nil { - return err - } - err = updateSystemFromFlags(cmd, newSystem) - if err != nil { - return err - } - if newSystem.Meta.IsDirty { - newSystem, err = Client.GetSystem( - newSystem.Name, - newSystem.Meta.IsFlattened, - newSystem.Meta.IsResolved, - ) + // Update system in-memory + err = updateSystemFromFlags(cmd, &newSystem) if err != nil { return err } - } - return Client.UpdateSystem(newSystem) - }, -} - -func reportSystems(systemNames []string) error { - for _, itemName := range systemNames { - system, err := Client.GetSystem(itemName, false, false) - if err != nil { - return err - } - printStructured(system) - fmt.Println("") - } - return nil -} - -var systemReportCmd = &cobra.Command{ - Use: "report", - Short: "list all systems in detail", - Long: `Shows detailed information about all systems.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - name, err := cmd.Flags().GetString("name") - if err != nil { - return err - } - itemNames := make([]string, 0) - if name == "" { - itemNames, err = Client.ListSystemNames() + // Now create the system via XML-RPC + system, err := Client.CreateSystem(newSystem) if err != nil { return err } - } else { - itemNames = append(itemNames, name) - } - return reportSystems(itemNames) - }, -} - -func init() { - rootCmd.AddCommand(systemCmd) - systemCmd.AddCommand(systemAddCmd) - systemCmd.AddCommand(systemCopyCmd) - systemCmd.AddCommand(systemDumpVarsCmd) - systemCmd.AddCommand(systemEditCmd) - systemCmd.AddCommand(systemFindCmd) - systemCmd.AddCommand(systemGetAutoinstallCmd) - systemCmd.AddCommand(systemListCmd) - systemCmd.AddCommand(systemPowerOffCmd) - systemCmd.AddCommand(systemPowerOnCmd) - systemCmd.AddCommand(systemPowerStatusCmd) - systemCmd.AddCommand(systemRebootCmd) - systemCmd.AddCommand(systemRemoveCmd) - systemCmd.AddCommand(systemRenameCmd) - systemCmd.AddCommand(systemReportCmd) - - // local flags for system add + fmt.Fprintf(cmd.OutOrStdout(), "System %s created\n", system.Name) + return nil + }, + } addCommonArgs(systemAddCmd) addStringFlags(systemAddCmd, systemStringFlagMetadata) addStringFlags(systemAddCmd, systemPowerStringFlagMetadata) @@ -1185,8 +838,60 @@ func init() { // 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 +} + +func NewSystemCopyCmd() *cobra.Command { + systemCopyCmd := &cobra.Command{ + Use: "copy", + Short: "copy system", + Long: `Copies a system.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - // local flags for system copy + systemName, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + systemNewName, err := cmd.Flags().GetString("newname") + if err != nil { + return err + } + + systemHandle, err := Client.GetSystemHandle(systemName) + if err != nil { + return err + } + err = Client.CopySystem(systemHandle, systemNewName) + if err != nil { + return err + } + newSystem, err := Client.GetSystem(systemNewName, false, false) + if err != nil { + return err + } + // Update the system in-memory + err = updateSystemFromFlags(cmd, newSystem) + if err != nil { + return err + } + if newSystem.Meta.IsDirty { + newSystem, err = Client.GetSystem( + newSystem.Name, + newSystem.Meta.IsFlattened, + newSystem.Meta.IsResolved, + ) + if err != nil { + return err + } + } + // Update the system via XML-RPC + return Client.UpdateSystem(newSystem) + }, + } addCommonArgs(systemCopyCmd) addStringFlags(systemCopyCmd, systemStringFlagMetadata) addStringFlags(systemCopyCmd, systemPowerStringFlagMetadata) @@ -1200,15 +905,85 @@ func init() { 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 +} + +func NewSystemDumpVarsCmd() *cobra.Command { + systemDumpVarsCmd := &cobra.Command{ + Use: "dumpvars", + Short: "dump system variables", + Long: `Prints all system variables to stdout.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - // local flags for system dumpvars + // Get CLI flags + systemName, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + + // Now retrieve data + blendedData, err := Client.GetBlendedData("", systemName) + if err != nil { + return err + } + // Print data + printDumpVars(cmd, blendedData) + return err + }, + } systemDumpVarsCmd.Flags().String("name", "", "the system name") + return systemDumpVarsCmd +} + +func NewSystemEditCmd() *cobra.Command { + systemEditCmd := &cobra.Command{ + Use: "edit", + Short: "edit system", + Long: `Edits a system.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + // find profile through its name + systemName, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + updateSystem, err := Client.GetSystem(systemName, false, false) + if err != nil { + return err + } - // local flags for system edit + // Update the system in-memory + err = updateSystemFromFlags(cmd, updateSystem) + if err != nil { + return err + } + if updateSystem.Meta.IsDirty { + updateSystem, err = Client.GetSystem( + updateSystem.Name, + updateSystem.Meta.IsFlattened, + updateSystem.Meta.IsResolved, + ) + if err != nil { + return err + } + } + // Update the system via XML-RPC + return Client.UpdateSystem(updateSystem) + }, + } addCommonArgs(systemEditCmd) addStringFlags(systemEditCmd, systemStringFlagMetadata) addStringFlags(systemEditCmd, systemPowerStringFlagMetadata) @@ -1226,8 +1001,23 @@ func init() { 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 +} - // local flags for system find +func NewSystemFindCmd() *cobra.Command { + systemFindCmd := &cobra.Command{ + Use: "find", + Short: "find system", + Long: `Finds a given system.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + return FindItemNames(cmd, args, "system") + }, + } addCommonArgs(systemFindCmd) addStringFlags(systemFindCmd, systemStringFlagMetadata) addStringFlags(systemFindCmd, systemPowerStringFlagMetadata) @@ -1245,27 +1035,256 @@ func init() { addIntFlags(systemFindCmd, findIntFlagMetadata) addFloatFlags(systemFindCmd, findFloatFlagMetadata) systemFindCmd.Flags().String("interface", "", "the interface to operate on") + return systemFindCmd +} + +func NewSystemGetAutoinstallCmd() *cobra.Command { + systemGetAutoinstallCmd := &cobra.Command{ + Use: "get-autoinstall", + Short: "dump autoinstall XML", + Long: `Prints the autoinstall XML file of the given system to stdout.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - // local flags for system get-autoinstall + systemName, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + systemExists, err := Client.HasItem("system", systemName) + if err != nil { + return err + } + if !systemExists { + //goland:noinspection GoErrorStringFormat + return fmt.Errorf("System does not exist") + } + autoinstallRendered, err := Client.GenerateAutoinstall("", systemName) + if err != nil { + return err + } + fmt.Fprintln(cmd.OutOrStdout(), autoinstallRendered) + return nil + }, + } systemGetAutoinstallCmd.Flags().String("name", "", "the system name") + return systemGetAutoinstallCmd +} + +func NewSystemListCmd() *cobra.Command { + systemListCmd := &cobra.Command{ + Use: "list", + Short: "list all systems", + Long: `Lists all available systems.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + systemNames, err := Client.ListSystemNames() + if err != nil { + return err + } + listItems(cmd, "systems", systemNames) + return nil + }, + } + return systemListCmd +} + +func NewSystemPowerOffCmd() *cobra.Command { + systemPowerOffCmd := &cobra.Command{ + Use: "poweroff", + Short: "power off system", + Long: `Powers off the selected system.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + // Get flags + systemName, err := cmd.Flags().GetString("name") + if err != nil { + return err + } - // local flags for system poweroff + // Perform action + systemHandle, err := Client.GetSystemHandle(systemName) + if err != nil { + return err + } + _, err = Client.PowerSystem(systemHandle, "off") + return err + }, + } systemPowerOffCmd.Flags().String("name", "", "the system name") + return systemPowerOffCmd +} + +func NewSystemPowerOnCmd() *cobra.Command { + systemPowerOnCmd := &cobra.Command{ + Use: "poweron", + Short: "power on system", + Long: `Powers on the selected system.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + // Get flags + systemName, err := cmd.Flags().GetString("name") + if err != nil { + return err + } - // local flags for system poweron + // Perform action + systemHandle, err := Client.GetSystemHandle(systemName) + if err != nil { + return err + } + _, err = Client.PowerSystem(systemHandle, "on") + return err + }, + } systemPowerOnCmd.Flags().String("name", "", "the system name") + return systemPowerOnCmd +} - // local flags for system powerstatus +func NewSystemPowerStatusCmd() *cobra.Command { + systemPowerStatusCmd := &cobra.Command{ + Use: "powerstatus", + Short: "Power status of the system", + Long: `Querys the power status of the selected system.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + // Get flags + systemName, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + + // Perform action + systemHandle, err := Client.GetSystemHandle(systemName) + if err != nil { + return err + } + _, err = Client.PowerSystem(systemHandle, "status") + return err + }, + } systemPowerStatusCmd.Flags().String("name", "", "the system name") + return systemPowerStatusCmd +} - // local flags for system reboot +func NewSystemRebootCmd() *cobra.Command { + systemRebootCmd := &cobra.Command{ + Use: "reboot", + Short: "reboot system", + Long: `Reboots the selected system.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + // Get flags + systemName, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + + // Perform action + systemHandle, err := Client.GetSystemHandle(systemName) + if err != nil { + return err + } + _, err = Client.PowerSystem(systemHandle, "reboot") + return err + }, + } systemRebootCmd.Flags().String("name", "", "the system name") + return systemRebootCmd +} + +func NewSystemRemoveCmd() *cobra.Command { + systemRemoveCmd := &cobra.Command{ + Use: "remove", + Short: "remove system", + Long: `Removes a given system.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } - // local flags for system remove + return RemoveItemRecursive(cmd, args, "system") + }, + } systemRemoveCmd.Flags().String("name", "", "the system name") systemRemoveCmd.Flags().Bool("recursive", false, "also delete child objects") + return systemRemoveCmd +} + +func NewSystemRenameCmd() *cobra.Command { + systemRenameCmd := &cobra.Command{ + Use: "rename", + Short: "rename system", + Long: `Renames a given system.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + + // Get flags + systemName, err := cmd.Flags().GetString("name") + if err != nil { + return err + } + systemNewName, err := cmd.Flags().GetString("newname") + if err != nil { + return err + } - // local flags for system rename + // Perform action + systemHandle, err := Client.GetSystemHandle(systemName) + if err != nil { + return err + } + err = Client.RenameSystem(systemHandle, systemNewName) + if err != nil { + return err + } + newSystem, err := Client.GetSystem(systemNewName, false, false) + if err != nil { + return err + } + err = updateSystemFromFlags(cmd, newSystem) + if err != nil { + return err + } + if newSystem.Meta.IsDirty { + newSystem, err = Client.GetSystem( + newSystem.Name, + newSystem.Meta.IsFlattened, + newSystem.Meta.IsResolved, + ) + if err != nil { + return err + } + } + return Client.UpdateSystem(newSystem) + }, + } addCommonArgs(systemRenameCmd) addStringFlags(systemRenameCmd, systemStringFlagMetadata) addStringFlags(systemRenameCmd, systemPowerStringFlagMetadata) @@ -1279,12 +1298,53 @@ func init() { addBoolFlags(systemRenameCmd, interfaceBoolFlagMetadata) addStringSliceFlags(systemRenameCmd, interfaceStringSliceFlagMetadata) // Other - systemRenameCmd.Flags().String("newname", "", "the new system name") + 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 +} + +func reportSystems(cmd *cobra.Command, systemNames []string) error { + for _, itemName := range systemNames { + system, err := Client.GetSystem(itemName, false, false) + if err != nil { + return err + } + printStructured(cmd, system) + fmt.Fprintln(cmd.OutOrStdout(), "") + } + return nil +} - // local flags for system report +func NewSystemReportCmd() *cobra.Command { + systemReportCmd := &cobra.Command{ + Use: "report", + Short: "list all systems in detail", + Long: `Shows detailed information about all systems.`, + 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.ListSystemNames() + if err != nil { + return err + } + } else { + itemNames = append(itemNames, name) + } + return reportSystems(cmd, itemNames) + }, + } systemReportCmd.Flags().String("name", "", "the system name") + return systemReportCmd } diff --git a/cmd/system_test.go b/cmd/system_test.go new file mode 100644 index 0000000..6264d57 --- /dev/null +++ b/cmd/system_test.go @@ -0,0 +1,455 @@ +package cmd + +import ( + "bytes" + "fmt" + cobbler "github.com/cobbler/cobblerclient" + "github.com/spf13/cobra" + "io" + "strings" + "testing" +) + +func createSystem(client cobbler.Client, name string) (*cobbler.System, error) { + system := cobbler.NewSystem() + system.Name = name + system.Profile = "Ubuntu-20.04-x86_64" + return client.CreateSystem(system) +} + +func removeSystem(client cobbler.Client, name string) error { + return client.DeleteSystem(name) +} + +func Test_SystemAddCmd(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", "system", "add", "--name", "test-plain", "--profile", "Ubuntu-20.04-x86_64"}}, + want: "System 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 := removeSystem(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_SystemCopyCmd(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", "system", "copy", "--name", "system-to-copy", "--newname", "copied-system"}}, + 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 := removeSystem(Client, tt.args.command[5]) + cobbler.FailOnError(t, cleanupErr) + cleanupErr = removeSystem(Client, tt.args.command[7]) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createSystem(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.GetSystem(tt.args.command[7], false, false) + cobbler.FailOnError(t, err) + }) + } +} + +func Test_SystemEditCmd(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", "system", "edit", "--name", "test-system-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 := removeSystem(Client, tt.args.command[5]) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createSystem(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) + updatedSystem, err := Client.GetSystem(tt.args.command[5], false, false) + cobbler.FailOnError(t, err) + if updatedSystem.Comment != "testcomment" { + t.Fatal("system update wasn't successful") + } + }) + } +} + +func Test_SystemFindCmd(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", "system", "find", "--name", "test-system-find"}}, + want: "Event ID:", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Cleanup + systemName := "test-system-find" + var err error + defer func() { + // Client is initialized since this is the cleanup + cleanupErr := removeSystem(Client, systemName) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createSystem(Client, systemName) + 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, systemName) { + fmt.Println(stdoutString) + t.Fatal("system not successfully found") + } + }) + } +} + +func Test_SystemListCmd(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", "system", "list"}}, + want: "systems:", + 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("system list marker not located in output") + } + }) + } +} + +func Test_SystemRemoveCmd(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", "system", "remove", "--name", "test-system-remove"}}, + want: "Event ID:", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Arrange + setupClient(t) + _, err := createSystem(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("system", 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("system not successfully removed") + } + }) + } +} + +func Test_SystemRenameCmd(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", "system", "rename", "--name", "test-system-rename", "--newname", "test-system-renamed"}}, + want: "Event ID:", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Cleanup + systemName := "test-system-rename" + newSystemName := "test-system-renamed" + var err error + defer func() { + // Client is initialized since this is the cleanup + cleanupErr := removeSystem(Client, newSystemName) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createSystem(Client, systemName) + 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("system", systemName) + cobbler.FailOnError(t, err) + if resultOldName { + t.Fatal("system not successfully renamed (old name present)") + } + resultNewName, err := Client.HasItem("system", newSystemName) + cobbler.FailOnError(t, err) + if !resultNewName { + t.Fatal("system not successfully renamed (new name not present)") + } + }) + } +} + +func Test_SystemReportCmd(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", "system", "report", "--name", "test-system-report"}}, + want: ": test-system-report", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Cleanup + systemName := "test-system-report" + var err error + defer func() { + // Client is initialized since this is the cleanup + cleanupErr := removeSystem(Client, systemName) + cobbler.FailOnError(t, cleanupErr) + }() + // Arrange + setupClient(t) + _, err = createSystem(Client, systemName) + 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/testing.go b/cmd/testing.go new file mode 100644 index 0000000..3ecb48b --- /dev/null +++ b/cmd/testing.go @@ -0,0 +1,20 @@ +package cmd + +import ( + "bytes" + cobbler "github.com/cobbler/cobblerclient" + "testing" +) + +func setupClient(t *testing.T) { + cfgFile = "../testing/.cobbler.yaml" + initConfig() + err := generateCobblerClient() + cobbler.FailOnError(t, err) +} + +func FailOnNonEmptyStream(t *testing.T, buffer *bytes.Buffer) { + if buffer.Available() > 0 { + t.Fatal("stream wasn't empty!") + } +} diff --git a/cmd/utils.go b/cmd/utils.go index 4cfe981..e09c431 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -3,6 +3,7 @@ package cmd import ( "encoding/json" "fmt" + "github.com/spf13/cobra" "reflect" "strconv" "strings" @@ -34,46 +35,46 @@ func covertFloatToUtcTime(t float64) (time.Time, error) { return time.Unix(seconds, nanoSeconds).In(timezone), nil } -func printDumpVars(blendedData map[string]interface{}) { +func printDumpVars(cmd *cobra.Command, blendedData map[string]interface{}) { for key, value := range blendedData { if value == nil { - fmt.Printf("%s:\n", key) + fmt.Fprintf(cmd.OutOrStdout(), "%s:\n", key) continue } valueType := reflect.TypeOf(value).Kind() switch valueType { case reflect.Bool: - fmt.Printf("%s: %t\n", key, value.(bool)) + fmt.Fprintf(cmd.OutOrStdout(), "%s: %t\n", key, value.(bool)) case reflect.Int64: - fmt.Printf("%s: %d\n", key, value.(int64)) + fmt.Fprintf(cmd.OutOrStdout(), "%s: %d\n", key, value.(int64)) case reflect.Int32: - fmt.Printf("%s: %d\n", key, value.(int32)) + fmt.Fprintf(cmd.OutOrStdout(), "%s: %d\n", key, value.(int32)) case reflect.Int16: - fmt.Printf("%s: %d\n", key, value.(int16)) + fmt.Fprintf(cmd.OutOrStdout(), "%s: %d\n", key, value.(int16)) case reflect.Int8: - fmt.Printf("%s: %d\n", key, value.(int8)) + fmt.Fprintf(cmd.OutOrStdout(), "%s: %d\n", key, value.(int8)) case reflect.Int: - fmt.Printf("%s: %d\n", key, value.(int)) + fmt.Fprintf(cmd.OutOrStdout(), "%s: %d\n", key, value.(int)) case reflect.Float32: - fmt.Printf("%s: %f\n", key, value.(float32)) + fmt.Fprintf(cmd.OutOrStdout(), "%s: %f\n", key, value.(float32)) case reflect.Float64: - fmt.Printf("%s: %f\n", key, value.(float64)) + fmt.Fprintf(cmd.OutOrStdout(), "%s: %f\n", key, value.(float64)) case reflect.Slice, reflect.Array: arr := reflect.ValueOf(value) - fmt.Printf("%s: [", key) + fmt.Fprintf(cmd.OutOrStdout(), "%s: [", key) for i := 0; i < arr.Len(); i++ { if i+1 != arr.Len() { - fmt.Printf("'%v', ", arr.Index(i).Interface()) + fmt.Fprintf(cmd.OutOrStdout(), "'%v', ", arr.Index(i).Interface()) } else { - fmt.Printf("'%v'", arr.Index(i).Interface()) + fmt.Fprintf(cmd.OutOrStdout(), "'%v'", arr.Index(i).Interface()) } } - fmt.Printf("]\n") + fmt.Fprintf(cmd.OutOrStdout(), "]\n") case reflect.Map: res2B, _ := json.Marshal(value) - fmt.Printf("%s: %s\n", key, string(res2B)) + fmt.Fprintf(cmd.OutOrStdout(), "%s: %s\n", key, string(res2B)) default: - fmt.Printf("%s: %s\n", key, value) + fmt.Fprintf(cmd.OutOrStdout(), "%s: %s\n", key, value) } } } diff --git a/cmd/utils_test.go b/cmd/utils_test.go index c743d63..a82235f 100644 --- a/cmd/utils_test.go +++ b/cmd/utils_test.go @@ -1,7 +1,6 @@ package cmd import ( - "fmt" "reflect" "testing" "time" @@ -26,7 +25,6 @@ func Test_covertFloatToTime(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := covertFloatToUtcTime(tt.args.t) - fmt.Println(got) if (err != nil) != tt.wantErr { t.Errorf("covertFloatToTime() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/cmd/validateAutoinstalls.go b/cmd/validateAutoinstalls.go index ed3656f..e1c5c6d 100644 --- a/cmd/validateAutoinstalls.go +++ b/cmd/validateAutoinstalls.go @@ -9,22 +9,25 @@ import ( "github.com/spf13/cobra" ) -// validateAutoinstallsCmd represents the validateAutoinstalls command -var validateAutoinstallsCmd = &cobra.Command{ - Use: "validate-autoinstalls", - Short: "Autoinstall validation", - Long: `Validates the autoinstall files.`, - RunE: func(cmd *cobra.Command, args []string) error { - generateCobblerClient() - eventId, err := Client.BackgroundValidateAutoinstallFiles() - if err != nil { - return err - } - fmt.Printf("Event ID: %s\n", eventId) - return nil - }, -} +// NewValidateAutoinstallsCmd builds a command that represents the validateAutoinstalls action +func NewValidateAutoinstallsCmd() *cobra.Command { + validateAutoinstallsCmd := &cobra.Command{ + Use: "validate-autoinstalls", + Short: "Autoinstall validation", + Long: `Validates the autoinstall files.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } -func init() { - rootCmd.AddCommand(validateAutoinstallsCmd) + eventId, err := Client.BackgroundValidateAutoinstallFiles() + if err != nil { + return err + } + fmt.Fprintf(cmd.OutOrStdout(), "Event ID: %s\n", eventId) + return nil + }, + } + return validateAutoinstallsCmd } diff --git a/cmd/validateAutoinstalls_test.go b/cmd/validateAutoinstalls_test.go new file mode 100644 index 0000000..d80f24c --- /dev/null +++ b/cmd/validateAutoinstalls_test.go @@ -0,0 +1,38 @@ +package cmd + +import ( + "bytes" + "fmt" + "github.com/cobbler/cobblerclient" + "github.com/spf13/cobra" + "io" + "strings" + "testing" +) + +func Test_ValidateAutoinstallsCmd(t *testing.T) { + // Arrange + cobra.OnInitialize(initConfig, setupLogger) + rootCmd := NewRootCmd() + rootCmd.SetArgs([]string{"--config", "../testing/.cobbler.yaml", "validate-autoinstalls"}) + 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, "Event ID:") { + fmt.Println(stdoutString) + t.Fatal("No Event ID present") + } +} diff --git a/cmd/version.go b/cmd/version.go index 29c4705..03d73b9 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -10,24 +10,31 @@ import ( "runtime/debug" ) -// versionCmd represents the version command -var versionCmd = &cobra.Command{ - Use: "version", - Short: "Print the Cobbler version", - Long: `Shows the Cobbler server version.`, - Run: func(cmd *cobra.Command, args []string) { - generateCobblerClient() - version, err := Client.ExtendedVersion() - if err != nil { - fmt.Println(err) - } - clientVersion, cliVersion, _ := getClientVersion() - fmt.Printf("Cobbler %s\n", version.Version) - fmt.Printf(" source: %s, %s\n", version.Gitstamp, version.Gitdate) - fmt.Printf(" build time: %s\n", version.Builddate) - fmt.Printf(" cli: %s\n", cliVersion) - fmt.Printf(" client: %s\n", clientVersion) - }, +// NewVersionCmd builds a new command that represents the version action +func NewVersionCmd() *cobra.Command { + versionCmd := &cobra.Command{ + Use: "version", + Short: "Print the Cobbler version", + Long: `Shows the Cobbler server version.`, + RunE: func(cmd *cobra.Command, args []string) error { + err := generateCobblerClient() + if err != nil { + return err + } + version, err := Client.ExtendedVersion() + if err != nil { + return err + } + clientVersion, cliVersion, _ := getClientVersion() + fmt.Fprintf(cmd.OutOrStdout(), "Cobbler %s\n", version.Version) + fmt.Fprintf(cmd.OutOrStdout(), " source: %s, %s\n", version.Gitstamp, version.Gitdate) + fmt.Fprintf(cmd.OutOrStdout(), " build time: %s\n", version.Builddate) + fmt.Fprintf(cmd.OutOrStdout(), " cli: %s\n", cliVersion) + fmt.Fprintf(cmd.OutOrStdout(), " client: %s\n", clientVersion) + return nil + }, + } + return versionCmd } func getClientVersion() (string, string, error) { @@ -43,7 +50,3 @@ func getClientVersion() (string, string, error) { } return clientVersion, cliVersion, nil } - -func init() { - rootCmd.AddCommand(versionCmd) -} diff --git a/cmd/version_test.go b/cmd/version_test.go new file mode 100644 index 0000000..ac86d73 --- /dev/null +++ b/cmd/version_test.go @@ -0,0 +1,38 @@ +package cmd + +import ( + "bytes" + "fmt" + "github.com/cobbler/cobblerclient" + "github.com/spf13/cobra" + "io" + "strings" + "testing" +) + +func Test_VersionCommand(t *testing.T) { + // Arrange + cobra.OnInitialize(initConfig, setupLogger) + rootCmd := NewRootCmd() + rootCmd.SetArgs([]string{"--config", "../testing/.cobbler.yaml", "version"}) + 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, "source: ?, ?") { + fmt.Println(stdoutString) + t.Fatal("CLI version not part of the return string") + } +} diff --git a/go.mod b/go.mod index 7884c39..374f586 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/cobbler/cli go 1.22 require ( - github.com/cobbler/cobblerclient v0.5.4 + github.com/cobbler/cobblerclient v0.5.5 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 fcea715..68d5924 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/cobbler/cobblerclient v0.5.4 h1:ix+z9gwfUxv28ANz7giPGt4ICufR83rUm9E2q9dCwuw= -github.com/cobbler/cobblerclient v0.5.4/go.mod h1:n6b8fTUOlg7BdMl6FeifUm4Uk1JY6/tlTlOClV4x2Wc= +github.com/cobbler/cobblerclient v0.5.5 h1:sMd+j3IcW8atuAbwiJWOL+vriqxTPi5jxEg10TdQUc0= +github.com/cobbler/cobblerclient v0.5.5/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= diff --git a/testing/.cobbler.yaml b/testing/.cobbler.yaml new file mode 100644 index 0000000..6531712 --- /dev/null +++ b/testing/.cobbler.yaml @@ -0,0 +1,3 @@ +server_url: "http://127.0.0.1:8081/cobbler_api" +server_username: "cobbler" +server_password: "cobbler" diff --git a/testing/compose.yml b/testing/compose.yml new file mode 100644 index 0000000..f02d3f3 --- /dev/null +++ b/testing/compose.yml @@ -0,0 +1,17 @@ +services: + cobbler: + image: cobbler-dev + container_name: cobbler-dev + privileged: true # Required for Cobbler 3.3.2 and newer + volumes: + - ./cobbler_source:/code + - ../extracted_iso_image:/extracted_iso_image + ports: + - 8081:80 + # We chmod the code, otherwise some files are read-only and cannot be cleaned up: + command: bash -c " + /code/docker/develop/scripts/setup-supervisor.sh && + chmod -R o+w /code && + cobbler import --name Ubuntu-20.04 --breed ubuntu --path /extracted_iso_image/ && + tail -F /dev/null + " diff --git a/testing/start.sh b/testing/start.sh new file mode 100755 index 0000000..1d75463 --- /dev/null +++ b/testing/start.sh @@ -0,0 +1,70 @@ +#! /bin/bash + +# Requires xorriso (sudo apt-get install -y xorriso, sudo yum install xorriso -y, or sudo zypper install -y xorriso) +if [ -z "$1" ] + then + echo "No cobbler server url supplied" +fi + +cobbler_commit=df356046f3cf27be62a61001b982d5983800cfd9 # 3.3.6 as of 2024-10-09 +cobbler_branch=release33 +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 + 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 + +echo $(pwd) +if [ -f "$iso_filename" ] && [ $(sha1sum $iso_filename | awk '{print $1}') == "$valid_iso_checksum" ]; then + echo "ISO already downloaded" +else + rm $iso_filename + wget $iso_url +fi + +extracted_iso_checksum=$(find extracted_iso_image -type f -exec md5sum {} \; | sort -k 2 | md5sum | awk '{print $1}') +if [ -d "extracted_iso_image" ] && [ $extracted_iso_checksum == $valid_extracted_iso_checksum ]; then + echo "ISO already extracted" +else + xorriso -osirrox on -indev $iso_filename -extract / extracted_iso_image +fi + +docker build -f ./testing/cobbler_source/docker/develop/develop.dockerfile -t cobbler-dev . +docker compose -f testing/compose.yml up -d + +SERVER_URL=$1 +printf "### Waiting for Cobbler to become available on ${SERVER_URL} \n\n" + +attempt_counter=0 +max_attempts=48 + +until $(curl --connect-timeout 1 --output /dev/null --silent ${SERVER_URL}); do + if [ ${attempt_counter} -eq ${max_attempts} ];then + echo "Max attempts reached" + # Debug logs + docker compose -f ./testing/compose.yml logs + exit 1 + fi + + attempt_counter=$(($attempt_counter+1)) + sleep 5 +done + +# Sleep 10 seconds to let the "cobbler import" succeed +sleep 10 + +docker compose -f testing/compose.yml logs \ No newline at end of file