Skip to content

Commit

Permalink
add: unifi backup frontend, fix backend symlink bug
Browse files Browse the repository at this point in the history
  • Loading branch information
paepckehh committed Dec 9, 2024
1 parent 6fd454c commit 64f7a2a
Show file tree
Hide file tree
Showing 10 changed files with 275 additions and 138 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ see opnborg-prometheus-grafana.nix
- OPN_APISECRET - OPNsense Backup User APISECRET [string, base64 encoded]
- OPN_TARGETS - list of OPNSense Target Server to Backup [string, hostnames, comma separated]
- OPN_TARGETS_* - alternative: custom groups for OPNSense Target server [example: OPN_TARGETS_INTRANET="opn-int-01.lan:8443,..."]
- OPN_TARGETS_IMGURL_* - alternative: custom image url for customs groups within WebUI [example: OPN_TARGETS_IMG_INTRANET="https://paepcke.de/img/intra.png"]
- OPN_TARGETS_IMGURL_* - alternative: custom image url for customs groups within WebUI [example: OPN_TARGETS_IMGURL_INTRANET="https://paepcke.de/img/intra.png"]
# Optional
- OPN_PATH - specify a target path (absolut or releative) to store backups [string: defaults to '.']
Expand Down Expand Up @@ -143,6 +143,9 @@ see opnborg-prometheus-grafana.nix
# Unifi
- OPN_UNIFI_WEBUI - Unifi Web Console target & port [example: http://localhost:8444]
- OPN_UNIFI_BACKUP_USER - Unifi Backup User Account
- OPN_UNIFI_BACKUP_SECRET - Unifi Backup User Account Password
- OPN_UNIFI_BACKUP_IMGURL - Unifi Backup Group Image URL [example: OPN_UNIFI_BACKUP_IMGURL="https://paepcke.de/img/unifi.png"]
# Wazuh
- OPN_WAZUH_WEBUI - Wazuh Web Console target & port [example: http://localhost:8446]
Expand Down
12 changes: 4 additions & 8 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ package opnborg

import (
"net/url"
"sync"
"sync/atomic"
)

// global exported consts
const SemVer = "v0.1.44"
const SemVer = "v0.1.45"

// global var
var (
Expand All @@ -21,13 +20,16 @@ var (
// OPNGroup Type
type OPNGroup struct {
Name string // group name
OPN bool // is OPNsense Appliance
Unifi bool // is Unifi Controller
Img bool // group image available
ImgURL string // group image url
Member []string // group member
}

// OPNCall
type OPNCall struct {
Enable bool // enable OPNsense Backup mode
Targets string // list of OPNSense Appliances, csv comma seperated
TGroups []OPNGroup // list of OPNSense Appliances Target Groups and Member
Key string // OPNSense Backup User API Key (required)
Expand Down Expand Up @@ -89,12 +91,6 @@ type OPNCall struct {
}
}

// global
var hive []string
var hiveMutex sync.Mutex
var updateOPN = make(chan bool, 1)
var updateUnifi = make(chan bool, 1)

// Start Application Server
func Start(config *OPNCall) error {
return srv(config)
Expand Down
3 changes: 1 addition & 2 deletions example-env-config-unifi.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ export OPN_SLEEP='60'
export OPN_DEBUG='1'
export OPN_SYNC_PKG='1'
export OPN_HTTPD_SERVER='127.0.0.1:6464'
export OPN_RSYSLOG_ENABLE='1'
export OPN_RSYSLOG_SERVER='192.168.122.1:5140'
export OPN_GRAFANA_WEBUI='http://localhost:9090'
export OPN_GRAFANA_DASHBOARD_FREEBSD='Kczn-jPZz/node-exporter-freebsd'
export OPN_GRAFANA_DASHBOARD_HAPROXY='rEqu1u5ue/haproxy-2-full'
Expand All @@ -26,3 +24,4 @@ export OPN_UNIFI_WEBUI='https://localhost:8443'
export OPN_UNIFI_VERSION='8.5.6'
export OPN_UNIFI_BACKUP_USER='admin'
export OPN_UNIFI_BACKUP_SECRET='start'
# export OPN_UNIFI_BACKUP_IMGURL='https://paepcke.de/res/uni.png'
16 changes: 11 additions & 5 deletions httpd-handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,18 @@ func getHive() string {
s.WriteString(_lf)
for _, srv := range grp.Member {
s.WriteString(" <tr><td>")
for _, line := range hive {
if strings.Contains(line, srv) {
s.WriteString(line)
break
if grp.OPN {
for _, line := range hive {
if strings.Contains(line, srv) {
s.WriteString(line)
break
}
}
}
s.WriteString(" </tr></td>")
if grp.Unifi {
s.WriteString(unifiStatus)
}
s.WriteString(" </td></tr>")
s.WriteString(_lf)
}
s.WriteString(" </table>")
Expand Down Expand Up @@ -183,6 +188,7 @@ func getNavi() string {
s.WriteString("><button><b>[ Unifi Dashboard ]</b></button></a> ")
}
if unifiWebUI != nil {
// if unifiWebUI != nil && !unifiEnable.Load() {
s.WriteString(" <a href=\"")
s.WriteString(unifiWebUI.String())
s.WriteString("/")
Expand Down
4 changes: 1 addition & 3 deletions httpd-ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,5 @@ const (

_degraded = `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><rect width="2.8" height="12" x="1" y="6" fill="#1c71d8"><animate attributeName="y" begin="svgSpinnersBarsScaleMiddle0.begin+0.4s" calcMode="spline" dur="0.6s" keySplines=".14,.73,.34,1;.65,.26,.82,.45" values="6;1;6"/><animate attributeName="height" begin="svgSpinnersBarsScaleMiddle0.begin+0.4s" calcMode="spline" dur="0.6s" keySplines=".14,.73,.34,1;.65,.26,.82,.45" values="12;22;12"/></rect><rect width="2.8" height="12" x="5.8" y="6" fill="#1c71d8"><animate attributeName="y" begin="svgSpinnersBarsScaleMiddle0.begin+0.2s" calcMode="spline" dur="0.6s" keySplines=".14,.73,.34,1;.65,.26,.82,.45" values="6;1;6"/><animate attributeName="height" begin="svgSpinnersBarsScaleMiddle0.begin+0.2s" calcMode="spline" dur="0.6s" keySplines=".14,.73,.34,1;.65,.26,.82,.45" values="12;22;12"/></rect><rect width="2.8" height="12" x="10.6" y="6" fill="#1c71d8"><animate id="svgSpinnersBarsScaleMiddle0" attributeName="y" begin="0;svgSpinnersBarsScaleMiddle1.end-0.1s" calcMode="spline" dur="0.6s" keySplines=".14,.73,.34,1;.65,.26,.82,.45" values="6;1;6"/><animate attributeName="height" begin="0;svgSpinnersBarsScaleMiddle1.end-0.1s" calcMode="spline" dur="0.6s" keySplines=".14,.73,.34,1;.65,.26,.82,.45" values="12;22;12"/></rect><rect width="2.8" height="12" x="15.4" y="6" fill="#1c71d8"><animate attributeName="y" begin="svgSpinnersBarsScaleMiddle0.begin+0.2s" calcMode="spline" dur="0.6s" keySplines=".14,.73,.34,1;.65,.26,.82,.45" values="6;1;6"/><animate attributeName="height" begin="svgSpinnersBarsScaleMiddle0.begin+0.2s" calcMode="spline" dur="0.6s" keySplines=".14,.73,.34,1;.65,.26,.82,.45" values="12;22;12"/></rect><rect width="2.8" height="12" x="20.2" y="6" fill="#1c71d8"><animate id="svgSpinnersBarsScaleMiddle1" attributeName="y" begin="svgSpinnersBarsScaleMiddle0.begin+0.4s" calcMode="spline" dur="0.6s" keySplines=".14,.73,.34,1;.65,.26,.82,.45" values="6;1;6"/><animate attributeName="height" begin="svgSpinnersBarsScaleMiddle0.begin+0.4s" calcMode="spline" dur="0.6s" keySplines=".14,.73,.34,1;.65,.26,.82,.45" values="12;22;12"/></rect><title>DEGRADED</title></svg>`

// _ui = `<svg fill="#0559C9" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Ubiquiti</title><path d="M23.1627 0h-1.4882v1.4882h1.4882zm-5.2072 10.4226V7.4409l.0007.001h2.9755v2.9762h2.9756v.9433c0 1.0906-.0927 2.3827-.306 3.3973-.1194.5672-.3004 1.1308-.5127 1.672-.2175.5537-.468 1.0841-.7408 1.5595a11.6795 11.6795 0 0 1-1.2456 1.7762l-.0253.0294-.0417.0488c-.1148.1347-.2283.2679-.3531.398a11.7612 11.7612 0 0 1-.4494.4492c-1.9046 1.8343-4.3861 2.98-6.9808 3.243-.3122.032-.939.0652-1.2519.0652-.3139-.001-.9397-.0331-1.252-.0651-2.5946-.263-5.0761-1.4097-6.9806-3.243a11.75 11.75 0 0 1-.4495-.4494c-.131-.1356-.249-.2748-.3683-.4154l-.0006-.0004-.0512-.0603a11.6576 11.6576 0 0 1-1.2456-1.7762c-.2727-.4763-.5233-1.0058-.7408-1.5595-.2123-.5414-.3933-1.1048-.5128-1.6718C.1854 13.743.0927 12.452.0927 11.3616V.1864h5.9518v10.2362s0 .7847.0099 1.0415l.0022.0599v.0004c.0127.332.0247.6575.0594.9812.098.919.3014 1.7913.7203 2.5288.1213.213.2443.42.3915.616.8953 1.1939 2.2577 2.0901 3.9573 2.3398.2022.0294.6108.0552.8149.0552.204 0 .6125-.0258.8149-.0552 1.6996-.2497 3.062-1.146 3.9573-2.3398.148-.196.2701-.403.3914-.616.419-.7375.6224-1.6095.7204-2.5288.0346-.3243.047-.6503.0594-.9831l.0022-.0584c.0099-.2568.0099-1.0415.0099-1.0415zm.7427-8.19h2.2326v2.2319h2.9764v2.9764h-2.9764V4.4654h-2.2326V2.2328Z"/></svg>`

// _gitLogLink = "<b>BorgAUDIT</b><br><b> [ Module:Changelog:Active ] </b><br><a href=\"./gitlog/\"><button type=\"button\"><b> [ Hive Audit-Changelog last 14 days ] </b></button></a><br>"
_unifi = `<button><svg fill="#0559C9" role="img" width="1em" height="1em" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Ubiquiti</title><path d="M23.1627 0h-1.4882v1.4882h1.4882zm-5.2072 10.4226V7.4409l.0007.001h2.9755v2.9762h2.9756v.9433c0 1.0906-.0927 2.3827-.306 3.3973-.1194.5672-.3004 1.1308-.5127 1.672-.2175.5537-.468 1.0841-.7408 1.5595a11.6795 11.6795 0 0 1-1.2456 1.7762l-.0253.0294-.0417.0488c-.1148.1347-.2283.2679-.3531.398a11.7612 11.7612 0 0 1-.4494.4492c-1.9046 1.8343-4.3861 2.98-6.9808 3.243-.3122.032-.939.0652-1.2519.0652-.3139-.001-.9397-.0331-1.252-.0651-2.5946-.263-5.0761-1.4097-6.9806-3.243a11.75 11.75 0 0 1-.4495-.4494c-.131-.1356-.249-.2748-.3683-.4154l-.0006-.0004-.0512-.0603a11.6576 11.6576 0 0 1-1.2456-1.7762c-.2727-.4763-.5233-1.0058-.7408-1.5595-.2123-.5414-.3933-1.1048-.5128-1.6718C.1854 13.743.0927 12.452.0927 11.3616V.1864h5.9518v10.2362s0 .7847.0099 1.0415l.0022.0599v.0004c.0127.332.0247.6575.0594.9812.098.919.3014 1.7913.7203 2.5288.1213.213.2443.42.3915.616.8953 1.1939 2.2577 2.0901 3.9573 2.3398.2022.0294.6108.0552.8149.0552.204 0 .6125-.0258.8149-.0552 1.6996-.2497 3.062-1.146 3.9573-2.3398.148-.196.2701-.403.3914-.616.419-.7375.6224-1.6095.7204-2.5288.0346-.3243.047-.6503.0594-.9831l.0022-.0584c.0099-.2568.0099-1.0415.0099-1.0415zm.7427-8.19h2.2326v2.2319h2.9764v2.9764h-2.9764V4.4654h-2.2326V2.2328Z"/></svg></button>`
)
78 changes: 63 additions & 15 deletions setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,22 @@ package opnborg
import (
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
)

// global
var (
hive []string
hiveMutex, unifiMutex sync.Mutex
updateOPN = make(chan bool, 1)
updateUnifi = make(chan bool, 1)
unifiStatus string
)

// Setup reads OPNBorgs configuration via env, sanitizes, sets sane defaults
Expand All @@ -16,13 +27,9 @@ func Setup() (*OPNCall, error) {
// var
var err error

// check if setup requirements are meet
if err = checkSetRequired(); err != nil {
return nil, err
}

// setup from env
config := &OPNCall{
Enable: checkSetRequiredOPN(),
Targets: os.Getenv("OPN_TARGETS"),
Key: os.Getenv("OPN_APIKEY"),
Secret: os.Getenv("OPN_APISECRET"),
Expand All @@ -31,6 +38,12 @@ func Setup() (*OPNCall, error) {
Email: os.Getenv("OPN_EMAIL"),
}

// check if we meet basic requirements
config.Unifi.Backup.Enable = checkSetRequiredUnifi()
if !config.Enable && !config.Unifi.Backup.Enable {
return nil, errors.New("Please enable either OPN or Unifi backup. Please set OPN_APIKEY & OPN_APISECRET or OPN_UNIFI_BACKUP_USER & SECRET")
}

// setup app name
if config.AppName == "" {
config.AppName = "[OPNBORG-API]"
Expand Down Expand Up @@ -199,16 +212,13 @@ func Setup() (*OPNCall, error) {

}

// checkRequired env input
func checkSetRequired() error {
// checkRequired OPN env
func checkSetRequiredOPN() bool {

if !isEnv("OPN_APIKEY") {
return fmt.Errorf("set env variable 'OPN_APIKEY' to your opnsense api key")
if !isEnv("OPN_APIKEY") || !isEnv("OPN_APISECRET") {
return false
}

if !isEnv("OPN_APISECRET") {
return fmt.Errorf("set env variable 'OPN_APISECRET' to your opnsense api key secret")
}
if !isEnv("OPN_TARGETS") {
member := ""
env := os.Environ()
Expand All @@ -229,13 +239,17 @@ func checkSetRequired() error {
tg = append(tg, OPNGroup{
Name: grp[0][12:],
Img: true,
OPN: true,
Unifi: false,
ImgURL: os.Getenv("OPN_TARGETS_IMGURL_" + grp[0][12:]),
Member: strings.Split(grp[1], ","),
})
} else {
tg = append(tg, OPNGroup{
Name: grp[0][12:],
Img: false,
OPN: true,
Unifi: false,
Member: strings.Split(grp[1], ","),
})
}
Expand All @@ -244,11 +258,45 @@ func checkSetRequired() error {
}
if len(member) > 0 {
os.Setenv("OPN_TARGETS", member)
return nil
return true
}
}
return fmt.Errorf("add at least one target server to env var 'OPN_TARGETS' or 'OPN_TARGETS_* '(multi valued, comma seperated)")
return false
}
tg = append(tg, OPNGroup{Name: "", Member: strings.Split(os.Getenv("OPN_TARGETS"), ",")})
return nil
return true
}

// checkRequired Unifi env
func checkSetRequiredUnifi() bool {

unifiURL, err := url.Parse(os.Getenv("OPN_UNIFI_WEBUI"))
if err != nil {
return false // detailed checks & err analysis later
}

if !isEnv("OPN_UNIFI_BACKUP_USER") || !isEnv("OPN_UNIFI_BACKUP_SECRET") {
return false
}

// add unifi group
if isEnv("OPN_UNIFI_BACKUP_IMGURL") {
tg = append(tg, OPNGroup{
Name: "UNIFI CONTROLLER",
Img: true,
OPN: false,
Unifi: true,
ImgURL: os.Getenv("OPN_UNIFI_BACKUP_IMGURL"),
Member: strings.Split(unifiURL.Hostname(), ","),
})
} else {
tg = append(tg, OPNGroup{
Name: "UNIFI CONTROLLER",
Img: false,
OPN: false,
Unifi: true,
Member: strings.Split(unifiURL.Hostname(), ","),
})
}
return true
}
92 changes: 50 additions & 42 deletions srv.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,30 @@ import (
func srv(config *OPNCall) error {
// init
var err error
var servers []string

// spin up Log/Display Engine
display.Add(1)

// spin up internal log / display engine
go startLog(config)

// startup app version & state, sleep panic gate
suffix := "[CLI-ONE-TIME-PASS-MODE]"
if config.Daemon {
suffix = "[DAEMON-MODE][SLEEP:" + sleep + " SECONDS]"
}
displayChan <- []byte("[STARTING][" + _app + "][" + SemVer + "]" + suffix)

// arm background timer
go func() {
time.Sleep(time.Duration(config.Sleep) * time.Second)
updateOPN <- true
if unifiEnable.Load() {
updateUnifi <- true
}
}()

// spin up internal webserver
state := "[DISABLED]"
if config.Httpd.Enable {
Expand All @@ -35,63 +52,54 @@ func srv(config *OPNCall) error {
// spin up unifi backup server
state = "[DISABLED]"
if config.Unifi.Backup.Enable {
unifiStatus = _na + " <b>Member: </b> " + config.Unifi.WebUI.String() + " <b>Version: </b>n/a <b>Last Seen: </b>n/a<br>"
go unifiBackupServer(config)
state = "[ENABLED]"
}
displayChan <- []byte("[SERVICE][UNIFI-BACKUP]" + state)

// setup hive
servers := strings.Split(config.Targets, ",")
for _, server := range servers {
status := _na + " <b>Member: </b> " + server + " <b>Version: </b>n/a <b>Last Seen: </b>n/a<br>"
hive = append(hive, status)
}

// startup app version & state, sleep panic gate
suffix := "[CLI-ONE-TIME-PASS-MODE]"
if config.Daemon {
suffix = "[DAEMON-MODE][SLEEP:" + sleep + " SECONDS]"
}
displayChan <- []byte("[STARTING][" + _app + "][" + SemVer + "]" + suffix)

// spin up timer
go func() {
time.Sleep(time.Duration(config.Sleep) * time.Second)
updateOPN <- true
if unifiEnable.Load() {
updateUnifi <- true
// is opnsense hive is enabled?
if config.Enable {
// setup hive
servers := strings.Split(config.Targets, ",")
for _, server := range servers {
status := _na + " <b>Member: </b> " + server + " <b>Version: </b>n/a <b>Last Seen: </b>n/a<br>"
hive = append(hive, status)
}
}()
}

// main loop
for {

// fetch target configuration from master server
if config.Sync.Enable {
config.Sync.validConf = true
config, err = readMasterConf(config)
if err != nil {
config.Sync.validConf = false
displayChan <- []byte("[ERROR][UNABLE-TO-READ-MASTER-CONFIG]" + err.Error())
}
}

// reset global (atomic) git worktree state tracker
if config.Git {
config.dirty.Store(false)
}

// spinup individual worker for every server
if config.Debug {
displayChan <- []byte("[STARTING][BACKUP]")
}
for id, server := range servers {
wg.Add(1)
go actionOPN(server, config, id, &wg)
}
// is opnsense hive is enabled
if config.Enable {

// wait till all worker done
wg.Wait()
// fetch target configuration from master server
if config.Sync.Enable {
config.Sync.validConf = true
config, err = readMasterConf(config)
if err != nil {
config.Sync.validConf = false
displayChan <- []byte("[ERROR][UNABLE-TO-READ-MASTER-CONFIG]" + err.Error())
}
}

// spinup individual worker for every server
if config.Debug {
displayChan <- []byte("[STARTING][BACKUP]")
}
for id, server := range servers {
wg.Add(1)
go actionOPN(server, config, id, &wg)
}

// wait till all worker done
wg.Wait()
}

// check files into local git repo
if config.dirty.Load() {
Expand Down
Loading

0 comments on commit 64f7a2a

Please sign in to comment.