This repository has been archived by the owner on Oct 24, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[APP-3490] Provisioning/Networking Subsystem
- Loading branch information
Showing
11 changed files
with
2,062 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
bin/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
GOOS ?= "linux" | ||
GOARCH ?= $(shell go env GOARCH) | ||
ifeq ($(GOARCH),amd64) | ||
LINUX_ARCH = x86_64 | ||
else ifeq ($(GOARCH),arm64) | ||
LINUX_ARCH = aarch64 | ||
endif | ||
|
||
GIT_REVISION = $(shell git rev-parse HEAD | tr -d '\n') | ||
TAG_VERSION ?= $(shell git tag --points-at | sort -Vr | head -n1 | cut -c2-) | ||
ifeq ($(TAG_VERSION),) | ||
PATH_VERSION = custom | ||
else | ||
PATH_VERSION = v$(TAG_VERSION) | ||
endif | ||
|
||
LDFLAGS = "-s -w -X 'github.com/viamrobotics/agent-provisioning.Version=${TAG_VERSION}' -X 'github.com/viamrobotics/agent-provisioning.GitRevision=${GIT_REVISION}'" | ||
TAGS = osusergo,netgo | ||
|
||
.DEFAULT_GOAL := bin/viam-agent-provisioning-$(PATH_VERSION)-$(LINUX_ARCH) | ||
|
||
.PHONY: all | ||
all: amd64 arm64 | ||
|
||
.PHONY: arm64 | ||
arm64: | ||
make GOARCH=arm64 | ||
|
||
.PHONY: amd64 | ||
amd64: | ||
make GOARCH=amd64 | ||
|
||
bin/viam-agent-provisioning-$(PATH_VERSION)-$(LINUX_ARCH): go.* *.go */*.go */*/*.go portal/templates/* | ||
go build -o $@ -tags $(TAGS) -ldflags $(LDFLAGS) ./cmd/viam-agent-provisioning/main.go | ||
test "$(PATH_VERSION)" != "custom" && cp $@ bin/viam-agent-provisioning-stable-$(LINUX_ARCH) || true | ||
|
||
.PHONY: clean | ||
clean: | ||
rm -rf bin/ | ||
|
||
bin/golangci-lint: | ||
GOBIN=`pwd`/bin go install github.com/golangci/golangci-lint/cmd/[email protected] | ||
|
||
.PHONY: lint | ||
lint: bin/golangci-lint | ||
go mod tidy | ||
bin/golangci-lint run -v --fix |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,260 @@ | ||
package main | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"fmt" | ||
"os" | ||
"os/signal" | ||
"sync" | ||
"sync/atomic" | ||
"syscall" | ||
"time" | ||
|
||
"github.com/edaniels/golog" | ||
"github.com/jessevdk/go-flags" | ||
errw "github.com/pkg/errors" | ||
|
||
"github.com/viamrobotics/agent-provisioning" | ||
netman "github.com/viamrobotics/agent-provisioning/networkmanager" | ||
) | ||
|
||
var ( | ||
// only changed/set at startup, so no mutex. | ||
log = golog.NewDevelopmentLogger("agent-provisioning") | ||
activeBackgroundWorkers sync.WaitGroup | ||
) | ||
|
||
func main() { | ||
ctx := setupExitSignalHandling() | ||
|
||
var opts struct { | ||
Config string `default:"/opt/viam/etc/agent-provisioning.json" description:"Path to config file" long:"config" short:"c"` | ||
AppConfig string `default:"/etc/viam.json" description:"Path to main viam cloud (app) config file" long:"app" short:"a"` | ||
ProvisioningConfig string `default:"/etc/viam-provisioning.json" description:"Path to provisioning (customization) config file" long:"provisioning" short:"p"` | ||
Debug bool `description:"Enable debug logging" long:"debug" short:"d"` | ||
Help bool `description:"Show this help message" long:"help" short:"h"` | ||
Version bool `description:"Show version" long:"version" short:"v"` | ||
//Install bool `description:"Install systemd service" long:"install"` | ||
} | ||
|
||
parser := flags.NewParser(&opts, flags.IgnoreUnknown) | ||
parser.Usage = "runs as a background service and manages updates and the process lifecycle for viam-server." | ||
|
||
_, err := parser.Parse() | ||
exitIfError(err) | ||
|
||
if opts.Help { | ||
var b bytes.Buffer | ||
parser.WriteHelp(&b) | ||
//nolint:forbidigo | ||
fmt.Println(b.String()) | ||
return | ||
} | ||
|
||
if opts.Version { | ||
fmt.Printf("Version: %s\nGit Revision: %s\n", provisioning.GetVersion(), provisioning.GetRevision()) | ||
return | ||
} | ||
|
||
if opts.Debug { | ||
log = golog.NewDebugLogger("agent-provisioning") | ||
} | ||
|
||
pCfg, err := provisioning.LoadProvisioningConfig(opts.ProvisioningConfig) | ||
if err != nil { | ||
log.Warn(err) | ||
} | ||
|
||
cfg, err := provisioning.LoadConfig(opts.Config) | ||
if err != nil { | ||
log.Warn(err) | ||
} | ||
|
||
// If user settings override the hotspot password, use that instead | ||
if cfg.HotspotPassword != "" { | ||
pCfg.HotspotPassword = cfg.HotspotPassword | ||
} | ||
|
||
nm, err := netman.NewNMWrapper(log, pCfg) | ||
if err != nil { | ||
log.Error(err) | ||
return | ||
} | ||
defer nm.Close() | ||
|
||
for _, network := range cfg.Networks { | ||
log.Debugf("adding/updating NetworkManager configuration for %s", network.SSID) | ||
if err := nm.AddOrUpdateConnection(network); err != nil { | ||
log.Error(errw.Wrapf(err, "error adding network %s", network.SSID)) | ||
} | ||
} | ||
|
||
// exact text is important, the parent process will watch for this line to indicate startup is successful | ||
log.Info("agent-provisioning startup complete") | ||
|
||
var prevError error | ||
|
||
// initial scan | ||
if err := nm.WifiScan(ctx); err != nil { | ||
log.Error(err) | ||
} | ||
|
||
var settingsChan <-chan netman.WifiSettings | ||
for { | ||
if !provisioning.HealthySleep(ctx, time.Second * 15) { | ||
activeBackgroundWorkers.Wait() | ||
return | ||
} | ||
|
||
online, err := nm.CheckOnline() | ||
if err != nil { | ||
log.Error(err) | ||
} | ||
|
||
if online { | ||
nm.MarkSSIDsTried() | ||
} | ||
|
||
// check if we have a readable cloud config, if not, we need to enter provisioning mode | ||
_, err = os.ReadFile(opts.AppConfig) | ||
|
||
configured := err == nil | ||
|
||
log.Debugf("online: %t, config_present: %t", online, configured) | ||
|
||
// restart the loop if everything is good | ||
if online && configured { | ||
continue | ||
} | ||
|
||
// provisioning mode logic starts here for when not online and configured | ||
if err := nm.WifiScan(ctx); err != nil { | ||
log.Error(err) | ||
} | ||
provisioningMode, provisioningTime := nm.GetProvisioning() | ||
_, _, lastOnline := nm.GetOnline() | ||
// not in provisioning mode, so start it if not configured (/etc/viam.json) | ||
// OR as long as we've been OUT of provisioning for two minutes to try connections | ||
if !provisioningMode && | ||
(!configured || time.Now().After(provisioningTime.Add(time.Second)) && time.Now().After(lastOnline.Add(time.Minute * 2))) { | ||
log.Debug("starting provisioning mode") | ||
settingsChan, err = nm.StartProvisioning(ctx, prevError) | ||
if err != nil { | ||
log.Error(errw.Wrap(err, "error starting provisioning mode")) | ||
continue | ||
} | ||
provisioningMode = true | ||
} | ||
|
||
if !provisioningMode { | ||
continue | ||
} | ||
|
||
// in provisioning mode, wait for settings from user OR timeout | ||
log.Debug("provisioning mode ready, waiting for user input") | ||
|
||
var activateSSID string | ||
// will exit provisioning after the select by default | ||
shouldStopProvisioning := true | ||
select { | ||
case settings := <-settingsChan: | ||
// non-empty settings mean add a new network and exit provisioning mode | ||
if settings.SSID != "" && settings.PSK != "" { | ||
log.Debug("settings recieved") | ||
err := nm.AddOrUpdateConnection(provisioning.NetworkConfig{ | ||
Type: "wifi", | ||
SSID: settings.SSID, | ||
PSK: settings.PSK, | ||
Priority: 100, | ||
}) | ||
if err != nil { | ||
prevError = err | ||
log.Error(err) | ||
continue | ||
} | ||
activateSSID = settings.SSID | ||
} | ||
// empty settings mean a known SSID newly became visible, but we don't exit if someone's in the portal | ||
if !time.Now().After(nm.GetLastInteraction().Add(time.Minute * 5)) { | ||
shouldStopProvisioning = false | ||
} | ||
case <-ctx.Done(): | ||
log.Debug("main context cancelled") | ||
case <-time.After(10 * time.Minute): | ||
// don't exit provisioning mode if someone is active in the portal | ||
if !time.Now().After(nm.GetLastInteraction().Add(time.Minute * 5)) { | ||
shouldStopProvisioning = false | ||
} | ||
log.Debug("10 minute timeout") | ||
} | ||
|
||
if shouldStopProvisioning { | ||
log.Debug("provisioning mode stopping") | ||
err = nm.StopProvisioning() | ||
if err != nil { | ||
log.Error(err) | ||
} | ||
} | ||
// force activating the SSID to save time (or if it was somehow manually disabled) | ||
if activateSSID != "" { | ||
if err := nm.ActivateConnection(activateSSID); err != nil { | ||
prevError = err | ||
log.Error(err) | ||
} | ||
} | ||
} | ||
} | ||
|
||
func setupExitSignalHandling() context.Context { | ||
ctx, cancel := context.WithCancel(context.Background()) | ||
sigChan := make(chan os.Signal, 16) | ||
|
||
healthcheckRequest := &atomic.Bool{} | ||
ctx = context.WithValue(ctx, provisioning.HCReqKey, healthcheckRequest) | ||
|
||
activeBackgroundWorkers.Add(1) | ||
go func() { | ||
defer activeBackgroundWorkers.Done() | ||
defer cancel() | ||
for { | ||
sig := <-sigChan | ||
switch sig { | ||
// things we exit for | ||
case os.Interrupt: | ||
fallthrough | ||
case syscall.SIGQUIT: | ||
fallthrough | ||
case syscall.SIGABRT: | ||
fallthrough | ||
case syscall.SIGTERM: | ||
log.Info("exiting") | ||
signal.Ignore(os.Interrupt, syscall.SIGTERM, syscall.SIGABRT) // keeping SIGQUIT for stack trace debugging | ||
return | ||
|
||
// this will eventually be handled elsewhere as a restart, not exit | ||
case syscall.SIGHUP: | ||
|
||
// ignore SIGURG entirely, it's used for real-time scheduling notifications | ||
case syscall.SIGURG: | ||
|
||
// used by parent viam-agent for healthchecks | ||
case syscall.SIGUSR1: | ||
healthcheckRequest.Store(true) | ||
|
||
// log everything else | ||
default: | ||
log.Debugw("received unknown signal", "signal", sig) | ||
} | ||
} | ||
}() | ||
|
||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGABRT, syscall.SIGUSR1) | ||
return ctx | ||
} | ||
|
||
func exitIfError(err error) { | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
module github.com/viamrobotics/agent-provisioning | ||
|
||
go 1.21.4 | ||
|
||
require ( | ||
github.com/Wifx/gonetworkmanager/v2 v2.1.0 | ||
github.com/google/uuid v1.4.0 | ||
go.uber.org/zap v1.23.0 | ||
) | ||
|
||
require ( | ||
github.com/benbjohnson/clock v1.1.0 // indirect | ||
go.uber.org/atomic v1.7.0 // indirect | ||
go.uber.org/multierr v1.6.0 // indirect | ||
golang.org/x/sys v0.5.0 // indirect | ||
) | ||
|
||
require ( | ||
github.com/edaniels/golog v0.0.0-20230215213219-28954395e8d0 | ||
github.com/godbus/dbus/v5 v5.1.0 // indirect | ||
github.com/jessevdk/go-flags v1.5.0 | ||
github.com/pkg/errors v0.9.1 | ||
) |
Oops, something went wrong.