diff --git a/hivesim/hive.go b/hivesim/hive.go index 35ba7f23d4..c70c4d445b 100644 --- a/hivesim/hive.go +++ b/hivesim/hive.go @@ -173,6 +173,26 @@ func (sim *Simulation) StopClient(testSuite SuiteID, test TestID, nodeid string) return err } +// PauseClient signals to the host that the node needs to be paused. +func (sim *Simulation) PauseClient(testSuite SuiteID, test TestID, nodeid string) error { + req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/testsuite/%d/test/%d/node/%s/pause", sim.url, testSuite, test, nodeid), nil) + if err != nil { + return err + } + _, err = http.DefaultClient.Do(req) + return err +} + +// UnpauseClient signals to the host that the node needs to be unpaused. +func (sim *Simulation) UnpauseClient(testSuite SuiteID, test TestID, nodeid string) error { + req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/testsuite/%d/test/%d/node/%s/pause", sim.url, testSuite, test, nodeid), nil) + if err != nil { + return err + } + _, err = http.DefaultClient.Do(req) + return err +} + // ClientEnodeURL returns the enode URL of a running client. func (sim *Simulation) ClientEnodeURL(testSuite SuiteID, test TestID, node string) (string, error) { return sim.ClientEnodeURLNetwork(testSuite, test, node, "bridge") diff --git a/hivesim/testapi.go b/hivesim/testapi.go index 200cb8136b..9485f33c8d 100644 --- a/hivesim/testapi.go +++ b/hivesim/testapi.go @@ -170,6 +170,16 @@ func (c *Client) Exec(command ...string) (*ExecInfo, error) { return c.test.Sim.ClientExec(c.test.SuiteID, c.test.TestID, c.Container, command) } +// Pauses the client container. +func (c *Client) Pause() error { + return c.test.Sim.PauseClient(c.test.SuiteID, c.test.TestID, c.Container) +} + +// Unpauses the client container. +func (c *Client) Unpause() error { + return c.test.Sim.UnpauseClient(c.test.SuiteID, c.test.TestID, c.Container) +} + // T is a running test. This is a lot like testing.T, but has some additional methods for // launching clients. // diff --git a/internal/fakes/container.go b/internal/fakes/container.go index 7e70c38433..ccda8d1c66 100644 --- a/internal/fakes/container.go +++ b/internal/fakes/container.go @@ -14,10 +14,12 @@ import ( // BackendHooks can be used to override the behavior of the fake backend. type BackendHooks struct { - CreateContainer func(image string, opt libhive.ContainerOptions) (string, error) - StartContainer func(image, containerID string, opt libhive.ContainerOptions) (*libhive.ContainerInfo, error) - DeleteContainer func(containerID string) error - RunProgram func(containerID string, cmd []string) (*libhive.ExecInfo, error) + CreateContainer func(image string, opt libhive.ContainerOptions) (string, error) + StartContainer func(image, containerID string, opt libhive.ContainerOptions) (*libhive.ContainerInfo, error) + DeleteContainer func(containerID string) error + PauseContainer func(containerID string) error + UnpauseContainer func(containerID string) error + RunProgram func(containerID string, cmd []string) (*libhive.ExecInfo, error) NetworkNameToID func(string) (string, error) CreateNetwork func(string) (string, error) @@ -145,6 +147,20 @@ func (b *fakeBackend) DeleteContainer(containerID string) error { return err } +func (b *fakeBackend) PauseContainer(containerID string) error { + if b.hooks.PauseContainer != nil { + return b.hooks.PauseContainer(containerID) + } + return nil +} + +func (b *fakeBackend) UnpauseContainer(containerID string) error { + if b.hooks.UnpauseContainer != nil { + return b.hooks.UnpauseContainer(containerID) + } + return nil +} + func (b *fakeBackend) RunProgram(ctx context.Context, containerID string, cmd []string) (*libhive.ExecInfo, error) { if b.hooks.RunProgram != nil { return b.hooks.RunProgram(containerID, cmd) diff --git a/internal/libdocker/container.go b/internal/libdocker/container.go index 849bd7bae4..ef723b8428 100644 --- a/internal/libdocker/container.go +++ b/internal/libdocker/container.go @@ -202,6 +202,26 @@ func (b *ContainerBackend) DeleteContainer(containerID string) error { return err } +// PauseContainer pauses the given container. +func (b *ContainerBackend) PauseContainer(containerID string) error { + b.logger.Debug("pausing container", "container", containerID[:8]) + err := b.client.PauseContainer(containerID) + if err != nil { + b.logger.Error("can't pause container", "container", containerID[:8], "err", err) + } + return err +} + +// UnpauseContainer unpauses the given container. +func (b *ContainerBackend) UnpauseContainer(containerID string) error { + b.logger.Debug("unpausing container", "container", containerID[:8]) + err := b.client.UnpauseContainer(containerID) + if err != nil { + b.logger.Error("can't unpause container", "container", containerID[:8], "err", err) + } + return err +} + // CreateNetwork creates a docker network. func (b *ContainerBackend) CreateNetwork(name string) (string, error) { network, err := b.client.CreateNetwork(docker.CreateNetworkOptions{ diff --git a/internal/libhive/api.go b/internal/libhive/api.go index 227004eeed..d0a051e3fa 100644 --- a/internal/libhive/api.go +++ b/internal/libhive/api.go @@ -38,6 +38,8 @@ func newSimulationAPI(b ContainerBackend, env SimEnv, tm *TestManager) http.Hand router.HandleFunc("/testsuite/{suite}/test/{test}/node/{node}", api.getNodeStatus).Methods("GET") router.HandleFunc("/testsuite/{suite}/test/{test}/node", api.startClient).Methods("POST") router.HandleFunc("/testsuite/{suite}/test/{test}/node/{node}", api.stopClient).Methods("DELETE") + router.HandleFunc("/testsuite/{suite}/test/{test}/node/{node}/pause", api.pauseClient).Methods("POST") + router.HandleFunc("/testsuite/{suite}/test/{test}/node/{node}/pause", api.unpauseClient).Methods("DELETE") router.HandleFunc("/testsuite/{suite}/test", api.startTest).Methods("POST") // post because the delete http verb does not always support a message body router.HandleFunc("/testsuite/{suite}/test/{test}", api.endTest).Methods("POST") @@ -367,6 +369,46 @@ func (api *simAPI) stopClient(w http.ResponseWriter, r *http.Request) { } } +// pauseClient pauses a client container. +func (api *simAPI) pauseClient(w http.ResponseWriter, r *http.Request) { + _, testID, err := api.requestSuiteAndTest(r) + if err != nil { + serveError(w, err, http.StatusBadRequest) + return + } + node := mux.Vars(r)["node"] + + err = api.tm.PauseNode(testID, node) + switch { + case err == ErrNoSuchNode: + serveError(w, err, http.StatusNotFound) + case err != nil: + serveError(w, err, http.StatusInternalServerError) + default: + serveOK(w) + } +} + +// unpauseClient unpauses a client container. +func (api *simAPI) unpauseClient(w http.ResponseWriter, r *http.Request) { + _, testID, err := api.requestSuiteAndTest(r) + if err != nil { + serveError(w, err, http.StatusBadRequest) + return + } + node := mux.Vars(r)["node"] + + err = api.tm.UnpauseNode(testID, node) + switch { + case err == ErrNoSuchNode: + serveError(w, err, http.StatusNotFound) + case err != nil: + serveError(w, err, http.StatusInternalServerError) + default: + serveOK(w) + } +} + // getNodeStatus returns the status of a client container. func (api *simAPI) getNodeStatus(w http.ResponseWriter, r *http.Request) { suiteID, testID, err := api.requestSuiteAndTest(r) diff --git a/internal/libhive/dockerface.go b/internal/libhive/dockerface.go index 5b21d2de28..a7832ec509 100644 --- a/internal/libhive/dockerface.go +++ b/internal/libhive/dockerface.go @@ -23,6 +23,8 @@ type ContainerBackend interface { CreateContainer(ctx context.Context, image string, opt ContainerOptions) (string, error) StartContainer(ctx context.Context, containerID string, opt ContainerOptions) (*ContainerInfo, error) DeleteContainer(containerID string) error + PauseContainer(containerID string) error + UnpauseContainer(containerID string) error // RunProgram runs a command in the given container and returns its outputs and exit code. RunProgram(ctx context.Context, containerID string, cmdline []string) (*ExecInfo, error) diff --git a/internal/libhive/testmanager.go b/internal/libhive/testmanager.go index ac4bf3d085..25fc019767 100644 --- a/internal/libhive/testmanager.go +++ b/internal/libhive/testmanager.go @@ -485,6 +485,46 @@ func (manager *TestManager) StopNode(testID TestID, nodeID string) error { return nil } +// PauseNode pauses a client container. +func (manager *TestManager) PauseNode(testID TestID, nodeID string) error { + manager.testCaseMutex.Lock() + defer manager.testCaseMutex.Unlock() + + testCase, ok := manager.runningTestCases[testID] + if !ok { + return ErrNoSuchNode + } + nodeInfo, ok := testCase.ClientInfo[nodeID] + if !ok { + return ErrNoSuchNode + } + // Pause the container. + if err := manager.backend.PauseContainer(nodeInfo.ID); err != nil { + return fmt.Errorf("unable to pause client: %v", err) + } + return nil +} + +// UnpauseNode unpauses a client container. +func (manager *TestManager) UnpauseNode(testID TestID, nodeID string) error { + manager.testCaseMutex.Lock() + defer manager.testCaseMutex.Unlock() + + testCase, ok := manager.runningTestCases[testID] + if !ok { + return ErrNoSuchNode + } + nodeInfo, ok := testCase.ClientInfo[nodeID] + if !ok { + return ErrNoSuchNode + } + // Unpause the container. + if err := manager.backend.UnpauseContainer(nodeInfo.ID); err != nil { + return fmt.Errorf("unable to unpause client: %v", err) + } + return nil +} + // writeSuiteFile writes the simulation result to the log directory. func writeSuiteFile(s *TestSuite, logdir string) error { suiteData, err := json.Marshal(s)