Skip to content
This repository has been archived by the owner on Oct 24, 2024. It is now read-only.

Commit

Permalink
[APP-3490] Provisioning/Networking Subsystem
Browse files Browse the repository at this point in the history
  • Loading branch information
Otterverse authored Feb 1, 2024
2 parents 2e99820 + 226fcf1 commit 92eda9a
Show file tree
Hide file tree
Showing 11 changed files with 2,062 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bin/
47 changes: 47 additions & 0 deletions Makefile
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
50 changes: 49 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,52 @@
This is a subsystem (plugin) for the viam-agent that provides network (wifi) management and headless provisioning services.

## WIP
This is a work in progress. This readme will be updated when ready for use.
This is a work in progress projects changing rapidly. More features will be coming soon.

## Installation
This will be automatically installed for online devices (robots) that are using the agent. See install instructions at https://github.com/viamrobotics/agent

It should work "out of the box" on Debian Bookworm or newer. For Bullseye, it's neccessary to switch the network configuration to using NetworkManager first. `sudo raspi-config` and then navigate `Advanced Options`>`Network Config`>`NetworkManager` (this will be automated soon.)

### Offline/pre-installation
Scripted and detailed documentation coming soon.

## Configuration
No configuration is neccessary for basic use. Provisioning mode will start a hotspot when either not configured (no /etc/viam.json) or not online.

### Additional Networks (Optional)
To add additional networks to an already-only device, go to the "Raw JSON" button on the Config tab for your robot/device in https://app.viam.com

From there, add an `attributes` field to the agent-provisioning subsystem, using the example below.

```json
"agent_config": {
"subsystems": {
"agent-provisioning": {
"release_channel": "stable",
"pin_version": "",
"pin_url": "",
"disable_subsystem": false,
"attributes": {
"hotspot_password": "testpass",
"networks": [
{
"type": "wifi",
"ssid": "primaryNet",
"psk": "myFirstPassword",
"priority": 30
},
{
"type": "wifi",
"ssid": "fallbackNet",
"psk": "mySecondPassword",
"priority": 10
}
]
}
},
```
Note: the `hotspot_password` overrides the default `viamsetup` password used to connect to the hotspot if you wish to further secure things. It is optional and can be omitted entirely.

## Use
Provisioning mode will start a hotspot when either not configured (no /etc/viam.json) or not online. By default, the wifi SSID will be `viam-setup-$HOSTNAME`. The default password is `viamsetup`. After connecting with a mobile device, you should be redirected to a sign-in page. If you are using a laptop or are not redirected, try opening http://viam.setup/ in a browswer. From the portal page, you can select an SSID and provide a password to allow your device to connect. The setup hotspot will disappear (and disconnect your mobile device) while the device attempts connection. If the hotspot reappears, there may be have been an issue or invalid password. Please try again.
260 changes: 260 additions & 0 deletions cmd/viam-agent-provisioning/main.go
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)
}
}
23 changes: 23 additions & 0 deletions go.mod
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
)
Loading

0 comments on commit 92eda9a

Please sign in to comment.