Skip to content

Commit

Permalink
coredump: support options in pack subcommand
Browse files Browse the repository at this point in the history
-e,--executable path  Tarantool executable path
-p,--pid pid          PID of the dumped process
-t,--time time        Time of dump (seconds since the Epoch)

Closes #763
  • Loading branch information
elhimov committed Dec 17, 2024
1 parent 5ac981c commit 2228bd5
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 52 deletions.
4 changes: 4 additions & 0 deletions .github/actions/prepare-ce-test-env/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,7 @@ runs:
TT_CLI_BUILD_SSL: 'static'
run: mage build
shell: bash

- name: Install test requirements
run: |
sudo apt -y gdb
4 changes: 4 additions & 0 deletions .github/actions/prepare-ee-test-env/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,7 @@ runs:
TT_CLI_BUILD_SSL: 'static'
run: mage build
shell: bash

- name: Install test requirements
run: |
sudo apt -y gdb
6 changes: 6 additions & 0 deletions .github/workflows/full-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ jobs:
run: sudo systemctl kill mono-xsp4 || true

- name: Integration tests
env:
TT_ENABLE_COREDUMP_TESTS: '1'
run: mage integrationfull

full-ci-ce-linux-arm64:
Expand Down Expand Up @@ -83,6 +85,8 @@ jobs:
run: mage unit:fullSkipDocker

- name: Integration tests
env:
TT_ENABLE_COREDUMP_TESTS: '1'
run: mage integrationfullskipdocker

full-ci-sdk:
Expand Down Expand Up @@ -139,6 +143,8 @@ jobs:
run: sudo systemctl kill mono-xsp4 || true

- name: Integration tests
env:
TT_ENABLE_COREDUMP_TESTS: '1'
run: mage integrationfull

full-ci-macOS:
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
an extra path with modules.
- `tt play`: support connection to a target instance by `application` name
or `application:instance` name.
- `tt coredump pack`: add several options to customize dump processing:
* `-e (--executable)`: specify Tarantool executable path.
* `-p (--pid)`: specify PID of the dumped process.
* `-t (--time)`: specify time of dump (seconds since the Epoch).

### Changed

- `tt coredump pack`: by default tarantool executable path is obtained from
`PATH` instead of using the hardcoded path `/usr/bin/tarantool`.

### Fixed

- `tt coredump inspect`: fails for tarantool-ee coredump archive if the source
Expand Down
43 changes: 31 additions & 12 deletions cli/cmd/coredump.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,17 @@ import (
"github.com/tarantool/tt/cli/util"
)

var (
coredumpPackExecutable string
coredumpPackPID uint
coredumpPackTime string
coredumpPackOutputDirectory string
coredumpInspectSourceDir string
)

