diff --git a/common_utils/context.go b/common_utils/context.go index fdb3011d2..740f7e655 100644 --- a/common_utils/context.go +++ b/common_utils/context.go @@ -49,6 +49,8 @@ const ( GNOI_HEALTHZ_ACK GNOI_HEALTHZ_CHECK GNOI_HEALTHZ_COLLECT + GNSI_CREDZ_SET + GNSI_CREDZ_CHECKPOINT DBUS DBUS_FAIL DBUS_APPLY_PATCH_DB @@ -95,6 +97,10 @@ func (c CounterType) String() string { return "GNOI Healthz Check" case GNOI_HEALTHZ_COLLECT: return "GNOI Healthz Collect" + case GNSI_CREDZ_SET: + return "GNSI Credz Set" + case GNSI_CREDZ_CHECKPOINT: + return "GNSI Credz Checkpoint" case DBUS: return "DBUS" case DBUS_FAIL: diff --git a/sonic_service_client/dbus_client.go b/sonic_service_client/dbus_client.go index 9032481f6..8d380ee42 100644 --- a/sonic_service_client/dbus_client.go +++ b/sonic_service_client/dbus_client.go @@ -1,6 +1,7 @@ package host_service import ( + "context" "fmt" "reflect" "strings" @@ -49,8 +50,37 @@ type Service interface { // Docker services APIs LoadDockerImage(image string) error InstallOS(req string) (string, error) + //Credentialz service APIs + SSHMgmtSet(cmd string) error + GLOMEConfigSet(ctx context.Context, cmd string) error + SSHCheckpoint(action CredzCheckpointAction) error + GLOMERestoreCheckpoint(ctx context.Context) error + ConsoleSet(cmd string) error + ConsoleCheckpoint(action CredzCheckpointAction) error } +// Define a function type that matches the DbusApi signature +type DbusApiFunc func(busName, busPath, intName string, timeout int, args ...interface{}) (interface{}, error) + +// Use a variable to hold the implementation. It defaults to the real DbusApi function. +var dbusApiCaller DbusApiFunc = DbusApi + +// NewDbusClientFunc defines the signature for creating a D-Bus client. +type NewDbusClientFunc func() (Service, error) + +// NewDbusClientProvider is a variable that defaults to the real constructor. +// Tests can overwrite this to return a FakeClient. +var NewDbusClientProvider NewDbusClientFunc = NewDbusClient + +type CredzCheckpointAction string + +const ( + CredzCPCreate CredzCheckpointAction = ".create_checkpoint" + CredzCPDelete CredzCheckpointAction = ".delete_checkpoint" + CredzCPRestore CredzCheckpointAction = ".restore_checkpoint" + CredzGlomePushConfig CredzCheckpointAction = ".push_config" +) + type DbusClient struct { busNamePrefix string busPathPrefix string @@ -424,3 +454,96 @@ func (c *DbusClient) HealthzAck(req string) (string, error) { } return strResult, nil } + +func (c *DbusClient) ConsoleSet(cmd string) error { + modName := "gnsi_console" + busName := c.busNamePrefix + modName + busPath := c.busPathPrefix + modName + intName := c.intNamePrefix + modName + ".set" + + common_utils.IncCounter(common_utils.GNSI_CREDZ_SET) + _, err := dbusApiCaller(busName, busPath, intName, 10, cmd) + return err +} + +func (c *DbusClient) SSHMgmtSet(cmd string) error { + modName := "ssh_mgmt" + busName := c.busNamePrefix + modName + busPath := c.busPathPrefix + modName + intName := c.intNamePrefix + modName + ".set" + + common_utils.IncCounter(common_utils.GNSI_CREDZ_SET) + _, err := dbusApiCaller(busName, busPath, intName, 10, cmd) + return err +} + +// GLOMEConfigSet is used to write the GLOME config in the host service file system. +func (c *DbusClient) GLOMEConfigSet(ctx context.Context, cmd string) error { + modName := "glome" + busName := c.busNamePrefix + modName + busPath := c.busPathPrefix + modName + intName := c.intNamePrefix + modName + string(CredzGlomePushConfig) + + common_utils.IncCounter(common_utils.GNSI_CREDZ_SET) + timeout := 10 // Default timeout in seconds. + if deadline, ok := ctx.Deadline(); ok { + remaining := time.Until(deadline) + if remaining <= 0 { + return context.DeadlineExceeded + } + timeout = int(remaining.Seconds()) + if timeout > 10 { + timeout = 10 + } + } + _, err := dbusApiCaller(busName, busPath, intName, timeout, cmd) + return err +} + +func (c *DbusClient) ConsoleCheckpoint(action CredzCheckpointAction) error { + modName := "gnsi_console" + busName := c.busNamePrefix + modName + busPath := c.busPathPrefix + modName + intName := c.intNamePrefix + modName + string(action) + + common_utils.IncCounter(common_utils.GNSI_CREDZ_CHECKPOINT) + _, err := dbusApiCaller(busName, busPath, intName, 10, "") + return err +} + +func (c *DbusClient) SSHCheckpoint(action CredzCheckpointAction) error { + modName := "ssh_mgmt" + busName := c.busNamePrefix + modName + busPath := c.busPathPrefix + modName + intName := c.intNamePrefix + modName + string(action) + + common_utils.IncCounter(common_utils.GNSI_CREDZ_CHECKPOINT) + _, err := dbusApiCaller(busName, busPath, intName, 10, "") + return err +} + +// GLOMERestoreCheckpoint is used to restore the GLOME config metadata to the +// checkpoint state. This is used to rollback the GLOME config in the host +// service file system. +func (c *DbusClient) GLOMERestoreCheckpoint(ctx context.Context) error { + modName := "glome" + busName := c.busNamePrefix + modName + busPath := c.busPathPrefix + modName + intName := c.intNamePrefix + modName + string(CredzCPRestore) + + common_utils.IncCounter(common_utils.GNSI_CREDZ_CHECKPOINT) + // Default timeout in seconds. Set to 5 minutes to give enough time for rollback. + timeout := 300 + if deadline, ok := ctx.Deadline(); ok { + remaining := time.Until(deadline) + if remaining <= 0 { + return context.DeadlineExceeded + } + timeout = int(remaining.Seconds()) + if timeout > 10 { + timeout = 10 + } + } + _, err := dbusApiCaller(busName, busPath, intName, timeout) + return err +} diff --git a/sonic_service_client/dbus_client_test.go b/sonic_service_client/dbus_client_test.go index b8b28c4a4..b27b002e8 100644 --- a/sonic_service_client/dbus_client_test.go +++ b/sonic_service_client/dbus_client_test.go @@ -1,6 +1,7 @@ package host_service import ( + "context" "errors" "fmt" "github.com/agiledragon/gomonkey/v2" @@ -11,6 +12,7 @@ import ( "reflect" "strings" "testing" + "time" ) func TestNewDbusClient(t *testing.T) { @@ -1656,3 +1658,133 @@ func TestHealthzAck_InvalidReturnType(t *testing.T) { assert.Contains(t, err.Error(), "Invalid result type") assert.Equal(t, "", result) } + +func TestCredentialzDbusMethods(t *testing.T) { + client := &DbusClient{ + busNamePrefix: "org.SONiC.HostService.", + busPathPrefix: "/org/SONiC/HostService/", + intNamePrefix: "org.SONiC.HostService.", + } + // Save the original implementation to restore it later + originalDbusApi := dbusApiCaller + defer func() { dbusApiCaller = originalDbusApi }() + + t.Run("ConsoleSet", func(t *testing.T) { + expectedCmd := "test-password-json" + // Overwrite the caller variable with a mock + dbusApiCaller = func(bus, path, intf string, timeout int, args ...interface{}) (interface{}, error) { + assert.Equal(t, "org.SONiC.HostService.gnsi_console", bus) + assert.Equal(t, "org.SONiC.HostService.gnsi_console.set", intf) + assert.Equal(t, expectedCmd, args[0]) + return nil, nil + } + + err := client.ConsoleSet(expectedCmd) + assert.NoError(t, err) + }) + + t.Run("SSHMgmtSet", func(t *testing.T) { + expectedCmd := "test-ssh-key-json" + dbusApiCaller = func(bus, path, intf string, timeout int, args ...interface{}) (interface{}, error) { + assert.Equal(t, "org.SONiC.HostService.ssh_mgmt", bus) + assert.Equal(t, "org.SONiC.HostService.ssh_mgmt.set", intf) + assert.Equal(t, expectedCmd, args[0]) + return nil, nil + } + + err := client.SSHMgmtSet(expectedCmd) + assert.NoError(t, err) + }) + + t.Run("SSHCheckpoint", func(t *testing.T) { + dbusApiCaller = func(bus, path, intf string, timeout int, args ...interface{}) (interface{}, error) { + assert.Equal(t, "org.SONiC.HostService.ssh_mgmt.create_checkpoint", intf) + assert.Equal(t, "", args[0]) + return nil, nil + } + + err := client.SSHCheckpoint(CredzCPCreate) + assert.NoError(t, err) + }) + + t.Run("GLOMEConfigSetWithContext", func(t *testing.T) { + expectedCmd := "glome-json" + // Create a context with a 5-second deadline to test timeout calculation + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + dbusApiCaller = func(bus, path, intf string, timeout int, args ...interface{}) (interface{}, error) { + assert.Equal(t, "org.SONiC.HostService.glome.push_config", intf) + assert.Equal(t, expectedCmd, args[0]) + // Timeout should be roughly 5 (based on ctx) + assert.True(t, timeout <= 5) + return nil, nil + } + + err := client.GLOMEConfigSet(ctx, expectedCmd) + assert.NoError(t, err) + }) + + t.Run("GLOMERestoreCheckpoint", func(t *testing.T) { + dbusApiCaller = func(bus, path, intf string, timeout int, args ...interface{}) (interface{}, error) { + + assert.Equal(t, "org.SONiC.HostService.glome.restore_checkpoint", intf) + // Restore checkpoint for GLOME often has a high default timeout (300s) + assert.Equal(t, 300, timeout) + return nil, nil + } + err := client.GLOMERestoreCheckpoint(context.Background()) + assert.NoError(t, err) + }) +} + +func TestConsoleCheckpoint(t *testing.T) { + client := &DbusClient{ + busNamePrefix: "org.SONiC.HostService.", + busPathPrefix: "/org/SONiC/HostService/", + intNamePrefix: "org.SONiC.HostService.", + } + originalDbusApi := dbusApiCaller + defer func() { dbusApiCaller = originalDbusApi }() + + tests := []struct { + name string + action CredzCheckpointAction + wantIntf string + }{ + { + name: "Console Create Checkpoint", + action: CredzCPCreate, + wantIntf: "org.SONiC.HostService.gnsi_console.create_checkpoint", + }, + { + name: "Console Delete Checkpoint", + action: CredzCPDelete, + wantIntf: "org.SONiC.HostService.gnsi_console.delete_checkpoint", + }, + { + name: "Console Restore Checkpoint", + action: CredzCPRestore, + wantIntf: "org.SONiC.HostService.gnsi_console.restore_checkpoint", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dbusApiCaller = func(bus, path, intf string, timeout int, args ...interface{}) (interface{}, error) { + assert.Equal(t, "org.SONiC.HostService.gnsi_console", bus) + assert.Equal(t, "/org/SONiC/HostService/gnsi_console", path) + assert.Equal(t, tt.wantIntf, intf) + assert.Equal(t, 10, timeout) + + // Verify ConsoleCheckpoint passes an empty string as the payload + assert.Len(t, args, 1) + assert.Equal(t, "", args[0]) + + return nil, nil + } + + err := client.ConsoleCheckpoint(tt.action) + assert.NoError(t, err) + }) + } +} diff --git a/sonic_service_client/dbus_fake_client.go b/sonic_service_client/dbus_fake_client.go index c8ccc394d..66f93f7ca 100644 --- a/sonic_service_client/dbus_fake_client.go +++ b/sonic_service_client/dbus_fake_client.go @@ -1,6 +1,7 @@ package host_service import ( + "context" "errors" "fmt" ) @@ -8,6 +9,7 @@ import ( // FakeClient is a mock implementation of the Service interface. type FakeClient struct { CollectResponse string + Command chan []string } func (f *FakeClient) Close() error { return nil } @@ -90,3 +92,33 @@ func (f *FakeClient) HealthzAck(req string) (string, error) { func (f *FakeClientWithError) HealthzAck(req string) (string, error) { return "", fmt.Errorf("simulated dbus error") } + +func (f *FakeClient) ConsoleCheckpoint(action CredzCheckpointAction) error { + f.Command <- []string{"gnsi_console" + string(action), ""} + return nil +} + +func (f *FakeClient) ConsoleSet(cmd string) error { + f.Command <- []string{"gnsi_console.set", cmd} + return nil +} + +func (f *FakeClient) SSHCheckpoint(action CredzCheckpointAction) error { + f.Command <- []string{"ssh_mgmt" + string(action), ""} + return nil +} + +func (f *FakeClient) SSHMgmtSet(cmd string) error { + f.Command <- []string{"ssh_mgmt.set", cmd} + return nil +} + +func (f *FakeClient) GLOMEConfigSet(ctx context.Context, cmd string) error { + f.Command <- []string{"glome" + string(CredzGlomePushConfig), cmd} + return nil +} + +func (f *FakeClient) GLOMERestoreCheckpoint(ctx context.Context) error { + f.Command <- []string{"glome" + string(CredzCPRestore), ""} + return nil +} diff --git a/sonic_service_client/dbus_fake_client_test.go b/sonic_service_client/dbus_fake_client_test.go index fa379abd4..4bc216873 100644 --- a/sonic_service_client/dbus_fake_client_test.go +++ b/sonic_service_client/dbus_fake_client_test.go @@ -1,13 +1,16 @@ package host_service import ( + "context" "testing" "github.com/stretchr/testify/assert" ) func TestFakeClientMethods(t *testing.T) { - client := &FakeClient{} + client := &FakeClient{ + Command: make(chan []string, 10), + } assert.NoError(t, client.Close()) assert.NoError(t, client.ConfigReload("test.conf")) @@ -75,4 +78,51 @@ func TestFakeClientMethods(t *testing.T) { assert.Error(t, err) assert.Equal(t, "", output) assert.Equal(t, "request cannot be empty", err.Error()) + + // --- Credentialz Fake Tests --- + + t.Run("SSHCheckpoint", func(t *testing.T) { + err := client.SSHCheckpoint(CredzCPCreate) + assert.NoError(t, err) + msg := <-client.Command + assert.Equal(t, []string{"ssh_mgmt.create_checkpoint", ""}, msg) + }) + + t.Run("SSHMgmtSet", func(t *testing.T) { + testCmd := `{"SshAccountKeys": []}` + err := client.SSHMgmtSet(testCmd) + assert.NoError(t, err) + msg := <-client.Command + assert.Equal(t, []string{"ssh_mgmt.set", testCmd}, msg) + }) + + t.Run("ConsoleCheckpoint", func(t *testing.T) { + err := client.ConsoleCheckpoint(CredzCPRestore) + assert.NoError(t, err) + msg := <-client.Command + assert.Equal(t, []string{"gnsi_console.restore_checkpoint", ""}, msg) + }) + + t.Run("ConsoleSet", func(t *testing.T) { + testCmd := `{"ConsolePasswords": []}` + err := client.ConsoleSet(testCmd) + assert.NoError(t, err) + msg := <-client.Command + assert.Equal(t, []string{"gnsi_console.set", testCmd}, msg) + }) + + t.Run("GLOMEConfigSet", func(t *testing.T) { + testCmd := `{"enabled": true}` + err := client.GLOMEConfigSet(context.Background(), testCmd) + assert.NoError(t, err) + msg := <-client.Command + assert.Equal(t, []string{"glome.push_config", testCmd}, msg) + }) + + t.Run("GLOMERestoreCheckpoint", func(t *testing.T) { + err := client.GLOMERestoreCheckpoint(context.Background()) + assert.NoError(t, err) + msg := <-client.Command + assert.Equal(t, []string{"glome.restore_checkpoint", ""}, msg) + }) }