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

Commit

Permalink
[APP-4985] OS Automatic Upgrade Control (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
Otterverse authored Aug 5, 2024
1 parent fdac1f4 commit 812b789
Show file tree
Hide file tree
Showing 5 changed files with 241 additions and 14 deletions.
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,21 @@ This is a subsystem (plugin) for the viam-agent that provides a number of system
## Current Options
Parameters are set via the `attributes` object of the agent-syscfg object in the agent config (currently via "Raw JSON" editor in https://app.viam.com/ )

Configuration is split into sections, each of which will control a different area of system management and/or configuration. Currently only logging control is available.
Configuration is split into sections, each of which will control a different area of system management and/or configuration. Currently only logging and automatic upgrade control is available.

### Logging
Two parameters can be set for logging control. `system_max_use` and `runtime_max_use` The first sets the maximum disk space journald will user for persistent log storage. The second, the runtime/temporary limit. Both of these will be configured to 512M by default if not set. Numeric values are in bytes, with optional single letter suffix for larger units, e.g. K, M, or G.
There is also `disable` which may be set to `true` to remove any prior tweaks to the logging config and disable the use of defaults.

### Automatic Upgrades
This enables (or disables) the "unattended upgrades" functionality in Debian (currently only bullseye or bookworm.) Set the `type` parameter to one of the following:
* `` (blank or unset, default) will do/change nothing
* `disable` will actively disable automatic upgrades
* `security` will only enable updates from sources with `security` in their codename, ex: `bookworm-security`
* `all` will enable updates from all configured repos/sources

Note that this will install the `unattended-upgrades` package, and then replace `20auto-upgrades` and `50unattended-upgrades` in `/etc/apt/apt.conf.d/`, with the latter's Origins-Pattern list being generated automatically from configured repositories on the system, so custom repos (at the time the setting is enabled) will be included.

## Example Config

```json
Expand All @@ -20,6 +29,9 @@ There is also `disable` which may be set to `true` to remove any prior tweaks to
"disable": true,
"system_max_use": "128M",
"runtime_max_use": "96M"
},
"upgrades": {
"type": "all"
}
}
}
Expand Down
10 changes: 7 additions & 3 deletions cmd/viam-agent-syscfg/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,19 @@ func main() {
}

log.Debugf("Config: %+v", cfg)

// exact text "startup complete" is important, the parent process will watch for this line to indicate startup is successful
log.Info("agent-syscfg startup complete")

// core one-shot functions start

// set journald max size limits
syscfg.EnforceLogging(cfg.Logging, log)

// core one-shot functions end
// set unattended upgrade
syscfg.EnforceUpgrades(ctx, cfg.Upgrades, log)

// exact text "startup complete" is important, the parent process will watch for this line to indicate startup is successful
log.Info("agent-syscfg startup complete")
// core one-shot functions end

// do nothing forever, just respond to health checks
for {
Expand Down
42 changes: 33 additions & 9 deletions logcfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,22 @@ func EnforceLogging(cfg LogConfig, log *zap.SugaredLogger) {
return
}
log.Error(errw.Wrapf(err, "deleting %s", journaldConfPath))
return
}

if !checkJournaldEnabled(log) {
return
}

if err := restartJournald(); err != nil {
log.Error(err)
return
}
log.Infof("Logging config disabled. Removing customized %s", journaldConfPath)
return
}

cmd := exec.Command("systemctl", "is-enabled", "systemd-journald")
output, err := cmd.CombinedOutput()
if err != nil {
log.Error(errw.Wrapf(err, "executing 'systemctl is-enabled systemd-journald' %s", output))
log.Error("agent-syscfg can only adjust logging settings for systems using systemd with journald enabled")
if !checkJournaldEnabled(log) {
return
}

Expand Down Expand Up @@ -86,12 +92,30 @@ func EnforceLogging(cfg LogConfig, log *zap.SugaredLogger) {
}

if isNew {
cmd = exec.Command("systemctl", "restart", "systemd-journald")
output, err = cmd.CombinedOutput()
if err != nil {
log.Error(errw.Wrapf(err, "executing 'systemctl restart systemd-journald' %s", output))
if err := restartJournald(); err != nil {
log.Error(err)
return
}
log.Infof("Updated %s, setting SystemMaxUse=%s and RuntimeMaxUse=%s", journaldConfPath, persistSize, tempSize)
}
}

func restartJournald() error {
cmd := exec.Command("systemctl", "restart", "systemd-journald")
output, err := cmd.CombinedOutput()
if err != nil {
return errw.Wrapf(err, "executing 'systemctl restart systemd-journald' %s", output)
}
return nil
}

func checkJournaldEnabled(log *zap.SugaredLogger) bool {
cmd := exec.Command("systemctl", "is-enabled", "systemd-journald")
output, err := cmd.CombinedOutput()
if err != nil {
log.Error(errw.Wrapf(err, "executing 'systemctl is-enabled systemd-journald' %s", output))
log.Error("agent-syscfg can only adjust logging settings for systems using systemd with journald enabled")
return false
}
return true
}
3 changes: 2 additions & 1 deletion syscfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ func GetRevision() string {
}

type Config struct {
Logging LogConfig `json:"logging"`
Logging LogConfig `json:"logging"`
Upgrades UpgradesConfig `json:"upgrades"`
}

func LoadConfig(path string) (*Config, error) {
Expand Down
186 changes: 186 additions & 0 deletions upgrades.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package syscfg

// This file contains tweaks for enabling/disabling unattended upgrades.

import (
"context"
"fmt"
"os"
"os/exec"
"regexp"
"strings"
"time"

errw "github.com/pkg/errors"
"go.uber.org/zap"
)

const (
autoUpgradesPath = "/etc/apt/apt.conf.d/20auto-upgrades"
autoUpgradesContentsEnabled = `APT::Periodic::Update-Package-Lists "1";` + "\n" + `APT::Periodic::Unattended-Upgrade "1";` + "\n"
autoUpgradesContentsDisabled = `APT::Periodic::Update-Package-Lists "1";` + "\n" + `APT::Periodic::Unattended-Upgrade "0";` + "\n"

unattendedUpgradesPath = "/etc/apt/apt.conf.d/50unattended-upgrades"
)

type UpgradesConfig struct {
// Type can be
// Empty/missing ("") to make no changes
// "disable" (or "disabled") to disable auto-upgrades
// "security" to enable ONLY security upgrades
// "all" to enable upgrades from all configured sources
Type string `json:"type"`
}

func EnforceUpgrades(ctx context.Context, cfg UpgradesConfig, log *zap.SugaredLogger) {
if cfg.Type == "" {
return
}

err := checkSupportedDistro()
if err != nil {
log.Error(err)
return
}

if cfg.Type == "disable" || cfg.Type == "disabled" {
isNew, err := writeFileIfNew(autoUpgradesPath, []byte(autoUpgradesContentsDisabled))
if err != nil {
log.Error(err)
}
if isNew {
log.Info("Disabled OS auto-upgrades.")
}
return
}

err = verifyInstall()
if err != nil {
err = doInstall(ctx)
if err != nil {
log.Error(err)
return
}
}

securityOnly := cfg.Type == "security"
confContents, err := generateOrigins(securityOnly)
if err != nil {
log.Error(err)
return
}

isNew1, err := writeFileIfNew(autoUpgradesPath, []byte(autoUpgradesContentsEnabled))
if err != nil {
log.Error(err)
return
}

isNew2, err := writeFileIfNew(unattendedUpgradesPath, []byte(confContents))
if err != nil {
log.Error(err)
return
}

if isNew1 || isNew2 {
if securityOnly {
log.Info("Enabled OS auto-upgrades (security only.)")
} else {
log.Info("Enabled OS auto-upgrades (full.)")
}
}

err = enableTimer()
if err != nil {
log.Error(err)
}
}

func checkSupportedDistro() error {
data, err := os.ReadFile("/etc/os-release")
if err != nil {
return err
}

if strings.Contains(string(data), "VERSION_CODENAME=bookworm") || strings.Contains(string(data), "VERSION_CODENAME=bullseye") {
return nil
}

return errw.New("cannot enable automatic upgrades for unknown distro, only support for Debian bullseye and bookworm is available")
}

// make sure the needed package is installed.
func verifyInstall() error {
cmd := exec.Command("unattended-upgrade", "-h")
output, err := cmd.CombinedOutput()
if err != nil {
return errw.Wrapf(err, "executing 'unattended-upgrade -h' %s", output)
}
return nil
}

func enableTimer() error {
// enable here
cmd := exec.Command("systemctl", "enable", "apt-daily-upgrade.timer")
output, err := cmd.CombinedOutput()
if err != nil {
return errw.Wrapf(err, "executing 'systemctl enable apt-daily-upgrade.timer' %s", output)
}
return nil
}

func doInstall(ctx context.Context) error {
// On low bandwidth connections, apt updates/installs can take a while, so start something to handle healthchecks
sleepCtx, cancel := context.WithCancel(ctx)
defer cancel()
go func() {
HealthySleep(sleepCtx, time.Hour)
}()

cmd := exec.CommandContext(ctx, "apt", "update")
output, err := cmd.CombinedOutput()
if err != nil {
return errw.Wrapf(err, "executing 'apt update' %s", output)
}

cmd = exec.CommandContext(ctx, "apt", "install", "-y", "unattended-upgrades")
output, err = cmd.CombinedOutput()
if err != nil {
return errw.Wrapf(err, "executing 'apt install -y unattended-upgrades' %s", output)
}

return nil
}

// generates the "Origins-Pattern" section of 50unattended-upgrades file.
func generateOrigins(securityOnly bool) (string, error) {
cmd := exec.Command("apt-cache", "policy")
output, err := cmd.CombinedOutput()
if err != nil {
return "", errw.Wrapf(err, "executing 'apt-cache policy' %s", output)
}

releaseRegex := regexp.MustCompile(`release.*o=([^,]+).*n=([^,]+).*`)
matches := releaseRegex.FindAllStringSubmatch(string(output), -1)

// use map to reduce to unique set
releases := map[string]bool{}
for _, release := range matches {
// we expect at least an origin and a codename from each line
if len(release) != 3 {
continue
}
if securityOnly && !strings.Contains(release[2], "security") {
continue
}
releases[fmt.Sprintf(`"origin=%s,codename=%s";`, release[1], release[2])] = true
}

// generate actual file contents
origins := "Unattended-Upgrade::Origins-Pattern {"
for release := range releases {
origins = fmt.Sprintf("%s\n %s", origins, release)
}
origins = fmt.Sprintf("%s\n};\n", origins)
return origins, nil
}

0 comments on commit 812b789

Please sign in to comment.