Add wg-quick/systemctl service control and bulk-delete list views

Per-server Start/Stop/Restart via systemd wg-quick@<iface>.service units
plus live status badge, and a table/list-view toggle with checkbox
bulk-delete for both the server list and per-server client list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PvrfUytqd74H6WcQkRzFM4
This commit is contained in:
sysops
2026-07-25 00:19:13 +02:00
co-authored by Claude Sonnet 5
parent 3a89a3cb5c
commit c29edfdcc3
6 changed files with 860 additions and 1 deletions
+88
View File
@@ -0,0 +1,88 @@
package wireguard
import (
"context"
"fmt"
"os/exec"
"github.com/ngoduykhanh/wireguard-ui/util"
)
// UnitName returns the systemd unit name managing the given WireGuard
// interface via wg-quick, e.g. "wg-quick@wg-home.service".
func UnitName(iface string) string {
return "wg-quick@" + iface + ".service"
}
// Start brings up the given WireGuard interface via
// `systemctl start wg-quick@<iface>.service`.
func Start(ctx context.Context, iface string) error {
return runSystemctl(ctx, "start", iface)
}
// Stop brings down the given WireGuard interface via
// `systemctl stop wg-quick@<iface>.service`.
func Stop(ctx context.Context, iface string) error {
return runSystemctl(ctx, "stop", iface)
}
// Restart restarts the given WireGuard interface via
// `systemctl restart wg-quick@<iface>.service`.
func Restart(ctx context.Context, iface string) error {
return runSystemctl(ctx, "restart", iface)
}
func runSystemctl(ctx context.Context, action, iface string) error {
if !util.ValidateInterfaceName(iface) {
return fmt.Errorf("invalid interface name: %q", iface)
}
cmd := exec.CommandContext(ctx, "systemctl", action, UnitName(iface))
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("systemctl %s %s failed: %w: %s", action, UnitName(iface), err, string(out))
}
return nil
}
// Status reports whether the given WireGuard interface's wg-quick unit is
// currently active and whether it is enabled to start on boot. Only real
// exec failures (e.g. systemctl binary missing) are returned as err - a
// unit being inactive/disabled/failed is a normal, non-error result
// reflected in the returned booleans.
func Status(ctx context.Context, iface string) (active bool, enabled bool, err error) {
if !util.ValidateInterfaceName(iface) {
return false, false, fmt.Errorf("invalid interface name: %q", iface)
}
unit := UnitName(iface)
active, err = systemctlBoolCheck(ctx, "is-active", unit)
if err != nil {
return false, false, err
}
enabled, err = systemctlBoolCheck(ctx, "is-enabled", unit)
if err != nil {
return active, false, err
}
return active, enabled, nil
}
// systemctlBoolCheck runs `systemctl <verb> <unit>` and interprets a
// non-zero exit code as a normal "false" result (inactive/disabled/failed),
// not an error. Only a failure to execute systemctl at all (binary missing,
// context cancelled, ...) is surfaced as err.
func systemctlBoolCheck(ctx context.Context, verb, unit string) (bool, error) {
cmd := exec.CommandContext(ctx, "systemctl", verb, unit)
err := cmd.Run()
if err == nil {
return true, nil
}
if _, ok := err.(*exec.ExitError); ok {
// systemctl ran fine and just reported a non-active/non-enabled
// state via its exit code - not an execution error.
return false, nil
}
return false, fmt.Errorf("systemctl %s %s failed to execute: %w", verb, unit, err)
}