308 lines
7.8 KiB
Go
308 lines
7.8 KiB
Go
/*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
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})
|
|
}
|
|
}
|