Files
elmprodvpn/selective-vpn-api/app/transport_backends_adapter_systemd_migration.go

219 lines
6.9 KiB
Go

package app
import (
"fmt"
"os"
"strings"
"time"
)
const (
transportSingBoxLegacyMigrateConfigKey = "singbox_legacy_unit_migrate"
transportSingBoxLegacyMigrateDryRunConfigKey = "singbox_legacy_unit_migrate_dry_run"
)
func transportSystemdMaybeMigrateLegacySingBoxUnits(client TransportClient, units []string) ([]string, []string) {
out := make([]string, 0, 4)
errOut := make([]string, 0, 4)
if !transportSystemdLegacySingBoxMigrationEnabled(client) {
return out, errOut
}
target := transportSystemdResolveSingBoxTemplateTargetUnit(client, units)
if target == "" {
return out, errOut
}
dryRun := transportSystemdLegacySingBoxMigrationDryRun(client)
legacyUnits := transportSystemdLegacySingBoxUnitCandidates(client, target)
if len(legacyUnits) == 0 {
return out, errOut
}
migrated := make([]string, 0, len(legacyUnits))
for _, legacyUnit := range legacyUnits {
if strings.EqualFold(strings.TrimSpace(legacyUnit), target) {
continue
}
path := transportSystemdUnitPath(legacyUnit)
owned, err := transportSystemdUnitOwnedByClient(path, client.ID)
if err != nil {
if os.IsNotExist(err) {
continue
}
errOut = append(errOut, fmt.Sprintf("legacy-migrate ownership check failed (%s): %v", legacyUnit, err))
appendTraceLineRateLimited(
"transport",
fmt.Sprintf("legacy unit migrate warning: client=%s unit=%s ownership-check err=%v", client.ID, legacyUnit, err),
30*time.Second,
)
continue
}
if !owned {
appendTraceLineRateLimited(
"transport",
fmt.Sprintf("legacy unit migrate skip: client=%s unit=%s reason=ownership-mismatch", client.ID, legacyUnit),
30*time.Second,
)
continue
}
if dryRun {
msg := fmt.Sprintf("legacy-migrate dry-run: %s -> %s", legacyUnit, target)
out = append(out, msg)
appendTraceLineRateLimited(
"transport",
fmt.Sprintf("legacy unit migrate dry-run: client=%s from=%s to=%s", client.ID, legacyUnit, target),
20*time.Second,
)
continue
}
transportSystemdRunLegacyMigrateCommand("stop", legacyUnit, &out, &errOut)
transportSystemdRunLegacyMigrateCommand("disable", legacyUnit, &out, &errOut)
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
errOut = append(errOut, fmt.Sprintf("legacy-migrate remove failed (%s): %v", legacyUnit, err))
appendTraceLineRateLimited(
"transport",
fmt.Sprintf("legacy unit migrate warning: client=%s unit=%s remove err=%v", client.ID, legacyUnit, err),
30*time.Second,
)
continue
}
out = append(out, fmt.Sprintf("legacy-migrate: %s -> %s", legacyUnit, target))
migrated = append(migrated, legacyUnit)
appendTraceLine(
"transport",
fmt.Sprintf("legacy unit migrated: client=%s from=%s to=%s", client.ID, legacyUnit, target),
)
}
if dryRun || len(migrated) == 0 {
return out, errOut
}
stdout, stderr, code, err := transportRunCommand(transportBackendActionTimeout, "systemctl", "daemon-reload")
if s := strings.TrimSpace(stdout); s != "" {
out = append(out, "legacy-migrate daemon-reload: "+s)
}
if s := strings.TrimSpace(stderr); s != "" {
errOut = append(errOut, "legacy-migrate daemon-reload: "+s)
}
if err != nil || code != 0 {
errOut = append(errOut, fmt.Sprintf("legacy-migrate daemon-reload failed (exit=%d)", code))
appendTraceLineRateLimited(
"transport",
fmt.Sprintf("legacy unit migrate warning: client=%s daemon-reload code=%d err=%v", client.ID, code, err),
20*time.Second,
)
}
for _, legacyUnit := range migrated {
stdout, stderr, code, err = transportRunCommand(transportBackendActionTimeout, "systemctl", "reset-failed", legacyUnit)
if s := strings.TrimSpace(stdout); s != "" {
out = append(out, fmt.Sprintf("legacy-migrate reset-failed %s: %s", legacyUnit, s))
}
if s := strings.TrimSpace(stderr); s != "" {
errOut = append(errOut, fmt.Sprintf("legacy-migrate reset-failed %s: %s", legacyUnit, s))
}
if err != nil || code != 0 {
errOut = append(errOut, fmt.Sprintf("legacy-migrate reset-failed %s failed (exit=%d)", legacyUnit, code))
appendTraceLineRateLimited(
"transport",
fmt.Sprintf("legacy unit migrate warning: client=%s reset-failed unit=%s code=%d err=%v", client.ID, legacyUnit, code, err),
20*time.Second,
)
}
}
return out, errOut
}
func transportSystemdRunLegacyMigrateCommand(action, unit string, out, errOut *[]string) {
stdout, stderr, code, err := transportRunCommand(transportBackendActionTimeout, "systemctl", action, unit)
if s := strings.TrimSpace(stdout); s != "" {
*out = append(*out, fmt.Sprintf("legacy-migrate %s %s: %s", action, unit, s))
}
if s := strings.TrimSpace(stderr); s != "" {
*errOut = append(*errOut, fmt.Sprintf("legacy-migrate %s %s: %s", action, unit, s))
}
if err == nil && code == 0 {
return
}
if transportSystemdUnitMissingError(stdout, stderr, code, err) {
return
}
*errOut = append(*errOut, fmt.Sprintf("legacy-migrate %s %s failed (exit=%d)", action, unit, code))
appendTraceLineRateLimited(
"transport",
fmt.Sprintf("legacy unit migrate warning: action=%s unit=%s code=%d err=%v", action, unit, code, err),
20*time.Second,
)
}
func transportSystemdLegacySingBoxMigrationEnabled(client TransportClient) bool {
if client.Kind != TransportClientSingBox {
return false
}
if transportConfigHasKey(client.Config, transportSingBoxLegacyMigrateConfigKey) {
return transportConfigBool(client.Config, transportSingBoxLegacyMigrateConfigKey)
}
return true
}
func transportSystemdLegacySingBoxMigrationDryRun(client TransportClient) bool {
return transportConfigBool(client.Config, transportSingBoxLegacyMigrateDryRunConfigKey)
}
func transportSystemdResolveSingBoxTemplateTargetUnit(client TransportClient, units []string) string {
for _, unit := range units {
u := strings.TrimSpace(unit)
if transportSystemdSingBoxUsesTemplateInstance(client, u) {
return u
}
}
return ""
}
func transportSystemdLegacySingBoxUnitCandidates(client TransportClient, targetUnit string) []string {
candidates := make([]string, 0, 4)
addCandidate := func(unit string) {
u := strings.TrimSpace(unit)
if !validSystemdUnitName(u) {
return
}
for _, existing := range candidates {
if strings.EqualFold(existing, u) {
return
}
}
candidates = append(candidates, u)
}
instanceRaw := transportSystemdInstanceIDFromUnit(targetUnit)
if instanceRaw != "" {
addCandidate("singbox-" + strings.ToLower(instanceRaw) + ".service")
if normalized := sanitizeID(instanceRaw); normalized != "" {
addCandidate("singbox-" + normalized + ".service")
}
}
if id := sanitizeID(client.ID); id != "" {
addCandidate("singbox-" + id + ".service")
}
return candidates
}
func transportSystemdInstanceIDFromUnit(unit string) string {
u := strings.TrimSpace(unit)
if u == "" || !strings.HasSuffix(strings.ToLower(u), ".service") {
return ""
}
at := strings.IndexByte(u, '@')
if at <= 0 || at+1 >= len(u) {
return ""
}
instance := strings.TrimSpace(strings.TrimSuffix(u[at+1:], ".service"))
if instance == "" {
return ""
}
return instance
}