This project was inspired by RIKRUS's and Hermann Björgvin's MOTD scripts.
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 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.
You can dump the default config by passing an invalid path as the -c/--config argument and using --dump-config at the same time.
Configuration changed on 2020-05-29, automatic conversion can be done with migrate.go. TL;DR of changes:
globalsection instead of being root levelheaderandcontentare nowpad_headerandpad_contentfailedOnlyis nowwarnings_only, I think this more clearly communicates what it does- All keys changed from camelCase to snake_case, follows yaml standards better
- Kernel 5.6+ (drivetemp module) or hddtemp daemon are required for disk temps
dockerMinAPIin docker.go might need tweakingzfs-utilsfor zpool status- go-check-updates for updates
lm_sensorsfor CPU temperatures
wget https://raw.githubusercontent.com/cosandr/go-motd/master/PKGBUILD
makepkg -sigo-motd will use the config file in /etc/go-motd/config.yaml.
# Clone repository and cd to it
git clone https://github.com/cosandr/go-motd
cd go-motd
## Manual installation
go mod vendor
go build -a -ldflags "-X main.defaultCfgPath=/etc/go-motd/config.yaml"
# Generate default config
sudo ./go-motd --config /dev/null --dump-config > "default-config.yaml" 2> /dev/null
# Install binary
sudo install -m 755 go-motd /usr/bin/
## Using setup.sh
sudo ./setup.sh installTwo modes of operations, running directly or as a daemon writing to a file at fixed intervals and triggered by SIGHUP.
Assuming it was installed as outlined above, just run the binary by adding go-motd in your shell rc file.
Recommended usage is running with systemd and pointing go-motd at /etc/motd.
[Unit]
Description=Go MOTD generator
[Service]
PIDFile=/run/go-motd.pid
ExecReload=/usr/bin/kill -s HUP $MAINPID
ExecStart=/usr/bin/go-motd --daemon --pid /run/go-motd.pid --output /etc/motd
If it's not showing up, you can add [[ -s /etc/motd ]] && cat /etc/motd to your shell rc file.
A refresh can be forced by issuing a SIGHUP to the process, either with systemctl reload go-motd.service or
kill -HUP $(cat /run/go-motd.pid)
warnings_onlywill hide content unless there is a warning, per-module override availableshow_orderlist of enabled modules, they will be displayed in the same order. If not defined, the order in defaultOrder will be used.col_defarrange module output in columns as defined by a 2-dimensional array, configuration for example pictures shown below. Note that this overridesshow_order.
col_def:
- [sysinfo]
- [updates]
- [docker, podman]
- [systemd]
- [cpu, disk]
- [zfs]
- [btrfs]col_padnumber of spaces between columns
All modules implement at least warnings_only, pad_header and pad_content.
warnings_onlyoverrides global setting for that module onlypad_headeris a 2-element array of integers, the first represents the number of spaces before the text, the second is spaces after the text, but before:
# pad_header: [0, 2]
Example : OK
# pad_header: [2, 0]
Example: OK
# pad_header: [1, 2]
Example : OK
pad_contentis the same but for details, the padding applies to all lines equally
warn/critare temperatures to consider warning or critical leveluse_execget CPU temperature by parsingsensors -joutput
warn/critare temperatures to consider warning or critical levelignorelist of disks to ignore (uses names from /dev/)use_syswill get disk temperatures from/sys/blockinstead of the hddtemp daemon. The drivetemp kernel module is required.
warn/critpercentage of disk space used before it is considered a warning or critical level, default is 70% and 90% respectively
show_freeshow free space instead of useduse_execget BTRFS filesystem info by parsingbtrfs filesystem usage --rawsudousesudo btrfscommands, required for accurate RAID56 databtrfs_cmdoverride btrfs command, useful for wrapper scripts. For example:
# visudo -f /etc/sudoers.d/btrfs-us
andrei ALL=(root) NOPASSWD: /usr/bin/btrfs-us
# cat /usr/bin/btrfs-us
#!/bin/sh
btrfs filesystem usage $@
ignorelist of ignored container namesuse_execget containers by parsingdockercommand output
ignorelist of ignored container namessudoget root containers, you should be able to runsudo podmanwithout a passwordinclude_sudoincludes both root and rootless containers
No extra config
unitslist of monitored units, must include file extension. This option must be set for the module to work.hide_exthide the unit file extension when displaying their statusinactive_okconsider inactive units with exit code 0 as being OK, if false they will be considered warningsshow_faileddisplay all failed units, similar tosystemctl --failed
showdisplays the list of pending updatesshort_namesuse short names for time values (1h5m instead of 1 hour, 5 min)addresslisten address of go-check-updates, can be unix socketeveryrequest cache update if it is older than this durationfilepath togo-check-updatesoutput json, setting this will not use the API at all
Basic datasources/example.go
package datasources
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 ^L^
examplePadR = "&" // Default is ^R^
)
// Optional, can use ConfBase or ConfBaseWarn
// Recommended to use a struct, even if it only contains one of the base configs
type ConfExample struct {
ConfBase `yaml:",inline"`
More bool `yaml:"more"`
}
// Init is mandatory
func (c *ConfExample) Init() {
// Base init must be called
c.ConfBase.Init()
// Can change default padding here
// Set right padding for header to 2 spaces
c.PadHeader[1] = 2
// Set other defaults
c.More = true
// Custom padding strings
c.padL = "^"
c.padR = "&"
}
func GetExample(ch chan<- SourceReturn, conf *Conf) {
c := conf.Example
// Optional, but recommended if you use WarnOnly
// Check for warnOnly override
if c.WarnOnly == nil {
c.WarnOnly = &conf.WarnOnly
}
sr := NewSourceReturn(conf.debug)
defer func() {
ch <- sr.Return(&c.ConfBase)
}()
sr.Header, sr.Content, sr.Error = internalFunc(&c)
return
}
func internalFunc(c *ConfExample) (header string, content string, err error) {
// You should return a ModuleNotAvailable error if it is appropriate.
// Remember to use c.padL/c.padR when preparing header and content
header = fmt.Sprintf("%s: %s\n", utils.Wrap("Example", c.padL, c.padR), utils.Good("OK"))
return
}Update common_vars.go
type Conf struct {
// Add your type to the Conf struct
Example ConfExample
}
// Update Init()
func (c *Conf) Init() {
// Init must be called to avoid likely panic
// This is caused by uninitialized padding slices if they are not in the config file
c.Example.Init()
}
// Add to a case to run your function in RunSources
case "example":
go GetExample(ch, c)Modify main.go (optional)
package main
// Add your module to defaultOrder
var defaultOrder = []string{..., "example"}You may also add an entry to config.yaml, this will override what you have set in Init().