Skip to content

Commit 03217dc

Browse files
committed
Migrate legacy server units in systemd install
1 parent 08c4927 commit 03217dc

7 files changed

Lines changed: 70 additions & 6 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ This creates a `subrouter` system user, stores state under `/var/lib/subrouter`,
8080
/usr/local/bin/subrouter serve --addr 0.0.0.0:31415 --sessions /var/lib/subrouter/sessions.json --transcripts /var/lib/subrouter/transcripts --cx-switch-interval 10m
8181
```
8282

83+
If legacy `switchboard` or `gateway` services exist, `sr install-systemd` stops and disables them, merges their `/var/lib/...` state into `/var/lib/subrouter`, and preserves their extra service args.
84+
8385
Useful endpoints:
8486

8587
```text

cmd/subrouter/install_systemd.go

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type systemdConfig struct {
3030
Start bool
3131
DryRun bool
3232
InstallAliases bool
33+
ReplaceLegacy bool
3334
}
3435

3536
func installSystemd(args []string) error {
@@ -48,6 +49,7 @@ func installSystemd(args []string) error {
4849
flags.BoolVar(&config.Start, "start", true, "enable and restart the systemd service")
4950
flags.BoolVar(&config.DryRun, "dry-run", false, "print the systemd unit without writing files")
5051
flags.BoolVar(&config.InstallAliases, "install-aliases", true, "install sr and cx symlinks to the subrouter binary")
52+
flags.BoolVar(&config.ReplaceLegacy, "replace-legacy", true, "stop legacy switchboard/gateway services and migrate their state")
5153
if err := flags.Parse(args); err != nil {
5254
return err
5355
}
@@ -56,6 +58,9 @@ func installSystemd(args []string) error {
5658
}
5759
if config.ExtraArgs == "" {
5860
config.ExtraArgs = readSystemdDefaultExtraArgs(config)
61+
if config.ExtraArgs == "" && config.ReplaceLegacy {
62+
config.ExtraArgs = readLegacySystemdExtraArgs()
63+
}
5964
}
6065
return installSystemdWithConfig(config, commandRunner{})
6166
}
@@ -77,6 +82,9 @@ func installSystemdWithConfig(config systemdConfig, runner commandRunner) error
7782
return errors.New("install-systemd must run as root; use sudo")
7883
}
7984

85+
if config.ReplaceLegacy {
86+
stopLegacySystemdServices(runner)
87+
}
8088
if err := ensureSystemUser(config, runner); err != nil {
8189
return err
8290
}
@@ -92,6 +100,9 @@ func installSystemdWithConfig(config systemdConfig, runner commandRunner) error
92100
return err
93101
}
94102
}
103+
if config.ReplaceLegacy {
104+
migrateLegacySystemdState(config, runner)
105+
}
95106
if err := installCurrentExecutable(config.InstallPath); err != nil {
96107
return err
97108
}
@@ -184,13 +195,32 @@ func ensureSystemUser(config systemdConfig, runner commandRunner) error {
184195
}
185196

