release: prepare agent 0.6.9 publication
This commit is contained in:
@@ -18,6 +18,7 @@ import (
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -25,7 +26,9 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const agentVersion = "0.6.8"
|
||||
const agentVersion = "0.6.9"
|
||||
|
||||
var stableVersionPattern = regexp.MustCompile(`^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$`)
|
||||
|
||||
type agentRuntimeHealth struct {
|
||||
StartedAt time.Time
|
||||
@@ -339,6 +342,10 @@ func buildInventory(cfg config) map[string]any {
|
||||
"openwrt_version": openwrtVersion(),
|
||||
"agent_version": agentVersion,
|
||||
"agent_runtime": "go",
|
||||
"agent_package": "rmm-agent-go-production",
|
||||
"package_manager": packageManager(),
|
||||
"openwrt_release": openwrtRelease(),
|
||||
"target": openwrtTarget(),
|
||||
"board": jsonObjectOrEmpty(commandOutput("ubus", "call", "system", "board")),
|
||||
"interfaces": interfaces(),
|
||||
"default_route": firstLine(commandOutput("ip", "route", "show", "default")),
|
||||
@@ -350,6 +357,23 @@ func buildInventory(cfg config) map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
func openwrtRelease() string {
|
||||
values := parseShellConfig(readFileString("/etc/openwrt_release"))
|
||||
return strings.TrimSpace(values["DISTRIB_RELEASE"])
|
||||
}
|
||||
|
||||
func openwrtTarget() string {
|
||||
var board struct {
|
||||
Release struct {
|
||||
Target string `json:"target"`
|
||||
} `json:"release"`
|
||||
}
|
||||
if json.Unmarshal([]byte(commandOutput("ubus", "call", "system", "board")), &board) != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.ReplaceAll(strings.TrimSpace(board.Release.Target), "/", "-")
|
||||
}
|
||||
|
||||
func (cfg config) displayHostname() string {
|
||||
if strings.TrimSpace(cfg.HostnameOverride) != "" {
|
||||
return strings.TrimSpace(cfg.HostnameOverride)
|
||||
@@ -423,6 +447,10 @@ func serverCheckTarget(serverURL string) string {
|
||||
|
||||
func processCommand(ctx context.Context, client *http.Client, cfg config, cmd command) {
|
||||
output, exitCode := runCommand(ctx, cfg, cmd)
|
||||
resultDetails := commandMetadata()
|
||||
if cmd.Type == "agent_update" || cmd.Type == "agent_rollback" {
|
||||
output, exitCode, resultDetails = agentPackageOperation(ctx, cmd.Type, cmd.Args)
|
||||
}
|
||||
status := "completed"
|
||||
if exitCode != 0 {
|
||||
status = "failed"
|
||||
@@ -433,7 +461,7 @@ func processCommand(ctx context.Context, client *http.Client, cfg config, cmd co
|
||||
"status": status,
|
||||
"exit_code": exitCode,
|
||||
"output": output,
|
||||
"result": map[string]any{"agent_version": agentVersion, "agent_runtime": "go"},
|
||||
"result": resultDetails,
|
||||
}
|
||||
if err := sendCommandResult(ctx, client, cfg, cmd.ID, result); err != nil {
|
||||
logf("failed to send result for %s: %v", cmd.ID, err)
|
||||
@@ -441,6 +469,10 @@ func processCommand(ctx context.Context, client *http.Client, cfg config, cmd co
|
||||
logf("failed to spool result for %s: %v", cmd.ID, err)
|
||||
}
|
||||
}
|
||||
if (cmd.Type == "agent_update" || cmd.Type == "agent_rollback") && exitCode == 0 {
|
||||
// The result is durable before replacing this running binary with the newly installed package.
|
||||
_, _ = execCommand(context.Background(), 30*time.Second, "/etc/init.d/rmm-agent", "restart")
|
||||
}
|
||||
}
|
||||
|
||||
func runCommand(ctx context.Context, cfg config, cmd command) (string, int) {
|
||||
@@ -454,13 +486,15 @@ func runCommand(ctx context.Context, cfg config, cmd command) (string, int) {
|
||||
if !safeHostName(target) {
|
||||
return "target is invalid\n", 2
|
||||
}
|
||||
return execCommand(ctx, 20*time.Second, "ping", "-c", "4", target)
|
||||
output, code := execCommand(ctx, 20*time.Second, "ping", "-c", "4", target)
|
||||
return output, code
|
||||
case "traceroute":
|
||||
target := commandTarget(args, "1.1.1.1")
|
||||
if !safeHostName(target) {
|
||||
return "target is invalid\n", 2
|
||||
}
|
||||
return execCommand(ctx, 45*time.Second, "traceroute", target)
|
||||
output, code := execCommand(ctx, 45*time.Second, "traceroute", target)
|
||||
return output, code
|
||||
case "route_show":
|
||||
return execCommand(ctx, 10*time.Second, "ip", "route", "show")
|
||||
case "interfaces_show":
|
||||
@@ -491,6 +525,8 @@ func runCommand(ctx context.Context, cfg config, cmd command) (string, int) {
|
||||
return "package name is invalid\n", 2
|
||||
}
|
||||
return runPackageCommand(ctx, "remove", packageName)
|
||||
case "agent_update", "agent_rollback":
|
||||
return "agent update is handled separately\n", 2
|
||||
case "uci_show":
|
||||
config, ok := uciConfigArg(args)
|
||||
if !ok {
|
||||
@@ -549,6 +585,96 @@ func runCommand(ctx context.Context, cfg config, cmd command) (string, int) {
|
||||
}
|
||||
}
|
||||
|
||||
func commandMetadata() map[string]any {
|
||||
return map[string]any{"agent_version": agentVersion, "agent_runtime": "go"}
|
||||
}
|
||||
|
||||
func agentPackageOperation(ctx context.Context, operation string, raw json.RawMessage) (string, int, map[string]any) {
|
||||
result := commandMetadata()
|
||||
result["operation"] = operation
|
||||
args := map[string]string{}
|
||||
if (operation != "agent_update" && operation != "agent_rollback") || json.Unmarshal(raw, &args) != nil || !validAgentPackageOperationArgs(args) || args["package"] != "rmm-agent-go-production" || !stableVersionPattern.MatchString(args["target_version"]) || !stableVersionPattern.MatchString(args["package_version"]) || !safeAgentFeedURL(args["feed_url"]) || (args["package_manager"] != "opkg" && args["package_manager"] != "apk") {
|
||||
result["reason"] = "invalid package operation arguments"
|
||||
return "agent package operation arguments are invalid\n", 2, result
|
||||
}
|
||||
if packageManager() != args["package_manager"] {
|
||||
result["reason"] = "package manager does not match inventory"
|
||||
return "agent package operation package manager does not match this router\n", 2, result
|
||||
}
|
||||
availableKB, ok := rootAvailableKB()
|
||||
result["available_kb"] = availableKB
|
||||
if !ok || availableKB < 8192 {
|
||||
result["reason"] = "insufficient disk space"
|
||||
return "agent package operation requires at least 8192 KiB free on /\n", 1, result
|
||||
}
|
||||
result["feed_url"] = args["feed_url"]
|
||||
result["package"] = args["package"]
|
||||
result["previous_version"] = agentVersion
|
||||
result["target_version"] = args["target_version"]
|
||||
result["package_version"] = args["package_version"]
|
||||
if args["package_manager"] == "apk" {
|
||||
apkArgs := []string{"add"}
|
||||
if operation == "agent_rollback" {
|
||||
apkArgs = append(apkArgs, "--allow-downgrade")
|
||||
} else {
|
||||
apkArgs = append(apkArgs, "--upgrade")
|
||||
}
|
||||
apkArgs = append(apkArgs, "--repository", args["feed_url"], args["package"]+"="+args["package_version"])
|
||||
output, code := execCommand(ctx, 2*time.Minute, "apk", apkArgs...)
|
||||
return output, code, result
|
||||
}
|
||||
if err := configureManagedOpkgFeed(args["feed_url"]); err != nil {
|
||||
result["reason"] = "configure managed feed"
|
||||
return err.Error() + "\n", 1, result
|
||||
}
|
||||
updateOutput, updateCode := execCommand(ctx, 90*time.Second, "opkg", "update")
|
||||
if updateCode != 0 {
|
||||
return updateOutput, updateCode, result
|
||||
}
|
||||
installOutput, installCode := execCommand(ctx, 2*time.Minute, "opkg", "install", args["package"]+"="+args["package_version"])
|
||||
return updateOutput + installOutput, installCode, result
|
||||
}
|
||||
|
||||
func validAgentPackageOperationArgs(args map[string]string) bool {
|
||||
for key := range args {
|
||||
switch key {
|
||||
case "package", "target_version", "package_version", "feed_url", "package_manager", "rollout_id", "channel":
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func safeAgentFeedURL(raw string) bool {
|
||||
u, err := url.Parse(strings.TrimSpace(raw))
|
||||
return err == nil && u.Scheme == "https" && u.Host != "" && u.RawQuery == "" && u.Fragment == "" && !strings.Contains(u.Path, "..")
|
||||
}
|
||||
|
||||
func rootAvailableKB() (int64, bool) {
|
||||
lines := strings.Split(strings.TrimSpace(commandOutput("df", "-k", "/")), "\n")
|
||||
if len(lines) < 2 {
|
||||
return 0, false
|
||||
}
|
||||
fields := strings.Fields(lines[len(lines)-1])
|
||||
if len(fields) < 4 {
|
||||
return 0, false
|
||||
}
|
||||
available, err := strconv.ParseInt(fields[3], 10, 64)
|
||||
return available, err == nil
|
||||
}
|
||||
|
||||
func configureManagedOpkgFeed(feedURL string) error {
|
||||
const path = "/etc/opkg/rmm-agent-managed.conf"
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(path+".tmp", []byte("src/gz rmm-agent-managed "+feedURL+"\n"), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(path+".tmp", path)
|
||||
}
|
||||
|
||||
func commandTarget(args map[string]string, fallback string) string {
|
||||
target := strings.TrimSpace(args["target"])
|
||||
if target == "" {
|
||||
|
||||
@@ -7,16 +7,47 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestAgentVersionIsStable(t *testing.T) {
|
||||
if agentVersion != "0.6.8" {
|
||||
if agentVersion != "0.6.9" {
|
||||
t.Fatalf("unexpected agent version %q", agentVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafeAgentFeedURL(t *testing.T) {
|
||||
for _, value := range []string{"https://packages.example.test/stable/24.10.7/x86-64", "https://packages.example.test/feed"} {
|
||||
if !safeAgentFeedURL(value) {
|
||||
t.Fatalf("expected %q to be accepted", value)
|
||||
}
|
||||
}
|
||||
for _, value := range []string{"http://packages.example.test/feed", "https://packages.example.test/feed?next=x", "https://packages.example.test/a/../feed", ""} {
|
||||
if safeAgentFeedURL(value) {
|
||||
t.Fatalf("expected %q to be rejected", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidAgentPackageOperationArgs(t *testing.T) {
|
||||
valid := map[string]string{"package": "rmm-agent-go-production", "target_version": "0.6.8", "package_version": "0.6.8-1", "feed_url": "https://packages.example.test/feed", "package_manager": "apk"}
|
||||
if !validAgentPackageOperationArgs(valid) {
|
||||
t.Fatal("expected exact package operation arguments to be accepted")
|
||||
}
|
||||
valid["unexpected"] = "value"
|
||||
if validAgentPackageOperationArgs(valid) {
|
||||
t.Fatal("unexpected package operation field was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenWrtTargetManifestFormat(t *testing.T) {
|
||||
if strings.ReplaceAll("x86/64", "/", "-") != "x86-64" {
|
||||
t.Fatal("OpenWrt target was not normalized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLocalInterfaceIPv4CandidatesPrefersLAN(t *testing.T) {
|
||||
output := `1: lo inet 127.0.0.1/8 scope host lo
|
||||
2: eth0 inet 203.0.113.10/24 brd 203.0.113.255 scope global eth0
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=rmm-agent-go-production
|
||||
PKG_VERSION:=0.6.8
|
||||
PKG_VERSION:=0.6.9
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_MAINTAINER:=RMM OpenWrt
|
||||
|
||||
Reference in New Issue
Block a user