Skip to content

Commit

Permalink
Add podman, common container type
Browse files Browse the repository at this point in the history
  • Loading branch information
cosandr committed May 15, 2020
1 parent d35adf4 commit e36ec90
Show file tree
Hide file tree
Showing 11 changed files with 284 additions and 133 deletions.
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This project was inspired by [RIKRUS's](https://github.com/RIKRUS/MOTD) and [Her

I've decided to use Go because it is about 10x faster than a similar bash script and it makes for a great first project using the language. In my tests it typically runs in 10-20ms, a similar bash script takes 200-500ms.

The available information will depend on the user privileges, you will need to be able to run (without sudo) `systemctl status`, `docker inspect` and `zpool status` for example.
The available information will depend on the user privileges, you will need to be able to run (without sudo) `systemctl status`, `docker ps` and `zpool status` for example.

Note that the BTRFS and ZFS space statistics are totals, that is to say, a RAID5 setup shows the used/total space across all drives. For example 3x4TB disks in RAIDZ1 show 10.91TB total, not the usable space which is about 7TB.

Expand Down Expand Up @@ -62,7 +62,8 @@ Example line in `~/.zshrc`
colDef:
- [sysinfo]
- [updates]
- [docker, systemd]
- [docker, podman]
- [systemd]
- [cpu, disk]
- [zfs]
- [btrfs]
Expand All @@ -75,7 +76,7 @@ colDef:
All modules implement at least `header`/`content`.

- `header`/`content` arrays define padding, first element is padding to the left (of the module name) and second to the right, before the semicolon (useful for aligning vertically)
- `warn`/`crit` unit depends on the module, for CPU/Disk temperatures it is degrees celsius, for ZFS pools it is % used
- `warn`/`crit` unit depends on the module, for CPU/Disk temperatures it is degrees celsius, for storage (ZFS/btrfs) it is % used

### Updates

Expand All @@ -91,6 +92,13 @@ The drivetemp kernel module is required.
### Docker

- `ignore` list of ignored container names
- `useExec` get containers by parsing `docker` command output

### Podman

- `ignore` list of ignored container names
- `sudo` get root containers, you should be able to run `sudo podman` without a password
- `includeSudo` includes both root and rootless containers

### Systemd

Expand All @@ -108,8 +116,8 @@ import "github.com/cosandr/go-motd/utils"
// These must not occur in the output string itself, if they do, feel free to use your own constants
const (
examplePadL = "^" // Default is $
examplePadR = "&" // Default is %
examplePadL = "^" // Default is ^L^
examplePadR = "&" // Default is ^R^
)
// Optional, can use CommonConf or CommonWithWarnConf
Expand Down
12 changes: 9 additions & 3 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ failedOnly: false
colDef:
- [sysinfo]
- [updates]
- [docker, systemd]
- [docker, podman]
- [systemd]
- [cpu, disk]
- [zfs, btrfs]
- [zfs]
- [btrfs]
colPad: 10
updates:
header: [0, 3]
Expand All @@ -30,11 +32,15 @@ systemd:
- check-nginx-modules.service
# - fictional.service
docker:
useExec: false
useExec: true
header: [0, 4]
content: [2, 1]
ignore:
- code-server
podman:
includeSudo: true
header: [0, 1]
content: [0, 1]
disk:
useSys: true
header: [0, 1]
Expand Down
4 changes: 2 additions & 2 deletions datasources/common_vars.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package datasources

const (
padL string = "$"
padR string = "%"
padL string = "^L^"
padR string = "^R^"
)

// CommonConf is the common type for all modules
Expand Down
64 changes: 64 additions & 0 deletions datasources/container_common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package datasources

import (
"fmt"
"sort"
"strings"

"github.com/cosandr/go-motd/utils"
)

type containerStatus struct {
Name string
Status string
}

type containerList struct {
Runtime string
Root bool
Containers []containerStatus
}

func (cl *containerList) toHeaderContent(ignoreList []string, failedOnly bool) (header string, content string, err error) {
// Make set of ignored containers
var ignoreSet utils.StringSet
ignoreSet = ignoreSet.FromList(ignoreList)
// Process output
var goodCont = make(map[string]string)
var failedCont = make(map[string]string)
var sortedNames []string
for _, c := range cl.Containers {
if ignoreSet.Contains(c.Name) {
continue
}
status := strings.ToLower(c.Status)
if status == "up" || status == "created" || status == "running" {
goodCont[c.Name] = status
} else {
failedCont[c.Name] = status
}
sortedNames = append(sortedNames, c.Name)
}
sort.Strings(sortedNames)

// Decide what header should be
if len(goodCont) == 0 {
header = fmt.Sprintf("%s: %s\n", utils.Wrap(cl.Runtime, padL, padR), utils.Err("critical"))
} else if len(failedCont) == 0 {
header = fmt.Sprintf("%s: %s\n", utils.Wrap(cl.Runtime, padL, padR), utils.Good("OK"))
if failedOnly {
return
}
} else if len(failedCont) < len(sortedNames) {
header = fmt.Sprintf("%s: %s\n", utils.Wrap(cl.Runtime, padL, padR), utils.Warn("warning"))
}
// Only print all containers if requested
for _, c := range sortedNames {
if val, ok := goodCont[c]; ok && !failedOnly {
content += fmt.Sprintf("%s: %s\n", utils.Wrap(c, padL, padR), utils.Good(val))
} else if val, ok := failedCont[c]; ok {
content += fmt.Sprintf("%s: %s\n", utils.Wrap(c, padL, padR), utils.Err(val))
}
}
return
}
42 changes: 42 additions & 0 deletions datasources/container_exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package datasources

import (
"bytes"
"os/exec"
"strings"
)

// getContainersExec returns container status using os/exec, ~5x slower than API
func getContainersExec(podman bool, sudo bool) (cl containerList, err error) {
var stdout bytes.Buffer
cl.Root = sudo
if podman {
cl.Runtime = "Podman"
} else {
cl.Runtime = "Docker"
}
var cmd *exec.Cmd
if sudo {
cmd = exec.Command("sudo", strings.ToLower(cl.Runtime), "ps", "--format", `"{{.Names}} {{.Status}}"`, "-a")
} else {
cmd = exec.Command(strings.ToLower(cl.Runtime), "ps", "--format", `"{{.Names}} {{.Status}}"`, "-a")
}
cmd.Stdout = &stdout
err = cmd.Run()
if err != nil {
return
}
for _, c := range strings.Split(stdout.String(), "\n") {
var tmp = strings.Split(strings.Trim(c, `"`), " ")
if len(tmp) < 2 {
continue
}
// tmp[0] - container name
// tmp[1] - container status (up/created/exited)
cl.Containers = append(cl.Containers, containerStatus{
Name: tmp[0],
Status: tmp[1],
})
}
return
}
58 changes: 14 additions & 44 deletions datasources/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package datasources
import (
"context"
"fmt"
"sort"
"strings"

"github.com/cosandr/go-motd/utils"
Expand All @@ -24,12 +23,19 @@ type DockerConf struct {

// GetDocker docker container status using the API
func GetDocker(ret chan<- string, c *DockerConf) {
var err error
var cl containerList
var header string
var content string
if c.Exec {
header, content, _ = checkContainersExec(c.Ignore, *c.FailedOnly)
cl, err = getContainersExec(false, false)
} else {
header, content, _ = checkContainers(c.Ignore, *c.FailedOnly)
cl, err = getDockerContainers()
}
if err != nil {
header = fmt.Sprintf("%s: %s\n", utils.Wrap("Docker", padL, padR), utils.Warn("unavailable"))
} else {
header, content, _ = cl.toHeaderContent(c.Ignore, *c.FailedOnly)
}
// Pad header
var p = utils.Pad{Delims: map[string]int{padL: c.Header[0], padR: c.Header[1]}, Content: header}
Expand All @@ -44,57 +50,21 @@ func GetDocker(ret chan<- string, c *DockerConf) {
ret <- header + "\n" + content
}

func checkContainers(ignoreList []string, failedOnly bool) (header string, content string, err error) {
func getDockerContainers() (cl containerList, err error) {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithVersion(dockerMinAPI))
if err != nil {
header = fmt.Sprintf("%s: %s\n", utils.Wrap("Docker", padL, padR), utils.Warn("unavailable"))
return
}

allContainers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{All: true})
if err != nil {
header = fmt.Sprintf("%s: %s\n", utils.Wrap("Docker", padL, padR), utils.Warn("unavailable"))
return
}
// Make set of ignored containers
var ignoreSet utils.StringSet
ignoreSet = ignoreSet.FromList(ignoreList)
// Process output
var goodCont = make(map[string]string)
var failedCont = make(map[string]string)
var sortedNames []string
for _, container := range allContainers {
var cleanName = strings.TrimPrefix(container.Names[0], "/")
if ignoreSet.Contains(cleanName) {
continue
}
if container.State != "running" {
failedCont[cleanName] = container.State
} else {
goodCont[cleanName] = container.State
}
sortedNames = append(sortedNames, cleanName)
}
sort.Strings(sortedNames)

// Decide what header should be
if len(goodCont) == 0 {
header = fmt.Sprintf("%s: %s\n", utils.Wrap("Docker", padL, padR), utils.Err("critical"))
} else if len(failedCont) == 0 {
header = fmt.Sprintf("%s: %s\n", utils.Wrap("Docker", padL, padR), utils.Good("OK"))
if failedOnly {
return
}
} else if len(failedCont) < len(allContainers) {
header = fmt.Sprintf("%s: %s\n", utils.Wrap("Docker", padL, padR), utils.Warn("warning"))
}
// Only print all containers if requested
for _, c := range sortedNames {
if val, ok := goodCont[c]; ok && !failedOnly {
content += fmt.Sprintf("%s: %s\n", utils.Wrap(c, padL, padR), utils.Good(val))
} else if val, ok := failedCont[c]; ok {
content += fmt.Sprintf("%s: %s\n", utils.Wrap(c, padL, padR), utils.Err(val))
}
cl.Containers = append(cl.Containers, containerStatus{
Name: strings.TrimPrefix(container.Names[0], "/"),
Status: container.State,
})
}
return
}
68 changes: 0 additions & 68 deletions datasources/docker_exec.go

This file was deleted.

Loading

0 comments on commit e36ec90

Please sign in to comment.