186197
func readSystemdDefaultExtraArgs(config systemdConfig) string {
187-
body, err := os.ReadFile(systemdDefaultPath(config))
198+
return readDefaultValue(systemdDefaultPath(config), "SUBROUTER_EXTRA_ARGS")
199+
}
200+
201+
func readLegacySystemdExtraArgs() string {
202+
for _, legacy := range []struct {
203+
path string
204+
key string
205+
}{
206+
{"/etc/default/switchboard", "SWITCHBOARD_EXTRA_ARGS"},
207+
{"/etc/default/gateway", "GATEWAY_EXTRA_ARGS"},
208+
} {
209+
if value := readDefaultValue(legacy.path, legacy.key); value != "" {
210+
return value
211+
}
212+
}
213+
return ""
214+
}
215+
216+
func readDefaultValue(path, keyName string) string {
217+
body, err := os.ReadFile(path)
188218
if err != nil {
189219
return ""
190220
}
191221
for _, line := range strings.Split(string(body), "\n") {
192222
key, value, ok := strings.Cut(line, "=")
193-
if !ok || strings.TrimSpace(key) != "SUBROUTER_EXTRA_ARGS" {
223+
if !ok || strings.TrimSpace(key) != keyName {
194224
continue
195225
}
196226
value = strings.TrimSpace(value)
@@ -200,6 +230,24 @@ func readSystemdDefaultExtraArgs(config systemdConfig) string {
200230
return ""
201231
}
202232

233+
func stopLegacySystemdServices(runner commandRunner) {
234+
for _, service := range []string{"switchboard", "gateway"} {
235+
_ = runner.Run("systemctl", "disable", "--now", service)
236+
}
237+
}
238+
239+
func migrateLegacySystemdState(config systemdConfig, runner commandRunner) {
240+
for _, legacyHome := range []string{"/var/lib/switchboard", "/var/lib/gateway"} {
241+
if legacyHome == config.Home {
242+
continue
243+
}
244+
if info, err := os.Stat(legacyHome); err != nil || !info.IsDir() {
245+
continue
246+
}
247+
_ = runner.Run("cp", "-a", "-n", legacyHome+"/.", config.Home+"/")
248+
}
249+
}
250+
203251
func systemdDefaultPath(config systemdConfig) string {
204252
return filepath.Join("/etc/default", config.ServiceName)
205253
}

cmd/subrouter/install_systemd_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package main
22

33
import (
4+
"os"
5+
"path/filepath"
46
"strings"
57
"testing"
68
)
@@ -49,3 +51,15 @@ func TestSystemdDefaultsEscapesExtraArgs(t *testing.T) {
4951
t.Fatalf("defaults did not quote extra args:\n%s", defaults)
5052
}
5153
}
54+
55+
func TestReadDefaultValueUnquotesEnvFileValue(t *testing.T) {
56+
path := filepath.Join(t.TempDir(), "subrouter")
57+
if err := os.WriteFile(path, []byte("SUBROUTER_EXTRA_ARGS=\"--transcript-gcs-uri=gs://bucket/prefix --fetch-usage=false\"\n"), 0o644); err != nil {
58+
t.Fatal(err)
59+
}
60+
got := readDefaultValue(path, "SUBROUTER_EXTRA_ARGS")
61+
want := "--transcript-gcs-uri=gs://bucket/prefix --fetch-usage=false"
62+
if got != want {
63+
t.Fatalf("extra args = %q, want %q", got, want)
64+
}
65+
}

deploy/gcp/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Install or upgrade Subrouter on the VM:
4040
deploy/gcp/publish-subrouter.sh
4141
```
4242

43-
The publish script configures the server with `sr server add`, then runs `sr server install`. The VM downloads the public release with the same curl installer and runs `sr install-systemd`; no locally built binary is copied to the server.
43+
The publish script configures the server with `sr server add`, then runs `sr server install`. The VM downloads the public release with the same curl installer and runs `sr install-systemd`; no locally built binary is copied to the server. If legacy `switchboard` or `gateway` services exist on the VM, the systemd installer disables them and migrates their state into `/var/lib/subrouter`.
4444

4545
Join or rejoin the host to Tailscale with an auth key:
4646

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "subrouter",
3-
"version": "0.1.2",
3+
"version": "0.1.3",
44
"description": "Routes AI coding-agent traffic across subscription accounts and API keys.",
55
"license": "MIT",
66
"homepage": "https://github.com/manaflow-ai/subrouter#readme",

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "subrouter"
7-
version = "0.1.2"
7+
version = "0.1.3"
88
description = "Routes AI coding-agent traffic across subscription accounts and API keys."
99
readme = "README.md"
1010
requires-python = ">=3.9"

subrouter_cli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import urllib.request
1010
from pathlib import Path
1111

12-
VERSION = "0.1.2"
12+
VERSION = "0.1.3"
1313

1414

1515
def _go_platform() -> str:

0 commit comments

Comments
 (0)