Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
bootery: add ability to turn power off/on for CI environment
  • Loading branch information
stapelberg committed Mar 4, 2022
1 parent 982f26f commit a32e6d0
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 6 deletions.
148 changes: 142 additions & 6 deletions cmd/bootery/bootery.go
Expand Up @@ -27,6 +27,7 @@ import (
"golang.org/x/sync/errgroup"
"golang.org/x/sys/unix"

mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/gokrazy/bakery/internal/ping"
"github.com/gokrazy/internal/fat"
"github.com/gokrazy/internal/mbr"
Expand Down Expand Up @@ -376,13 +377,26 @@ func testbootHandler(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()

if err := pm.use(); err != nil {
log.Printf("pm.use: %v", err)
}
defer func() {
if err := pm.release(); err != nil {
log.Printf("pm.release: %v", err)
}
}()

body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("testing boot image failed: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

if err := pm.awaitHealthy(r.Context()); err != nil {
log.Printf("pm.awaitHealthy: %v", err)
}

var buf bytes.Buffer
var eg errgroup.Group
for _, b := range filtered {
Expand Down Expand Up @@ -491,6 +505,118 @@ func pingHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "err=%v\n", err)
}

var pm = &powerManager{}

type powerManager struct {
tasmotaTopic string

mqtt mqtt.Client

mu sync.Mutex
users int
}

func (pm *powerManager) init() error {
var powerManagerConfig struct {
// e.g. tcp://10.0.0.54:1883, which is a static DHCP lease for the dr.lan
// Raspberry Pi, which is running an MQTT broker in my network.
MQTTBroker string `json:"mqtt_broker"`
// e.g. cmnd/tasmota_B79957/Power
TasmotaTopic string `json:"tasmota_topic"`
}
f, err := os.Open("/perm/bootery/powermanager.json")
if err != nil {
if os.IsNotExist(err) {
return nil // no power management desired
}
return err
}
defer f.Close()
if err := json.NewDecoder(f).Decode(&powerManagerConfig); err != nil {
return err
}

pm.tasmotaTopic = powerManagerConfig.TasmotaTopic

log.Printf("Connecting to MQTT broker %q", powerManagerConfig.MQTTBroker)
opts := mqtt.NewClientOptions().AddBroker(powerManagerConfig.MQTTBroker)
hostname := ""
if h, err := os.Hostname(); err == nil {
hostname = h
}
opts.SetClientID("bootery@" + hostname)
opts.SetConnectRetry(true)
mqttClient := mqtt.NewClient(opts)
if token := mqttClient.Connect(); token.Wait() && token.Error() != nil {
return fmt.Errorf("MQTT connection failed: %v", token.Error())
}
log.Printf("MQTT connected, controlling tasmota via topic %q", pm.tasmotaTopic)
pm.mqtt = mqttClient
return nil
}

func (pm *powerManager) power(payload string) error {
if pm.mqtt == nil {
return nil // no power management possible
}
log.Printf("power(%s)", payload)
token := pm.mqtt.Publish(pm.tasmotaTopic, 0 /* qos */, false, payload)
if token.Wait() && token.Error() != nil {
return token.Error()
}
return nil
}

func (pm *powerManager) use() error {
pm.mu.Lock()
pm.users++
log.Printf("use(), users=%d", pm.users)
pm.mu.Unlock()

return pm.power("ON")
}

func (pm *powerManager) release() error {
pm.mu.Lock()
pm.users--
log.Printf("release(), users=%d", pm.users)
pm.mu.Unlock()

go func() {
time.Sleep(10 * time.Minute)

pm.mu.Lock()
anyUsers := pm.users > 0
pm.mu.Unlock()
if anyUsers {
return // some other release() call will check
}
if err := pm.power("OFF"); err != nil {
log.Printf("auto power-off: %v", err)
}
}()

return nil
}

func (pm *powerManager) awaitHealthy(ctx context.Context) (err error) {
if pm.mqtt == nil {
return nil // no power management possible
}
log.Printf("awaitHealthy")
defer func() {
log.Printf("awaitHealthy returns err=%v", err)
}()
for {
if err := ctx.Err(); err != nil {
return err
}
if err := pingBakeries(ctx); err == nil {
return nil
}
}
}

func loadBakeries() error {
f, err := os.Open("/perm/bootery/bakeries.json")
if err != nil {
Expand Down Expand Up @@ -520,29 +646,39 @@ func enableUnprivilegedPing() error {
return ioutil.WriteFile("/proc/sys/net/ipv4/ping_group_range", []byte("0\t2147483647"), 0600)
}

func main() {
func bootery() error {
flag.Parse()

if err := enableUnprivilegedPing(); err != nil {
log.Fatal(err)
return err
}

if err := loadBakeries(); err != nil {
log.Fatal(err)
return err
}

if err := loadHTTPPassword(); err != nil {
log.Fatal(err)
return err
}

for _, b := range bakeries {
if err := b.init(); err != nil {
log.Fatal(err)
return err
}
}

if err := pm.init(); err != nil {
return err
}

http.HandleFunc("/testboot", authenticated(testbootHandler))
http.HandleFunc("/serial", authenticated(serialHandler))
http.HandleFunc("/ping", authenticated(pingHandler))
log.Fatal(http.ListenAndServe(*listen, nil))
return http.ListenAndServe(*listen, nil)
}

func main() {
if err := bootery(); err != nil {
log.Fatal(err)
}
}
3 changes: 3 additions & 0 deletions go.mod
Expand Up @@ -3,9 +3,12 @@ module github.com/gokrazy/bakery
go 1.17

require (
github.com/eclipse/paho.mqtt.golang v1.3.5
github.com/gokrazy/internal v0.0.0-20200626090505-539bb61868de
github.com/gokrazy/updater v0.0.0-20210106211705-4d92b338dd24
golang.org/x/net v0.0.0-20220225172249-27dd8689420f
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e
)

require github.com/gorilla/websocket v1.4.2 // indirect
9 changes: 9 additions & 0 deletions go.sum
@@ -1,15 +1,24 @@
github.com/eclipse/paho.mqtt.golang v1.3.5 h1:sWtmgNxYM9P2sP+xEItMozsR3w0cqZFlqnNN1bdl41Y=
github.com/eclipse/paho.mqtt.golang v1.3.5/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc=
github.com/gokrazy/internal v0.0.0-20200626090505-539bb61868de h1:sJ0qBKOIU1j3BLz6TIW3Y1Fg6hR/FZtGEonjkpfTMQQ=
github.com/gokrazy/internal v0.0.0-20200626090505-539bb61868de/go.mod h1:LA5TQy7LcvYGQOy75tkrYkFUhbV2nl5qEBP47PSi2JA=
github.com/gokrazy/updater v0.0.0-20210106211705-4d92b338dd24 h1:tGm83sgE6OiTIyHylO7ZBsBZcMocwLdvtBIOJbpyUIU=
github.com/gokrazy/updater v0.0.0-20210106211705-4d92b338dd24/go.mod h1:PYOvzGOL4nlBmuxu7IyKQTFLaxr61+WPRNRzVtuYOHw=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200406155108-e3b113bbe6a4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

0 comments on commit a32e6d0

Please sign in to comment.