/* * Copyright (C) 2026 Fluxer Contributors * * This file is part of Fluxer. * * Fluxer is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Fluxer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Fluxer. If not, see . */ package ops import ( "fmt" "os" "sort" "strings" "github.com/fluxerapp/fluxer/fluxer_devops/livekitctl/internal/configgen" "github.com/fluxerapp/fluxer/fluxer_devops/livekitctl/internal/errors" "github.com/fluxerapp/fluxer/fluxer_devops/livekitctl/internal/firewall" "github.com/fluxerapp/fluxer/fluxer_devops/livekitctl/internal/netutil" "github.com/fluxerapp/fluxer/fluxer_devops/livekitctl/internal/platform" "github.com/fluxerapp/fluxer/fluxer_devops/livekitctl/internal/secrets" "github.com/fluxerapp/fluxer/fluxer_devops/livekitctl/internal/state" "github.com/fluxerapp/fluxer/fluxer_devops/livekitctl/internal/util" "github.com/fluxerapp/fluxer/fluxer_devops/livekitctl/internal/validate" ) func secretsPath(st *state.BootstrapState) string { return st.Paths.SecretsPath } func LoadSecrets(st *state.BootstrapState) (*secrets.Secrets, error) { var sec secrets.Secrets if err := util.ReadJSON(secretsPath(st), &sec); err != nil { return nil, err } if sec.LiveKitAPIKey == "" { return nil, errors.NewPlatformError("Secrets file not found. Was bootstrap completed?") } return &sec, nil } func SaveSecrets(st *state.BootstrapState, sec *secrets.Secrets) error { return util.WriteJSON(secretsPath(st), sec, 0600, -1, -1) } func StatePathDefault() string { return state.DefaultPaths().StatePath } func EnsureLinuxRoot() error { if !platform.IsLinux() { return errors.NewPlatformError("This operation is only supported on Linux hosts.") } return platform.RequireRoot() } func ApplyConfigAndRestart(st *state.BootstrapState, kvBin, publicIPv4, privateIPv4 string) error { sec, err := LoadSecrets(st) if err != nil { return err } if err := configgen.WriteAllConfigs(configgen.WriteAllConfigsParams{ State: st, Secrets: sec, PublicIPv4: publicIPv4, PrivateIPv4: privateIPv4, KVBin: kvBin, }); err != nil { return err } sm := platform.DetectServiceManager() if !sm.IsSystemd() { return errors.NewPlatformError("systemd is required for managed services on this host.") } sm.DaemonReload() sm.Enable("livekit-kv.service") sm.Enable("livekit-coturn.service") sm.Enable("livekit.service") sm.Enable("caddy.service") sm.Restart("livekit-kv.service") sm.Restart("livekit-coturn.service") sm.Restart("livekit.service") sm.Restart("caddy.service") return nil } func OpStatus(st *state.BootstrapState) string { sm := platform.DetectServiceManager() if !sm.IsSystemd() { return "systemd not detected." } var parts []string for _, svc := range []string{"livekit-kv.service", "livekit-coturn.service", "livekit.service", "caddy.service"} { parts = append(parts, sm.Status(svc)) } return strings.TrimSpace(strings.Join(parts, "\n\n")) } func OpLogs(st *state.BootstrapState, service string, lines int) string { sm := platform.DetectServiceManager() if !sm.IsSystemd() { return "systemd not detected." } return sm.Logs(service, lines) } func OpRestart(services []string) error { sm := platform.DetectServiceManager() if !sm.IsSystemd() { return errors.NewPlatformError("systemd not detected.") } for _, s := range services { sm.Restart(s) } return nil } func WebhookList(st *state.BootstrapState) []string { return st.Webhooks } func WebhookAdd(st *state.BootstrapState, url string, allowHTTP bool) (bool, error) { u, err := validate.RequireWebhookURL(url, allowHTTP) if err != nil { return false, err } for _, existing := range st.Webhooks { if existing == u { return false, nil } } st.Webhooks = append(st.Webhooks, u) sort.Strings(st.Webhooks) st.Touch() return true, state.SaveState(st) } func WebhookRemove(st *state.BootstrapState, url string) (bool, error) { found := false var newList []string for _, existing := range st.Webhooks { if existing == url { found = true } else { newList = append(newList, existing) } } if !found { return false, nil } st.Webhooks = newList st.Touch() return true, state.SaveState(st) } func WebhookSet(st *state.BootstrapState, urls []string, allowHTTP bool) error { var cleaned []string seen := make(map[string]bool) for _, u := range urls { validated, err := validate.RequireWebhookURL(u, allowHTTP) if err != nil { return err } if !seen[validated] { seen[validated] = true cleaned = append(cleaned, validated) } } sort.Strings(cleaned) st.Webhooks = cleaned st.Touch() return state.SaveState(st) } func RunBasicHealthChecks(st *state.BootstrapState) string { var out []string out = append(out, "Listening sockets:") result, _ := util.Run([]string{"ss", "-lntup"}, util.RunOptions{Check: false, Capture: true}) if result != nil { out = append(out, strings.TrimSpace(result.Output)) } result2, _ := util.Run([]string{"curl", "-fsS", fmt.Sprintf("http://127.0.0.1:%d/", st.Ports.LiveKitHTTPLocal)}, util.RunOptions{Check: false, Capture: true}) if result2 != nil && result2.ExitCode == 0 { out = append(out, "LiveKit local HTTP reachable.") } else { out = append(out, "LiveKit local HTTP not reachable.") } return strings.TrimSpace(strings.Join(out, "\n")) } func EnsureStateLoadedOrFail(path string) (*state.BootstrapState, error) { if path == "" { path = StatePathDefault() } st, err := state.LoadState(path) if err != nil { return nil, err } if st == nil { return nil, errors.NewPlatformErrorf("State file not found: %s", path) } return st, nil } func ConfigureFirewallFromState(st *state.BootstrapState) (string, error) { tool := firewall.DetectFirewallTool() msg := firewall.ConfigureFirewall(tool, st.Ports, st.Firewall.Enabled) st.Firewall.Tool = tool.Name st.Touch() if err := state.SaveState(st); err != nil { return msg, err } return msg, nil } func DetectPublicIPsOrFail() (string, string, string, error) { pub4 := netutil.DetectPublicIP("4") if pub4 == "" { return "", "", "", errors.NewPlatformError("Could not detect public IPv4.") } var pub6 string if netutil.HasGlobalIPv6() { pub6 = netutil.DetectPublicIP("6") } priv4 := netutil.PrimaryPrivateIPv4() return pub4, pub6, priv4, nil } func ReadLinesFile(path string) ([]string, error) { data, err := os.ReadFile(path) if err != nil { return nil, errors.NewPlatformErrorf("File not found: %s", path) } var lines []string for _, line := range strings.Split(string(data), "\n") { s := strings.TrimSpace(line) if s != "" { lines = append(lines, s) } } return lines, nil } // StopConflictingServices stops system-installed services that conflict with managed ones func StopConflictingServices() { sm := platform.DetectServiceManager() if !sm.IsSystemd() { return } conflicting := []string{ "valkey-server.service", "valkey.service", "redis-server.service", "redis.service", "coturn.service", } for _, svc := range conflicting { util.Run([]string{"systemctl", "stop", svc}, util.RunOptions{Check: false, Capture: true}) util.Run([]string{"systemctl", "disable", svc}, util.RunOptions{Check: false, Capture: true}) } managed := []string{ "livekit-kv.service", "livekit-coturn.service", "livekit.service", "caddy.service", } for _, svc := range managed { util.Run([]string{"systemctl", "reset-failed", svc}, util.RunOptions{Check: false, Capture: true}) } }