// NewCoredumpCmd creates coredump command.
func NewCoredumpCmd() *cobra.Command {
var coredumpCmd = &cobra.Command{
var cmd = &cobra.Command{
Use: "coredump",
Short: "Perform manipulations with the tarantool coredumps",
}
Expand All @@ -17,12 +25,28 @@ func NewCoredumpCmd() *cobra.Command {
Use: "pack COREDUMP",
Short: "pack tarantool coredump into tar.gz archive",
Run: func(cmd *cobra.Command, args []string) {
if err := coredump.Pack(args[0]); err != nil {
err := coredump.Pack(args[0],
coredumpPackExecutable,
coredumpPackOutputDirectory,
coredumpPackPID,
coredumpPackTime,
)
if err != nil {
util.HandleCmdErr(cmd, err)
}
},
Args: cobra.ExactArgs(1),
}
packCmd.Flags().StringVarP(&coredumpPackExecutable, "executable", "e", "",
"Tarantool executable path")
packCmd.Flags().StringVarP(&coredumpPackOutputDirectory, "directory", "d", "",
"Directory the resulting archive is created in")
packCmd.Flags().StringVarP(&coredumpPackTime, "time", "t", "",
"Time of dump, expressed as seconds since the Epoch (1970-01-01 00:00 UTC)")
packCmd.Flags().UintVarP(&coredumpPackPID, "pid", "p", 0,
"PID of the dumped process, as seen in the PID namespace in which\n"+
"the given process resides (see %p in core(5) for more info). This flag\n"+
"is to be used when tt is used as kernel.core_pattern pipeline script")

var unpackCmd = &cobra.Command{
Use: "unpack ARCHIVE",
Expand All @@ -35,29 +59,24 @@ func NewCoredumpCmd() *cobra.Command {
Args: cobra.ExactArgs(1),
}

var sourceDir string
var inspectCmd = &cobra.Command{
Use: "inspect {ARCHIVE|DIRECTORY}",
Short: "inspect tarantool coredump",
Run: func(cmd *cobra.Command, args []string) {
if err := coredump.Inspect(args[0], sourceDir); err != nil {
if err := coredump.Inspect(args[0], coredumpInspectSourceDir); err != nil {
util.HandleCmdErr(cmd, err)
}
},
Args: cobra.ExactArgs(1),
}
inspectCmd.Flags().StringVarP(&sourceDir, "sourcedir", "s", "",
inspectCmd.Flags().StringVarP(&coredumpInspectSourceDir, "sourcedir", "s", "",
"Source directory")

subCommands := []*cobra.Command{
cmd.AddCommand(
packCmd,
unpackCmd,
inspectCmd,
}

for _, cmd := range subCommands {
coredumpCmd.AddCommand(cmd)
}
)

return coredumpCmd
return cmd
}
12 changes: 11 additions & 1 deletion cli/coredump/coredump.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"

"github.com/apex/log"
Expand All @@ -24,14 +25,23 @@ const packEmbedPath = "scripts/tarabrt.sh"
const inspectEmbedPath = "scripts/gdb.sh"

// Pack packs coredump into a tar.gz archive.
func Pack(corePath string) error {
func Pack(corePath string, executable string, outputDir string, pid uint, time string) error {
tmpDir, err := os.MkdirTemp(os.TempDir(), "tt-coredump-*")
if err != nil {
return fmt.Errorf("cannot create a temporary directory for archiving: %v", err)
}
defer os.RemoveAll(tmpDir) // Clean up on function return.

scriptArgs := []string{"-c", corePath}
if executable != "" {
scriptArgs = append(scriptArgs, "-e", executable)
}
if outputDir != "" {
scriptArgs = append(scriptArgs, "-d", outputDir)
}
if pid != 0 {
scriptArgs = append(scriptArgs, "-p", strconv.FormatUint(uint64(pid), 10))
}

// Prepare gdb wrapper for packing.
inspectPath := filepath.Join(tmpDir, filepath.Base(inspectEmbedPath))
Expand Down
22 changes: 16 additions & 6 deletions cli/coredump/scripts/tarabrt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ Supported options are:
within DIRECTORY.
-e TARANTOOL Use file TARANTOOL as the executable file for
examining with a core dump COREDUMP. If PID is
specified, the one from /proc/PID/exe is chosen
(see proc(5) for more info). If TARANTOOL is
omitted, /usr/bin/tarantool is chosen.
examining with a core dump COREDUMP. By default
tarantool executable path is obtained from PATH.
If PID is specified, the one from /proc/PID/exe
is chosen (see proc(5) for more info).
-g GDBWRAPPER Include GDB-wrapper script GDBWRAPPER into the
archive.
Expand Down Expand Up @@ -62,7 +62,7 @@ HELP
)

# Assign configurable parameters with the default values.
BINARY=/usr/bin/tarantool
BINARY=
COREDIR=${PWD}
COREFILE=
EXTS=
Expand Down Expand Up @@ -90,6 +90,16 @@ while true; do
esac
done

if [ -z "${BINARY}" ]; then
if ! which tarantool; then
[ -t 1 ] && cat <<NOEXEC
There is no Tarantool executable found in PATH. Either make sure
it is there or specify it explicitly with the '-e' option.
NOEXEC
fi
BINARY=$(which tarantool)
fi

# Do not proceed if there are some CLI arguments left. Everything
# should be parsed before this line.
if [ $# -ne 0 ]; then
Expand Down Expand Up @@ -175,7 +185,7 @@ fi
# behaviours for the check below. For more info, see the highly
# detailed answer on Stack Overflow below.
# https://stackoverflow.com/a/55704865/4609359
if file "${BINARY}" | grep -qvE 'executable|shared object'; then
if file -L "${BINARY}" | grep -qvE 'executable|shared object'; then
[ -t 1 ] && cat <<NOTELF
Not an ELF file: ${BINARY}
Expand Down
97 changes: 66 additions & 31 deletions test/integration/coredump/test_coredump.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import pytest

from utils import run_command_and_get_output
from utils import get_tarantool_version, run_command_and_get_output

# Fixture below produces a coredump. The location of the coredumps is
# configured over /proc/sys/kernel/core_pattern file in a various ways
Expand All @@ -19,31 +19,42 @@
# For the cases above generated files are removed after tests but in general
# there is no guarantee. So tests that use this fixture are disabled by default
# (marked with skipif) in order to avoid coredumps "leaks".
# One may launch them explicitly using TT_ENABLE_COREDUMP_FIXTURE environment
# TT_ENABLE_COREDUMP_FIXTURE=1 python3 -m pytest test/integration/coredump/.
# One may launch them explicitly using TT_ENABLE_COREDUMP_TESTS environment
# TT_ENABLE_COREDUMP_TESTS=1 python3 -m pytest test/integration/coredump/.


@pytest.fixture(scope="session")
def coredump(tmp_path_factory) -> Path:
coredump_tmpdir = tmp_path_factory.mktemp("coredump")
skip_coredump_cond = os.getenv('TT_ENABLE_COREDUMP_TESTS') is None
skip_coredump_reason = "Should be launched explicitly to control coredump it produces"


def generate_coredump(tmp_path_factory, env=None) -> Path:
coredump_dir = tmp_path_factory.mktemp("coredump")
with open('/proc/sys/kernel/core_pattern', 'r') as f:
core_pattern = f.read()
core_pattern = f.read().strip()

def coredump_apport(core_source, outdir):
rc, _ = run_command_and_get_output(['apport-unpack', core_source, outdir])
assert rc == 0
return outdir / 'CoreDump'

def apport_crash_to_coredump(crash):
apport_unpack_dir = coredump_tmpdir / 'apport-unpack'
rc, output = run_command_and_get_output(['apport-unpack', crash, apport_unpack_dir])
return apport_unpack_dir / 'CoreDump'
def coredump_systemd(core_source, outdir):
core = outdir / core_source.stem
rc, _ = run_command_and_get_output(['coredumpctl', 'dump', f'--output={core}'])
assert rc == 0
return core

to_coredump = None
if not core_pattern.startswith('|'):
core_wildcard = core_pattern.strip().split('%')[0] + '*'
core_wildcard = core_pattern.replace('%%', '%')
core_wildcard = re.sub('%[cdeEghiIpPstu]', '*', core_wildcard)
if not os.path.isabs(core_wildcard):
core_wildcard = coredump_tmpdir / core_wildcard
core_wildcard = str(coredump_dir / core_wildcard)
elif re.search(r"apport", core_pattern):
core_wildcard = os.path.join('/var/crash', '*.crash')
to_coredump = apport_crash_to_coredump
core_wildcard = '/var/crash/*.crash'
to_coredump = coredump_apport
elif re.search(r"systemd-coredump", core_pattern):
core_wildcard = os.path.join('/var/lib/systemd/coredump', '*')
core_wildcard = '/var/lib/systemd/coredump/*'
to_coredump = coredump_systemd
else:
assert False, "Unexpected core pattern '{}'".format(core_pattern)
cores = set(glob.glob(core_wildcard))
Expand All @@ -54,7 +65,7 @@ def apport_crash_to_coredump(crash):
resource.setrlimit(resource.RLIMIT_CORE, (resource.RLIM_INFINITY, rlim_core_hard))
# Crash tarantool.
cmd = ["tarantool", "-e", "require('ffi').cast('char *', 0)[0] = 42"]
rc, output = run_command_and_get_output(cmd, cwd=coredump_tmpdir)
rc, output = run_command_and_get_output(cmd, cwd=coredump_dir, env=env)
# Restore ulimit -c.
resource.setrlimit(resource.RLIMIT_CORE, (rlim_core_soft, rlim_core_hard))
assert rc != 0
Expand All @@ -63,14 +74,20 @@ def apport_crash_to_coredump(crash):
# Find the newly generated coredump.
new_cores = set(glob.glob(core_wildcard)) - cores
assert len(new_cores) == 1
core_path = Path(next(iter(new_cores)))

return core_path if to_coredump is None else to_coredump(core_path, coredump_dir)


@pytest.fixture(scope="session")
def coredump(tmp_path_factory) -> Path:
return generate_coredump(tmp_path_factory)

# And move it to the temporary directory (this directory is removed
# automatically, so there is no need to remove the coredump explicitly).
core_path = coredump_tmpdir / "core"
os.rename(next(iter(new_cores)), core_path)

assert os.path.exists(core_path)
return core_path if to_coredump is None else to_coredump(core_path)
@pytest.fixture(scope="session")
def coredump_alt(tmp_path_factory, tmpdir_with_tarantool) -> Path:
bin_dir = os.path.join(tmpdir_with_tarantool, "bin")
return generate_coredump(tmp_path_factory, env=dict(PATH=bin_dir))


@pytest.fixture(scope="session")
Expand Down Expand Up @@ -108,15 +125,36 @@ def test_coredump_pack_no_such_file(tt_cmd, tmp_path):
assert re.search(r"pack script execution failed", output)


@pytest.mark.skipif(os.getenv('TT_ENABLE_COREDUMP_FIXTURE') is None,
reason="Should be launched explicitly to control coredump it produces")
@pytest.mark.skipif(skip_coredump_cond, reason=skip_coredump_reason)
def test_coredump_pack(tt_cmd, tmp_path, coredump):
cmd = [tt_cmd, "coredump", "pack", coredump]
rc, output = run_command_and_get_output(cmd, cwd=tmp_path)
assert rc == 0
assert re.search(r"Core was successfully packed.", output)


@pytest.mark.skipif(skip_coredump_cond, reason=skip_coredump_reason)
@pytest.mark.slow
def test_coredump_pack_executable(tt_cmd, tmp_path, coredump_alt, tmpdir_with_tarantool):
executable = tmpdir_with_tarantool / "bin" / "tarantool"
version_expected = get_tarantool_version(dict(PATH=str(executable.parent)))

cmd = [tt_cmd, "coredump", "pack", "-e", executable, coredump_alt]
rc, output = run_command_and_get_output(cmd, cwd=tmp_path)
assert rc == 0
assert re.search(r"Core was successfully packed.", output)
archives = list(tmp_path.glob("*.tar.gz"))
assert len(archives) == 1

# Extract Tarantool executable and check its version.
run_command_and_get_output(["tar", "xzf", archives[0], "--wildcards", "*/tarantool"],
cwd=tmp_path)
executables = list(tmp_path.glob("*/tarantool"))
assert len(executables) == 1
version = get_tarantool_version(dict(PATH=str(executables[0].parent)))
assert version == version_expected


def test_coredump_unpack_no_arg(tt_cmd, tmp_path):
cmd = [tt_cmd, "coredump", "unpack"]
rc, output = run_command_and_get_output(cmd, cwd=tmp_path)
Expand All @@ -131,8 +169,7 @@ def test_coredump_unpack_no_such_file(tt_cmd, tmp_path):
assert re.search(r"failed to unpack", output)


@pytest.mark.skipif(os.getenv('TT_ENABLE_COREDUMP_FIXTURE') is None,
reason="Should be launched explicitly to control coredump it produces")
@pytest.mark.skipif(skip_coredump_cond, reason=skip_coredump_reason)
def test_coredump_unpack(tt_cmd, tmp_path, coredump_packed):
cmd = [tt_cmd, "coredump", "unpack", coredump_packed]
rc, output = run_command_and_get_output(cmd, cwd=tmp_path)
Expand All @@ -154,8 +191,7 @@ def test_coredump_inspect_no_such_file(tt_cmd, tmp_path):
assert re.search(r"failed to inspect", output)


@pytest.mark.skipif(os.getenv('TT_ENABLE_COREDUMP_FIXTURE') is None,
reason="Should be launched explicitly to control coredump it produces")
@pytest.mark.skipif(skip_coredump_cond, reason=skip_coredump_reason)
def test_coredump_inspect_packed(tt_cmd, tmp_path, coredump_packed):
cmd = [tt_cmd, "coredump", "inspect", coredump_packed]
process = subprocess.run(
Expand All @@ -167,8 +203,7 @@ def test_coredump_inspect_packed(tt_cmd, tmp_path, coredump_packed):
assert process.returncode == 0


@pytest.mark.skipif(os.getenv('TT_ENABLE_COREDUMP_FIXTURE') is None,
reason="Should be launched explicitly to control coredump it produces")
@pytest.mark.skipif(skip_coredump_cond, reason=skip_coredump_reason)
def test_coredump_inspect_unpacked(tt_cmd, tmp_path, coredump_unpacked):
cmd = [tt_cmd, "coredump", "inspect", coredump_unpacked]
process = subprocess.run(
Expand Down
Loading

0 comments on commit 2228bd5

Please sign in to comment.