From 3ba2c3e11791b9da11661ea45d966d2db621a6ac Mon Sep 17 00:00:00 2001 From: baude Date: Mon, 11 Dec 2017 09:34:58 -0600 Subject: kpod top Display information about processes in a running container. Signed-off-by: baude Closes: #121 Approved by: rhatdan --- README.md | 1 + cmd/kpod/images.go | 8 ++ cmd/kpod/main.go | 1 + cmd/kpod/top.go | 258 ++++++++++++++++++++++++++++++++++++++++++++++++ completions/bash/kpod | 8 ++ docs/kpod-top.1.md | 59 +++++++++++ docs/kpod.1.md | 3 + libpod/container_top.go | 57 +++++++++++ test/kpod_top.bats | 51 ++++++++++ 9 files changed, 446 insertions(+) create mode 100644 cmd/kpod/top.go create mode 100644 docs/kpod-top.1.md create mode 100644 libpod/container_top.go create mode 100644 test/kpod_top.bats diff --git a/README.md b/README.md index 2ee07a23d..7e51dff87 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ libpod is currently in active development. | [kpod-stats(1)](/docs/kpod-stats.1.md) | Display a live stream of one or more containers' resource usage statistics|| | [kpod-stop(1)](/docs/kpod-stop.1.md) | Stops one or more running containers || | [kpod-tag(1)](/docs/kpod-tag.1.md) | Add an additional name to a local image |[![...](/docs/play.png)](https://asciinema.org/a/133803)| +| [kpod-top(1)](/docs/kpod-top.1.md) | Display the running processes of a container | [kpod-umount(1)](/docs/kpod-umount.1.md) | Unmount a working container's root filesystem || | [kpod-unpause(1)](/docs/kpod-unpause.1.md) | Unpause one or more running containers |[![...](/docs/play.png)](https://asciinema.org/a/141292)| | [kpod-version(1)](/docs/kpod-version.1.md) | Display the version information |[![...](/docs/play.png)](https://asciinema.org/a/mfrn61pjZT9Fc8L4NbfdSqfgu)| diff --git a/cmd/kpod/images.go b/cmd/kpod/images.go index 2b1003ebd..6384d61b8 100644 --- a/cmd/kpod/images.go +++ b/cmd/kpod/images.go @@ -155,8 +155,13 @@ func genImagesFormat(format string, quiet, noHeading, digests bool) string { func imagesToGeneric(templParams []imagesTemplateParams, JSONParams []imagesJSONParams) (genericParams []interface{}) { if len(templParams) > 0 { for _, v := range templParams { + fmt.Println(v) + fmt.Printf("%T\n", v) genericParams = append(genericParams, interface{}(v)) } + fmt.Println("###") + fmt.Println(genericParams) + fmt.Println("###") return } for _, v := range JSONParams { @@ -178,6 +183,9 @@ func (i *imagesTemplateParams) headerMap() map[string]string { } values[key] = strings.ToUpper(splitCamelCase(value)) } + fmt.Println("!!!") + fmt.Printf("%+v\n", values) + fmt.Println("!!!") return values } diff --git a/cmd/kpod/main.go b/cmd/kpod/main.go index 708031dc9..90a4d1d7f 100644 --- a/cmd/kpod/main.go +++ b/cmd/kpod/main.go @@ -62,6 +62,7 @@ func main() { statsCommand, stopCommand, tagCommand, + topCommand, umountCommand, unpauseCommand, versionCommand, diff --git a/cmd/kpod/top.go b/cmd/kpod/top.go new file mode 100644 index 000000000..9c2fcd34e --- /dev/null +++ b/cmd/kpod/top.go @@ -0,0 +1,258 @@ +package main + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/pkg/errors" + "github.com/projectatomic/libpod/cmd/kpod/formats" + "github.com/projectatomic/libpod/libpod" + "github.com/urfave/cli" +) + +var ( + topFlags = []cli.Flag{ + cli.StringFlag{ + Name: "format", + Usage: "Change the output to JSON", + }, + } + topDescription = ` + kpod top + + Display the running processes of the container. +` + + topCommand = cli.Command{ + Name: "top", + Usage: "Display the running processes of a container", + Description: topDescription, + Flags: topFlags, + Action: topCmd, + ArgsUsage: "CONTAINER-NAME", + SkipArgReorder: true, + } +) + +func topCmd(c *cli.Context) error { + doJSON := false + if c.IsSet("format") { + if strings.ToUpper(c.String("format")) == "JSON" { + doJSON = true + } else { + return errors.Errorf("only 'json' is supported for a format option") + } + } + args := c.Args() + var psArgs []string + psOpts := []string{"-o", "uid,pid,ppid,c,stime,tname,time,cmd"} + if len(args) < 1 { + return errors.Errorf("you must provide the name or id of a running container") + } + if err := validateFlags(c, topFlags); err != nil { + return err + } + + runtime, err := getRuntime(c) + if err != nil { + return errors.Wrapf(err, "error creating libpod runtime") + } + defer runtime.Shutdown(false) + if len(args) > 1 { + psOpts = args[1:] + } + + container, err := runtime.LookupContainer(args[0]) + if err != nil { + return errors.Wrapf(err, "unable to lookup %s", args[0]) + } + conStat, err := container.State() + if err != nil { + return errors.Wrapf(err, "unable to look up state for %s", args[0]) + } + if conStat != libpod.ContainerStateRunning { + return errors.Errorf("top can only be used on running containers") + } + + psArgs = append(psArgs, psOpts...) + + results, err := container.GetContainerPidInformation(psArgs) + if err != nil { + return err + } + headers := getHeaders(results[0]) + format := genTopFormat(headers) + var out formats.Writer + psParams, err := psDataToPSParams(results[1:], headers) + if err != nil { + return errors.Wrap(err, "unable to convert ps data to proper structure") + } + if doJSON { + out = formats.JSONStructArray{Output: topToGeneric(psParams)} + } else { + out = formats.StdoutTemplateArray{Output: topToGeneric(psParams), Template: format, Fields: createTopHeaderMap(headers)} + } + formats.Writer(out).Out() + return nil +} + +func getHeaders(s string) []string { + var headers []string + tmpHeaders := strings.Fields(s) + for _, header := range tmpHeaders { + headers = append(headers, strings.Replace(header, "%", "", -1)) + } + return headers +} + +func genTopFormat(headers []string) string { + format := "table " + for _, header := range headers { + format = fmt.Sprintf("%s{{.%s}}\t", format, header) + } + return format +} + +// imagesToGeneric creates an empty array of interfaces for output +func topToGeneric(templParams []PSParams) (genericParams []interface{}) { + for _, v := range templParams { + genericParams = append(genericParams, interface{}(v)) + } + return +} + +// generate the header based on the template provided +func createTopHeaderMap(v []string) map[string]string { + values := make(map[string]string) + for _, key := range v { + value := key + if value == "CPU" { + value = "%CPU" + } else if value == "MEM" { + value = "%MEM" + } + values[key] = strings.ToUpper(splitCamelCase(value)) + } + return values +} + +// PSDataToParams converts a string array of data and its headers to an +// arra if PSParams +func psDataToPSParams(data []string, headers []string) ([]PSParams, error) { + var params []PSParams + for _, line := range data { + tmpMap := make(map[string]string) + tmpArray := strings.Fields(line) + if len(tmpArray) == 0 { + continue + } + for index, v := range tmpArray { + header := headers[index] + tmpMap[header] = v + } + jsonData, _ := json.Marshal(tmpMap) + var r PSParams + err := json.Unmarshal(jsonData, &r) + if err != nil { + return []PSParams{}, err + } + params = append(params, r) + } + return params, nil +} + +//PSParams is a list of options that the command line ps recognizes +type PSParams struct { + CPU string `json: "%CPU"` + MEM string `json: "%MEM"` + COMMAND string + BLOCKED string + START string + TIME string + C string + CAUGHT string + CGROUP string + CLSCLS string + CLS string + CMD string + CP string + DRS string + EGID string + EGROUP string + EIP string + ESP string + ELAPSED string + EUIDE string + USER string + F string + FGID string + FGROUP string + FUID string + FUSER string + GID string + GROUP string + IGNORED string + IPCNS string + LABEL string + STARTED string + SESSION string + LWP string + MACHINE string + MAJFLT string + MINFLT string + MNTNS string + NETNS string + NI string + NLWP string + OWNER string + PENDING string + PGID string + PGRP string + PID string + PIDNS string + POL string + PPID string + PRI string + PSR string + RGID string + RGROUP string + RSS string + RSZ string + RTPRIO string + RUID string + RUSER string + S string + SCH string + SEAT string + SESS string + P string + SGID string + SGROUP string + SID string + SIZE string + SLICE string + SPID string + STACKP string + STIME string + SUID string + SUPGID string + SUPGRP string + SUSER string + SVGID string + SZ string + TGID string + THCNT string + TID string + TTY string + TPGID string + TRS string + TT string + UID string + UNIT string + USERNS string + UTSNS string + UUNIT string + VSZ string + WCHAN string +} diff --git a/completions/bash/kpod b/completions/bash/kpod index 3e291c526..f5caeceb3 100644 --- a/completions/bash/kpod +++ b/completions/bash/kpod @@ -1288,6 +1288,14 @@ kpod_tag() { _complete_ "$options_with_args" "$boolean_options" } +kpod_top() { + local options_with_args=" + " + local boolean_options=" + " + _complete_ "$options_with_args" "$boolean_options" +} + _kpod_version() { local options_with_args=" " diff --git a/docs/kpod-top.1.md b/docs/kpod-top.1.md new file mode 100644 index 000000000..e19b7342c --- /dev/null +++ b/docs/kpod-top.1.md @@ -0,0 +1,59 @@ +% kpod(1) kpod-top - display the running processes of a container +% Brent Baude + +## NAME +kpod top - Display the running processes of a container + +## SYNOPSIS +**kpod top** +[**--help**|**-h**] + +## DESCRIPTION +Display the running process of the container. ps-OPTION can be any of the options you would pass to a Linux ps command + +**kpod [GLOBAL OPTIONS] top [OPTIONS]** + +## OPTIONS + +**--help, -h** + Print usage statement + +**--format** + Display the output in an alternate format. The only supported format is **JSON**. + +## EXAMPLES + +``` +# kpod top f5a62a71b07 + UID PID PPID %CPU STIME TT TIME CMD + 0 18715 18705 0.0 10:35 pts/0 00:00:00 /bin/bash + 0 18741 18715 0.0 10:35 pts/0 00:00:00 vi +# +``` + +``` +#kpod --log-level=debug top f5a62a71b07 -o fuser,f,comm,label +FUSER F COMMAND LABEL +root 4 bash system_u:system_r:container_t:s0:c429,c1016 +root 0 vi system_u:system_r:container_t:s0:c429,c1016 +# +``` +``` +# kpod top --format=json f5a62a71b07b -o %cpu,%mem,command,blocked +[ + { + "CPU": "0.0", + "MEM": "0.0", + "COMMAND": "vi", + "BLOCKED": "0000000000000000", + "START": "", + "TIME": "", + "C": "", + "CAUGHT": "", + ... +``` +## SEE ALSO +kpod(1), ps(1) + +## HISTORY +December 2017, Originally compiled by Brent Baude diff --git a/docs/kpod.1.md b/docs/kpod.1.md index dec29e395..02f97739e 100644 --- a/docs/kpod.1.md +++ b/docs/kpod.1.md @@ -136,6 +136,9 @@ Stops one or more running containers. ### tag Add an additional name to a local image +### top +Display the running processes of a container + ### umount Unmount a working container's root file system diff --git a/libpod/container_top.go b/libpod/container_top.go new file mode 100644 index 000000000..7d6dad2b4 --- /dev/null +++ b/libpod/container_top.go @@ -0,0 +1,57 @@ +package libpod + +import ( + "fmt" + "io/ioutil" + "path/filepath" + "strings" + + "github.com/pkg/errors" + "github.com/projectatomic/libpod/utils" + "github.com/sirupsen/logrus" +) + +// GetContainerPids reads sysfs to obtain the pids associated with the container's cgroup +// and uses locking +func (c *Container) GetContainerPids() ([]string, error) { + c.lock.Lock() + defer c.lock.Unlock() + if err := c.syncContainer(); err != nil { + return []string{}, errors.Wrapf(err, "error updating container %s state", c.ID()) + } + return c.getContainerPids() +} + +// Gets the pids for a container without locking. should only be called from a func where +// locking has already been established. +func (c *Container) getContainerPids() ([]string, error) { + taskFile := filepath.Join("/sys/fs/cgroup/pids", CGroupParent, fmt.Sprintf("libpod-conmon-%s", c.ID()), c.ID(), "tasks") + logrus.Debug("reading pids from ", taskFile) + content, err := ioutil.ReadFile(taskFile) + if err != nil { + return []string{}, errors.Wrapf(err, "unable to read pids from %s", taskFile) + } + return strings.Fields(string(content)), nil + +} + +// GetContainerPidInformation calls ps with the appropriate options and returns +// the results as a string +func (c *Container) GetContainerPidInformation(args []string) ([]string, error) { + c.lock.Lock() + defer c.lock.Unlock() + if err := c.syncContainer(); err != nil { + return []string{}, errors.Wrapf(err, "error updating container %s state", c.ID()) + } + pids, err := c.getContainerPids() + if err != nil { + return []string{}, errors.Wrapf(err, "unable to obtain pids for ", c.ID()) + } + args = append(args, "-p", strings.Join(pids, ",")) + logrus.Debug("Executing: ", strings.Join(args, " ")) + results, err := utils.ExecCmd("ps", args...) + if err != nil { + return []string{}, errors.Wrapf(err, "unable to obtain information about pids") + } + return strings.Split(results, "\n"), nil +} diff --git a/test/kpod_top.bats b/test/kpod_top.bats new file mode 100644 index 000000000..189772a11 --- /dev/null +++ b/test/kpod_top.bats @@ -0,0 +1,51 @@ +#!/usr/bin/env bats + +load helpers + +function teardown() { + cleanup_test +} + +function setup() { + copy_images +} + +@test "top without container name or id" { + run ${KPOD_BINARY} ${KPOD_OPTIONS} top + echo "$output" + [ "$status" -eq 1 ] +} + +@test "top a bogus container" { + run ${KPOD_BINARY} ${KPOD_OPTIONS} top foobar + echo "$output" + [ "$status" -eq 1 ] +} + +@test "top non-running container by id with defaults" { + run ${KPOD_BINARY} ${KPOD_OPTIONS} create -d ${ALPINE} sleep 60 + [ "$status" -eq 0 ] + ctr_id="$output" + run bash -c "${KPOD_BINARY} ${KPOD_OPTIONS} top $ctr_id" + echo "$output" + [ "$status" -eq 1 ] +} + +@test "top running container by id with defaults" { + run ${KPOD_BINARY} ${KPOD_OPTIONS} run -dt ${ALPINE} /bin/sh + [ "$status" -eq 0 ] + ctr_id="$output" + echo $ctr_id + run bash -c "${KPOD_BINARY} ${KPOD_OPTIONS} top $ctr_id" + echo "$output" + [ "$status" -eq 0 ] +} + +@test "top running container by id with ps opts" { + run ${KPOD_BINARY} ${KPOD_OPTIONS} run -d ${ALPINE} sleep 60 + [ "$status" -eq 0 ] + ctr_id="$output" + run bash -c "${KPOD_BINARY} ${KPOD_OPTIONS} top $ctr_id -o fuser,f,comm,label" + echo "$output" + [ "$status" -eq 0 ] +} -- cgit v1.2.3-54-g00ecf