2016-11-05 17:09:05 -07:00
|
|
|
/*
|
|
|
|
|
Copyright 2016 The Rook Authors. All rights reserved.
|
|
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
|
limitations under the License.
|
|
|
|
|
*/
|
2020-03-20 11:11:11 +01:00
|
|
|
|
2016-10-12 09:04:07 -07:00
|
|
|
package exec
|
2016-09-15 08:00:43 -07:00
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bufio"
|
2017-09-14 09:40:34 -07:00
|
|
|
"bytes"
|
2017-06-15 18:40:18 -07:00
|
|
|
"fmt"
|
2016-09-15 08:00:43 -07:00
|
|
|
"io"
|
2017-03-23 17:14:05 -07:00
|
|
|
"os"
|
2016-09-15 08:00:43 -07:00
|
|
|
"os/exec"
|
2021-07-06 23:27:15 +02:00
|
|
|
"reflect"
|
|
|
|
|
"strconv"
|
2016-09-15 08:00:43 -07:00
|
|
|
"strings"
|
2017-09-14 09:40:34 -07:00
|
|
|
"time"
|
2016-11-11 17:37:08 -08:00
|
|
|
|
|
|
|
|
"github.com/coreos/pkg/capnslog"
|
2021-01-22 16:00:33 +01:00
|
|
|
"github.com/pkg/errors"
|
2021-07-06 23:27:15 +02:00
|
|
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
|
|
|
|
kexec "k8s.io/utils/exec"
|
|
|
|
|
)
|
|
|
|
|
|
2022-09-09 10:22:55 -06:00
|
|
|
// TimeoutWaitingForMessage can be used to identify if an error is due to a timeout.
|
|
|
|
|
const TimeoutWaitingForMessage = "exec timeout waiting for"
|
|
|
|
|
|
2025-03-13 15:24:58 -07:00
|
|
|
var CephCommandsTimeout = 15 * time.Second
|
2016-09-15 08:00:43 -07:00
|
|
|
|
2026-07-06 16:10:13 +05:30
|
|
|
// outputDrainGracePeriod bounds how long executeCommandWithTimeout waits for the
|
|
|
|
|
// os/exec output-copier goroutines to drain after killing a timed-out process,
|
|
|
|
|
// so a killed command that orphaned a pipe holder cannot block the caller forever.
|
|
|
|
|
var outputDrainGracePeriod = 500 * time.Millisecond
|
|
|
|
|
|
2020-03-20 11:11:11 +01:00
|
|
|
// Executor is the main interface for all the exec commands
|
2016-09-15 08:00:43 -07:00
|
|
|
type Executor interface {
|
2020-03-18 11:53:33 -06:00
|
|
|
ExecuteCommand(command string, arg ...string) error
|
2020-11-06 09:46:09 +01:00
|
|
|
ExecuteCommandWithEnv(env []string, command string, arg ...string) error
|
2020-03-18 11:53:33 -06:00
|
|
|
ExecuteCommandWithOutput(command string, arg ...string) (string, error)
|
|
|
|
|
ExecuteCommandWithCombinedOutput(command string, arg ...string) (string, error)
|
|
|
|
|
ExecuteCommandWithTimeout(timeout time.Duration, command string, arg ...string) (string, error)
|
2022-11-21 16:29:58 +05:30
|
|
|
ExecuteCommandWithStdin(timeout time.Duration, command string, stdin *string, arg ...string) error
|
2016-09-15 08:00:43 -07:00
|
|
|
}
|
|
|
|
|
|
2020-03-20 11:11:11 +01:00
|
|
|
// CommandExecutor is the type of the Executor
|
2021-07-06 23:27:15 +02:00
|
|
|
type CommandExecutor struct{}
|
2016-09-15 08:00:43 -07:00
|
|
|
|
2020-03-20 11:11:11 +01:00
|
|
|
// ExecuteCommand starts a process and wait for its completion
|
|
|
|
|
func (c *CommandExecutor) ExecuteCommand(command string, arg ...string) error {
|
|
|
|
|
return c.ExecuteCommandWithEnv([]string{}, command, arg...)
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-21 16:29:58 +05:30
|
|
|
// ExecuteCommandWithStdin starts a process, provides stdin and wait for its completion with timeout.
|
|
|
|
|
func (c *CommandExecutor) ExecuteCommandWithStdin(timeout time.Duration, command string, stdin *string, arg ...string) error {
|
|
|
|
|
output, err := executeCommandWithTimeout(timeout, command, stdin, arg...)
|
|
|
|
|
logger.Infof("Command %q output: %q", command, output)
|
|
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-20 11:11:11 +01:00
|
|
|
// ExecuteCommandWithEnv starts a process with env variables and wait for its completion
|
|
|
|
|
func (*CommandExecutor) ExecuteCommandWithEnv(env []string, command string, arg ...string) error {
|
|
|
|
|
cmd, stdout, stderr, err := startCommand(env, command, arg...)
|
2016-10-28 17:07:31 -07:00
|
|
|
if err != nil {
|
2020-03-17 18:23:16 -06:00
|
|
|
return err
|
2016-09-15 08:00:43 -07:00
|
|
|
}
|
|
|
|
|
|
2020-03-18 11:53:33 -06:00
|
|
|
logOutput(stdout, stderr)
|
2016-09-15 08:00:43 -07:00
|
|
|
|
|
|
|
|
if err := cmd.Wait(); err != nil {
|
2020-03-17 18:23:16 -06:00
|
|
|
return err
|
2016-09-15 08:00:43 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-09 10:22:55 -06:00
|
|
|
// IsTimeout returns true if the error is due to a timeout in the exec function. Note that it cannot
|
|
|
|
|
// determine if a command timed out due to behavior of the command itself; for example if a
|
|
|
|
|
// '--timeout' flag was passed to the command.
|
|
|
|
|
func IsTimeout(err error) bool {
|
|
|
|
|
return strings.Contains(err.Error(), TimeoutWaitingForMessage)
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-14 09:40:34 -07:00
|
|
|
// ExecuteCommandWithTimeout starts a process and wait for its completion with timeout.
|
2020-03-18 11:53:33 -06:00
|
|
|
func (*CommandExecutor) ExecuteCommandWithTimeout(timeout time.Duration, command string, arg ...string) (string, error) {
|
2022-11-21 16:29:58 +05:30
|
|
|
return executeCommandWithTimeout(timeout, command, nil, arg...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// executeCommandWithTimeout starts a process, provides stdin and wait for its completion with timeout.
|
|
|
|
|
func executeCommandWithTimeout(timeout time.Duration, command string, stdin *string, arg ...string) (string, error) {
|
2020-03-18 11:53:33 -06:00
|
|
|
logCommand(command, arg...)
|
2022-04-04 19:55:41 +05:30
|
|
|
//nolint:gosec // Rook controls the input to the exec arguments
|
2017-09-14 09:40:34 -07:00
|
|
|
cmd := exec.Command(command, arg...)
|
|
|
|
|
|
|
|
|
|
var b bytes.Buffer
|
|
|
|
|
cmd.Stdout = &b
|
|
|
|
|
cmd.Stderr = &b
|
|
|
|
|
|
2022-11-21 16:29:58 +05:30
|
|
|
if stdin != nil {
|
|
|
|
|
cmd.Stdin = strings.NewReader(*stdin)
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-14 09:40:34 -07:00
|
|
|
if err := cmd.Start(); err != nil {
|
2020-03-17 18:23:16 -06:00
|
|
|
return "", err
|
2017-09-14 09:40:34 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
done := make(chan error, 1)
|
|
|
|
|
go func() {
|
|
|
|
|
done <- cmd.Wait()
|
|
|
|
|
}()
|
|
|
|
|
|
2020-03-17 15:58:04 -06:00
|
|
|
interruptSent := false
|
2017-09-14 09:40:34 -07:00
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case <-time.After(timeout):
|
2020-03-17 15:58:04 -06:00
|
|
|
if interruptSent {
|
2022-09-09 10:22:55 -06:00
|
|
|
logger.Infof("%s process %s to return after interrupt signal was sent. Sending kill signal to the process", TimeoutWaitingForMessage, command)
|
2017-09-14 09:40:34 -07:00
|
|
|
if err := cmd.Process.Kill(); err != nil {
|
2020-03-18 11:53:33 -06:00
|
|
|
logger.Errorf("Failed to kill process %s: %v", command, err)
|
2026-07-06 16:10:13 +05:30
|
|
|
// The process may still be running with the output-copier
|
|
|
|
|
// goroutines writing into b, so don't read it here.
|
|
|
|
|
return "", fmt.Errorf("%s the command %s to return; sent an interrupt, then the kill failed: %v", TimeoutWaitingForMessage, command, err)
|
|
|
|
|
}
|
|
|
|
|
// Kill() only signals the process; the os/exec goroutines that copy
|
|
|
|
|
// its output into b are joined only by cmd.Wait() (done). Reading b
|
|
|
|
|
// before done fires races with those writers, so wait for them. Bound
|
|
|
|
|
// the wait, though — a killed process can leave an orphaned descendant
|
|
|
|
|
// holding the output pipe open (a D-state cryptsetup/dmsetup child, or
|
|
|
|
|
// the downstream of a "sh -c '... | head'" pipeline), so cmd.Wait() may
|
|
|
|
|
// never return. Give up on the captured output after a grace period
|
|
|
|
|
// rather than blocking the caller forever.
|
|
|
|
|
select {
|
|
|
|
|
case <-done:
|
|
|
|
|
return strings.TrimSpace(b.String()), fmt.Errorf("%s the command %s to return", TimeoutWaitingForMessage, command)
|
|
|
|
|
case <-time.After(outputDrainGracePeriod):
|
|
|
|
|
return "", fmt.Errorf("%s the command %s to return", TimeoutWaitingForMessage, command)
|
2017-09-14 09:40:34 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-09 10:22:55 -06:00
|
|
|
logger.Infof("%s process %s to return. Sending interrupt signal to the process", TimeoutWaitingForMessage, command)
|
2017-09-14 09:40:34 -07:00
|
|
|
if err := cmd.Process.Signal(os.Interrupt); err != nil {
|
2020-03-18 11:53:33 -06:00
|
|
|
logger.Errorf("Failed to send interrupt signal to process %s: %v", command, err)
|
2017-09-14 09:40:34 -07:00
|
|
|
// kill signal will be sent next loop
|
|
|
|
|
}
|
2020-03-17 15:58:04 -06:00
|
|
|
interruptSent = true
|
2017-09-14 09:40:34 -07:00
|
|
|
case err := <-done:
|
|
|
|
|
if err != nil {
|
2020-03-17 18:23:16 -06:00
|
|
|
return strings.TrimSpace(b.String()), err
|
2017-09-14 09:40:34 -07:00
|
|
|
}
|
2020-03-17 15:58:04 -06:00
|
|
|
if interruptSent {
|
2022-09-09 10:22:55 -06:00
|
|
|
return strings.TrimSpace(b.String()), fmt.Errorf("%s the command %s to return", TimeoutWaitingForMessage, command)
|
2017-09-14 09:40:34 -07:00
|
|
|
}
|
2019-11-07 19:37:32 +08:00
|
|
|
return strings.TrimSpace(b.String()), nil
|
2017-09-14 09:40:34 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-20 11:11:11 +01:00
|
|
|
// ExecuteCommandWithOutput executes a command with output
|
2020-03-18 11:53:33 -06:00
|
|
|
func (*CommandExecutor) ExecuteCommandWithOutput(command string, arg ...string) (string, error) {
|
|
|
|
|
logCommand(command, arg...)
|
2022-04-04 19:55:41 +05:30
|
|
|
//nolint:gosec // Rook controls the input to the exec arguments
|
2016-10-28 17:07:31 -07:00
|
|
|
cmd := exec.Command(command, arg...)
|
2020-03-17 18:23:16 -06:00
|
|
|
return runCommandWithOutput(cmd, false)
|
2017-02-03 13:16:36 -08:00
|
|
|
}
|
|
|
|
|
|
2020-03-20 11:11:11 +01:00
|
|
|
// ExecuteCommandWithCombinedOutput executes a command with combined output
|
2020-03-18 11:53:33 -06:00
|
|
|
func (*CommandExecutor) ExecuteCommandWithCombinedOutput(command string, arg ...string) (string, error) {
|
|
|
|
|
logCommand(command, arg...)
|
2022-04-04 19:55:41 +05:30
|
|
|
//nolint:gosec // Rook controls the input to the exec arguments
|
2017-02-03 13:16:36 -08:00
|
|
|
cmd := exec.Command(command, arg...)
|
2020-03-17 18:23:16 -06:00
|
|
|
return runCommandWithOutput(cmd, true)
|
2016-09-15 08:00:43 -07:00
|
|
|
}
|
|
|
|
|
|
2020-03-20 11:11:11 +01:00
|
|
|
func startCommand(env []string, command string, arg ...string) (*exec.Cmd, io.ReadCloser, io.ReadCloser, error) {
|
2020-03-18 11:53:33 -06:00
|
|
|
logCommand(command, arg...)
|
2016-10-28 17:07:31 -07:00
|
|
|
|
2022-04-04 19:55:41 +05:30
|
|
|
//nolint:gosec // Rook controls the input to the exec arguments
|
2016-10-28 17:07:31 -07:00
|
|
|
cmd := exec.Command(command, arg...)
|
2019-01-23 13:55:58 -08:00
|
|
|
stdout, err := cmd.StdoutPipe()
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Warningf("failed to open stdout pipe: %+v", err)
|
|
|
|
|
}
|
|
|
|
|
stderr, err := cmd.StderrPipe()
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Warningf("failed to open stderr pipe: %+v", err)
|
|
|
|
|
}
|
2016-10-28 17:07:31 -07:00
|
|
|
|
2020-03-20 11:11:11 +01:00
|
|
|
if len(env) > 0 {
|
|
|
|
|
cmd.Env = env
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-23 13:55:58 -08:00
|
|
|
err = cmd.Start()
|
2016-10-28 17:07:31 -07:00
|
|
|
|
|
|
|
|
return cmd, stdout, stderr, err
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-23 13:55:58 -08:00
|
|
|
// read from reader line by line and write it to the log
|
2020-03-18 11:53:33 -06:00
|
|
|
func logFromReader(logger *capnslog.PackageLogger, reader io.ReadCloser) {
|
2021-06-22 16:02:54 +02:00
|
|
|
l := logger.Debug
|
|
|
|
|
// If we are an OSD we must log using Info to print out stdout/stderr
|
|
|
|
|
if os.Getenv("ROOK_OSD_ID") != "" {
|
|
|
|
|
l = logger.Info
|
|
|
|
|
}
|
2019-01-23 13:55:58 -08:00
|
|
|
in := bufio.NewScanner(reader)
|
|
|
|
|
lastLine := ""
|
|
|
|
|
for in.Scan() {
|
|
|
|
|
lastLine = in.Text()
|
2021-06-22 16:02:54 +02:00
|
|
|
l(lastLine)
|
2019-01-23 13:55:58 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-18 11:53:33 -06:00
|
|
|
func logOutput(stdout, stderr io.ReadCloser) {
|
2016-10-28 17:07:31 -07:00
|
|
|
if stdout == nil || stderr == nil {
|
2016-11-11 17:37:08 -08:00
|
|
|
logger.Warningf("failed to collect stdout and stderr")
|
2016-10-28 17:07:31 -07:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2016-11-18 08:27:18 -08:00
|
|
|
// The child processes should appropriately be outputting at the desired global level. Therefore,
|
2016-11-11 17:37:08 -08:00
|
|
|
// we always log at INFO level here, so that log statements from child procs at higher levels
|
|
|
|
|
// (e.g., WARNING) will still be displayed. We are relying on the child procs to output appropriately.
|
2020-03-17 18:23:16 -06:00
|
|
|
childLogger := capnslog.NewPackageLogger("github.com/rook/rook", "exec")
|
2016-11-18 08:27:18 -08:00
|
|
|
if !childLogger.LevelAt(capnslog.INFO) {
|
2016-11-11 17:37:08 -08:00
|
|
|
rl, err := capnslog.GetRepoLogger("github.com/rook/rook")
|
|
|
|
|
if err == nil {
|
2020-03-17 18:23:16 -06:00
|
|
|
rl.SetLogLevel(map[string]capnslog.LogLevel{"exec": capnslog.INFO})
|
2016-11-11 17:37:08 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-18 11:53:33 -06:00
|
|
|
go logFromReader(childLogger, stderr)
|
|
|
|
|
logFromReader(childLogger, stdout)
|
2016-10-28 17:07:31 -07:00
|
|
|
}
|
|
|
|
|
|
2020-03-17 18:23:16 -06:00
|
|
|
func runCommandWithOutput(cmd *exec.Cmd, combinedOutput bool) (string, error) {
|
2017-02-03 13:16:36 -08:00
|
|
|
var output []byte
|
|
|
|
|
var err error
|
2020-06-18 17:25:58 +02:00
|
|
|
var out string
|
2017-02-03 13:16:36 -08:00
|
|
|
|
|
|
|
|
if combinedOutput {
|
|
|
|
|
output, err = cmd.CombinedOutput()
|
|
|
|
|
} else {
|
|
|
|
|
output, err = cmd.Output()
|
2020-06-18 17:25:58 +02:00
|
|
|
if err != nil {
|
2020-10-28 10:54:20 +01:00
|
|
|
output = []byte(fmt.Sprintf("%s. %s", string(output), assertErrorType(err)))
|
2020-06-18 17:25:58 +02:00
|
|
|
}
|
2017-02-03 13:16:36 -08:00
|
|
|
}
|
|
|
|
|
|
2020-06-18 17:25:58 +02:00
|
|
|
out = strings.TrimSpace(string(output))
|
2017-06-14 16:07:36 -07:00
|
|
|
|
2016-09-15 08:00:43 -07:00
|
|
|
if err != nil {
|
2020-06-18 17:25:58 +02:00
|
|
|
return out, err
|
2016-09-15 08:00:43 -07:00
|
|
|
}
|
|
|
|
|
|
2017-06-14 16:07:36 -07:00
|
|
|
return out, nil
|
2016-09-15 08:00:43 -07:00
|
|
|
}
|
|
|
|
|
|
2020-03-18 11:53:33 -06:00
|
|
|
func logCommand(command string, arg ...string) {
|
|
|
|
|
logger.Debugf("Running command: %s %s", command, strings.Join(arg, " "))
|
2016-09-15 08:00:43 -07:00
|
|
|
}
|
2020-10-28 10:54:20 +01:00
|
|
|
|
|
|
|
|
func assertErrorType(err error) string {
|
|
|
|
|
switch errType := err.(type) {
|
|
|
|
|
case *exec.ExitError:
|
|
|
|
|
return string(errType.Stderr)
|
|
|
|
|
case *exec.Error:
|
|
|
|
|
return errType.Error()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ""
|
|
|
|
|
}
|
2021-04-12 14:10:26 -06:00
|
|
|
|
|
|
|
|
// ExtractExitCode attempts to get the exit code from the error returned by an Executor function.
|
2021-07-06 23:27:15 +02:00
|
|
|
// This should also work for any errors returned by the golang os/exec package and "k8s.io/utils/exec"
|
2021-04-12 14:10:26 -06:00
|
|
|
func ExtractExitCode(err error) (int, error) {
|
2021-07-06 23:27:15 +02:00
|
|
|
switch errType := err.(type) {
|
|
|
|
|
case *exec.ExitError:
|
|
|
|
|
return errType.ExitCode(), nil
|
|
|
|
|
|
|
|
|
|
case *kexec.CodeExitError:
|
|
|
|
|
return errType.ExitStatus(), nil
|
|
|
|
|
|
2021-09-17 17:37:01 -06:00
|
|
|
// have to check both *kexec.CodeExitError and kexec.CodeExitError because CodeExitError methods
|
|
|
|
|
// are not defined with pointer receivers; both pointer and non-pointers are valid `error`s.
|
|
|
|
|
case kexec.CodeExitError:
|
|
|
|
|
return errType.ExitStatus(), nil
|
|
|
|
|
|
2021-07-06 23:27:15 +02:00
|
|
|
case *kerrors.StatusError:
|
|
|
|
|
return int(errType.ErrStatus.Code), nil
|
|
|
|
|
|
|
|
|
|
default:
|
2024-12-10 20:35:53 +01:00
|
|
|
logger.Debugf("%s", err.Error())
|
2021-09-17 17:37:01 -06:00
|
|
|
// This is ugly, but it's a decent backup just in case the error isn't a type above.
|
2021-07-06 23:27:15 +02:00
|
|
|
if strings.Contains(err.Error(), "command terminated with exit code") {
|
|
|
|
|
a := strings.SplitAfter(err.Error(), "command terminated with exit code")
|
|
|
|
|
return strconv.Atoi(strings.TrimSpace(a[1]))
|
|
|
|
|
}
|
2021-09-17 17:37:01 -06:00
|
|
|
return -1, errors.Errorf("error %#v is an unknown error type: %v", err, reflect.TypeOf(err))
|
2021-04-12 14:10:26 -06:00
|
|
|
}
|
|
|
|
|
}
|