Skip to content

Commit 450e2b7

Browse files
committed
Add mgrctl commands "ssh" and "ssh knownhost remove"
1 parent e82b89a commit 450e2b7

6 files changed

Lines changed: 222 additions & 1 deletion

File tree

mgrctl/cmd/cmd.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// SPDX-FileCopyrightText: 2025 SUSE LLC
1+
// SPDX-FileCopyrightText: 2026 SUSE LLC
22
//
33
// SPDX-License-Identifier: Apache-2.0
44

@@ -15,6 +15,7 @@ import (
1515
"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/cp"
1616
"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/exec"
1717
"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/proxy"
18+
"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/ssh"
1819
"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/term"
1920
"github.com/uyuni-project/uyuni-tools/shared/completion"
2021
. "github.com/uyuni-project/uyuni-tools/shared/l10n"
@@ -55,6 +56,7 @@ func NewUyunictlCommand() *cobra.Command {
5556
rootCmd.AddCommand(cp.NewCommand(globalFlags))
5657
rootCmd.AddCommand(completion.NewCommand(globalFlags))
5758
rootCmd.AddCommand(proxy.NewCommand(globalFlags))
59+
rootCmd.AddCommand(ssh.NewCommand(globalFlags))
5860

5961
rootCmd.AddCommand(utils.GetConfigHelpCommand())
6062

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// SPDX-FileCopyrightText: 2026 SUSE LLC
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package knownhost
6+
7+
import (
8+
"github.com/spf13/cobra"
9+
"github.com/uyuni-project/uyuni-tools/shared/api"
10+
. "github.com/uyuni-project/uyuni-tools/shared/l10n"
11+
"github.com/uyuni-project/uyuni-tools/shared/types"
12+
"github.com/uyuni-project/uyuni-tools/shared/utils"
13+
)
14+
15+
type apiFlags struct {
16+
api.ConnectionDetails `mapstructure:"api"`
17+
}
18+
19+
func NewKnownHostCommand(globalFlags *types.GlobalFlags) *cobra.Command {
20+
var flags apiFlags
21+
22+
sshKnownhostCmd := &cobra.Command{
23+
Use: "knownhost",
24+
Short: L("SSH known_hosts file management"),
25+
}
26+
27+
sshRemoveKnownHostCmd := &cobra.Command{
28+
Use: "remove hostname [port]",
29+
Short: L("Remove a SSH known host"),
30+
Long: L(`Removes a host from the list of Salt's SSH known_hosts.
31+
If no port is specified, it will default to 22.`),
32+
RunE: func(cmd *cobra.Command, args []string) error {
33+
return utils.CommandHelper(globalFlags, cmd, args, &flags, nil, runRemoveKnownHost)
34+
},
35+
Args: cobra.RangeArgs(1, 2),
36+
}
37+
38+
sshKnownhostCmd.AddCommand(sshRemoveKnownHostCmd)
39+
return sshKnownhostCmd
40+
}

mgrctl/cmd/ssh/knownhost/remove.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// SPDX-FileCopyrightText: 2026 SUSE LLC
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package knownhost
6+
7+
import (
8+
"fmt"
9+
"net/url"
10+
11+
"github.com/rs/zerolog/log"
12+
"github.com/spf13/cobra"
13+
"github.com/uyuni-project/uyuni-tools/shared/api"
14+
. "github.com/uyuni-project/uyuni-tools/shared/l10n"
15+
"github.com/uyuni-project/uyuni-tools/shared/types"
16+
"github.com/uyuni-project/uyuni-tools/shared/utils"
17+
)
18+
19+
func removeKnownHost(client *api.APIClient, hostname string, port string) error {
20+
params := url.Values{
21+
"hostname": {hostname},
22+
"port": {port},
23+
}
24+
path := "admin/ssh/removeKnownHost?" + params.Encode()
25+
26+
var data map[string]interface{}
27+
res, err := api.Post[interface{}](client, path, data)
28+
if err != nil {
29+
return utils.Errorf(err, L("error in query '%s'"), path)
30+
}
31+
32+
if !res.Success {
33+
return fmt.Errorf(L("failed to remove the host: %s"), res.Message)
34+
}
35+
36+
success, ok := res.Result.(float64)
37+
if !ok {
38+
return fmt.Errorf(L("unexpected server response '%v'"), res.Result)
39+
}
40+
41+
if int(success) == 1 {
42+
fmt.Println(L("successfully removed host"))
43+
} else {
44+
fmt.Println(L("unable to remove host, server returned an error"))
45+
}
46+
47+
return nil
48+
}
49+
50+
func runRemoveKnownHost(_ *types.GlobalFlags, flags *apiFlags, _ *cobra.Command, args []string) error {
51+
hostname := args[0]
52+
// Default to port 22 if none is provided
53+
port := "22"
54+
if len(args) == 2 {
55+
port = args[1]
56+
}
57+
58+
log.Debug().Msgf("Removing host %s:%s", hostname, port)
59+
client, err := api.Init(&flags.ConnectionDetails)
60+
if err == nil {
61+
err = client.Login()
62+
}
63+
if err != nil {
64+
return utils.Errorf(err, L("unable to login to the server"))
65+
}
66+
67+
return removeKnownHost(client, hostname, port)
68+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// SPDX-FileCopyrightText: 2026 SUSE LLC
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package knownhost
6+
7+
import (
8+
"net/http"
9+
"testing"
10+
11+
"github.com/uyuni-project/uyuni-tools/shared/api"
12+
"github.com/uyuni-project/uyuni-tools/shared/api/mocks"
13+
"github.com/uyuni-project/uyuni-tools/shared/testutils"
14+
)
15+
16+
const user = "testUser"
17+
const password = "testPwd"
18+
const server = "testServer"
19+
20+
var connectionDetails = &api.ConnectionDetails{User: user, Password: password, Server: server}
21+
22+
// Test removeKnownHost function.
23+
func TestRemoveKnownHost(t *testing.T) {
24+
tests := []struct {
25+
name string
26+
hostname string
27+
port string
28+
statusCode int
29+
body string
30+
expectedError string
31+
}{
32+
{
33+
name: "Test removing normal hostname and port",
34+
hostname: "client.uyuni.lan",
35+
port: "22",
36+
statusCode: 200,
37+
body: `{"success":true,"result":1}`,
38+
expectedError: "",
39+
},
40+
{
41+
name: "Test removing escaped hostname and port",
42+
hostname: "escaped?client.uyuni.lan",
43+
port: "42",
44+
statusCode: 200,
45+
body: `{"success":true,"result":1}`,
46+
expectedError: "",
47+
},
48+
{
49+
name: "Test removing an invalid port value",
50+
hostname: "myotherclient.uyuni.lan",
51+
port: "abc",
52+
statusCode: 400,
53+
body: `{"success":false,"message":"mocked error"}`,
54+
expectedError: "mocked error",
55+
},
56+
}
57+
58+
for _, tt := range tests {
59+
t.Run(tt.name, func(t *testing.T) {
60+
client, err := api.Init(connectionDetails)
61+
if err != nil {
62+
t.FailNow()
63+
}
64+
65+
client.Client = &mocks.MockClient{
66+
DoFunc: func(req *http.Request) (*http.Response, error) {
67+
testutils.AssertEquals(t, "Wrong URL called", req.URL.Path, "/rhn/manager/api/admin/ssh/removeKnownHost")
68+
69+
query := req.URL.Query()
70+
testutils.AssertEquals(t, "The hostname is not properly passed", tt.hostname, query.Get("hostname"))
71+
testutils.AssertEquals(t, "The port is not properly passed", tt.port, query.Get("port"))
72+
73+
return testutils.GetResponse(tt.statusCode, tt.body)
74+
},
75+
}
76+
77+
errorMessage := ""
78+
if err := removeKnownHost(client, tt.hostname, tt.port); err != nil {
79+
errorMessage = err.Error()
80+
}
81+
testutils.AssertStringContains(t, "Unexpected error message", errorMessage, tt.expectedError)
82+
})
83+
}
84+
}

mgrctl/cmd/ssh/ssh.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// SPDX-FileCopyrightText: 2026 SUSE LLC
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package ssh
6+
7+
import (
8+
"github.com/spf13/cobra"
9+
"github.com/uyuni-project/uyuni-tools/mgrctl/cmd/ssh/knownhost"
10+
"github.com/uyuni-project/uyuni-tools/shared/api"
11+
. "github.com/uyuni-project/uyuni-tools/shared/l10n"
12+
"github.com/uyuni-project/uyuni-tools/shared/types"
13+
)
14+
15+
func NewCommand(globalFlags *types.GlobalFlags) *cobra.Command {
16+
17+
sshCmd := &cobra.Command{
18+
Use: "ssh",
19+
Short: L("SSH management commands"),
20+
}
21+
22+
sshCmd.AddCommand(knownhost.NewKnownHostCommand(globalFlags))
23+
api.AddAPIFlags(sshCmd)
24+
25+
return sshCmd
26+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Add mgrctl commands "ssh" and "ssh remove_known_host"

0 commit comments

Comments
 (0)