security: harden agent installers and auto-update mechanism
Install script (scripts/install.sh): - Add multi-platform support: Unraid, OpenRC/Alpine, Synology DSM 6/7 - Add input validation for URL, token format, and interval - Add binary magic verification (ELF/Mach-O/PE) - Add cleanup trap for temp files - Wrap script in main() for partial download protection - Fix shellcheck compliance issues - Add curl timeouts Agent auto-update (agentupdate, dockeragent): - Enforce TLS 1.2 minimum version - Make SHA256 checksum verification mandatory - Add 100MB binary size limit - Add binary magic verification before replacement - Add Unraid persistent binary update after self-update - Add 5-minute download timeout Frontend: - Update Linux install description to note auto-detection of init systems
This commit is contained in:
parent
2b19127c0c
commit
09ec0c3f01
4 changed files with 510 additions and 65 deletions
|
|
@ -36,14 +36,14 @@ const commandsByPlatform: Record<
|
||||||
linux: {
|
linux: {
|
||||||
title: 'Install on Linux',
|
title: 'Install on Linux',
|
||||||
description:
|
description:
|
||||||
'The unified installer downloads the agent binary and configures it as a systemd service.',
|
'The unified installer downloads the agent binary and configures the appropriate service for your system.',
|
||||||
snippets: [
|
snippets: [
|
||||||
{
|
{
|
||||||
label: 'Install with systemd',
|
label: 'Install',
|
||||||
command: `curl -fsSL ${pulseUrl()}/install.sh | sudo bash -s -- --url ${pulseUrl()} --token ${TOKEN_PLACEHOLDER} --interval 30s`,
|
command: `curl -fsSL ${pulseUrl()}/install.sh | sudo bash -s -- --url ${pulseUrl()} --token ${TOKEN_PLACEHOLDER} --interval 30s`,
|
||||||
note: (
|
note: (
|
||||||
<span>
|
<span>
|
||||||
Automatically installs to <code>/usr/local/bin/pulse-agent</code> and creates <code>/etc/systemd/system/pulse-agent.service</code>.
|
Automatically detects your init system (systemd, OpenRC, Unraid, Synology) and configures the appropriate service. Works on Debian, Ubuntu, Fedora, Alpine, Gentoo, Unraid, Synology, and more.
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,14 @@ import (
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// maxBinarySize is the maximum allowed size for downloaded binaries (100 MB)
|
||||||
|
maxBinarySize = 100 * 1024 * 1024
|
||||||
|
|
||||||
|
// downloadTimeout is the maximum time allowed for downloading a binary
|
||||||
|
downloadTimeout = 5 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
// Config holds the configuration for the updater.
|
// Config holds the configuration for the updater.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// PulseURL is the base URL of the Pulse server (e.g., "https://pulse.example.com:7655")
|
// PulseURL is the base URL of the Pulse server (e.g., "https://pulse.example.com:7655")
|
||||||
|
|
@ -70,7 +78,8 @@ func New(cfg Config) *Updater {
|
||||||
|
|
||||||
transport := &http.Transport{
|
transport := &http.Transport{
|
||||||
TLSClientConfig: &tls.Config{
|
TLSClientConfig: &tls.Config{
|
||||||
InsecureSkipVerify: cfg.InsecureSkipVerify,
|
MinVersion: tls.VersionTLS12,
|
||||||
|
InsecureSkipVerify: cfg.InsecureSkipVerify, //nolint:gosec
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,7 +87,7 @@ func New(cfg Config) *Updater {
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
client: &http.Client{
|
client: &http.Client{
|
||||||
Transport: transport,
|
Transport: transport,
|
||||||
Timeout: 30 * time.Second,
|
Timeout: downloadTimeout,
|
||||||
},
|
},
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
|
|
@ -199,6 +208,64 @@ func (u *Updater) getServerVersion(ctx context.Context) (string, error) {
|
||||||
return versionResp.Version, nil
|
return versionResp.Version, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isUnraid checks if we're running on Unraid by looking for /etc/unraid-version
|
||||||
|
func isUnraid() bool {
|
||||||
|
_, err := os.Stat("/etc/unraid-version")
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyBinaryMagic checks that the file is a valid executable for the current platform
|
||||||
|
func verifyBinaryMagic(path string) error {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
magic := make([]byte, 4)
|
||||||
|
if _, err := io.ReadFull(f, magic); err != nil {
|
||||||
|
return fmt.Errorf("failed to read magic bytes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "linux":
|
||||||
|
// ELF magic: 0x7f 'E' 'L' 'F'
|
||||||
|
if magic[0] == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F' {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("not a valid ELF binary")
|
||||||
|
|
||||||
|
case "darwin":
|
||||||
|
// Mach-O magic bytes (little-endian):
|
||||||
|
// - 0xfeedface (32-bit)
|
||||||
|
// - 0xfeedfacf (64-bit)
|
||||||
|
// - 0xcafebabe (universal/fat binary)
|
||||||
|
// Note: bytes are reversed due to little-endian
|
||||||
|
if (magic[0] == 0xcf && magic[1] == 0xfa && magic[2] == 0xed && magic[3] == 0xfe) || // 64-bit
|
||||||
|
(magic[0] == 0xce && magic[1] == 0xfa && magic[2] == 0xed && magic[3] == 0xfe) || // 32-bit
|
||||||
|
(magic[0] == 0xca && magic[1] == 0xfe && magic[2] == 0xba && magic[3] == 0xbe) { // universal
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("not a valid Mach-O binary")
|
||||||
|
|
||||||
|
case "windows":
|
||||||
|
// PE magic: 'M' 'Z'
|
||||||
|
if magic[0] == 'M' && magic[1] == 'Z' {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("not a valid PE binary")
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Unknown platform, skip verification
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unraidPersistentPath returns the path where the binary should be persisted on Unraid
|
||||||
|
func unraidPersistentPath(agentName string) string {
|
||||||
|
return fmt.Sprintf("/boot/config/plugins/%s/%s", agentName, agentName)
|
||||||
|
}
|
||||||
|
|
||||||
// performUpdate downloads and installs the new agent binary.
|
// performUpdate downloads and installs the new agent binary.
|
||||||
func (u *Updater) performUpdate(ctx context.Context) error {
|
func (u *Updater) performUpdate(ctx context.Context) error {
|
||||||
execPath, err := os.Executable()
|
execPath, err := os.Executable()
|
||||||
|
|
@ -268,29 +335,40 @@ func (u *Updater) performUpdate(ctx context.Context) error {
|
||||||
tmpPath := tmpFile.Name()
|
tmpPath := tmpFile.Name()
|
||||||
defer os.Remove(tmpPath) // Clean up on failure
|
defer os.Remove(tmpPath) // Clean up on failure
|
||||||
|
|
||||||
// Write downloaded binary with checksum calculation
|
// Write downloaded binary with checksum calculation and size limit
|
||||||
hasher := sha256.New()
|
hasher := sha256.New()
|
||||||
if _, err := io.Copy(tmpFile, io.TeeReader(resp.Body, hasher)); err != nil {
|
limitedReader := io.LimitReader(resp.Body, maxBinarySize+1) // +1 to detect overflow
|
||||||
|
written, err := io.Copy(tmpFile, io.TeeReader(limitedReader, hasher))
|
||||||
|
if err != nil {
|
||||||
tmpFile.Close()
|
tmpFile.Close()
|
||||||
return fmt.Errorf("failed to write binary: %w", err)
|
return fmt.Errorf("failed to write binary: %w", err)
|
||||||
}
|
}
|
||||||
|
if written > maxBinarySize {
|
||||||
|
tmpFile.Close()
|
||||||
|
return fmt.Errorf("downloaded binary exceeds maximum size (%d bytes)", maxBinarySize)
|
||||||
|
}
|
||||||
if err := tmpFile.Close(); err != nil {
|
if err := tmpFile.Close(); err != nil {
|
||||||
return fmt.Errorf("failed to close temp file: %w", err)
|
return fmt.Errorf("failed to close temp file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify checksum
|
// Verify it's a valid executable (basic sanity check)
|
||||||
downloadChecksum := hex.EncodeToString(hasher.Sum(nil))
|
if err := verifyBinaryMagic(tmpPath); err != nil {
|
||||||
if checksumHeader != "" {
|
return fmt.Errorf("downloaded file is not a valid executable: %w", err)
|
||||||
expected := strings.ToLower(strings.TrimSpace(checksumHeader))
|
|
||||||
actual := strings.ToLower(downloadChecksum)
|
|
||||||
if expected != actual {
|
|
||||||
return fmt.Errorf("checksum mismatch: expected %s, got %s", expected, actual)
|
|
||||||
}
|
|
||||||
u.logger.Debug().Str("checksum", downloadChecksum).Msg("Checksum verified")
|
|
||||||
} else {
|
|
||||||
u.logger.Warn().Msg("No checksum header; skipping verification")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify checksum (mandatory for security)
|
||||||
|
downloadChecksum := hex.EncodeToString(hasher.Sum(nil))
|
||||||
|
if checksumHeader == "" {
|
||||||
|
return fmt.Errorf("server did not provide checksum header (X-Checksum-Sha256); refusing update for security")
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := strings.ToLower(strings.TrimSpace(checksumHeader))
|
||||||
|
actual := strings.ToLower(downloadChecksum)
|
||||||
|
if expected != actual {
|
||||||
|
return fmt.Errorf("checksum mismatch: expected %s, got %s", expected, actual)
|
||||||
|
}
|
||||||
|
u.logger.Debug().Str("checksum", downloadChecksum).Msg("Checksum verified")
|
||||||
|
|
||||||
// Make executable
|
// Make executable
|
||||||
if err := os.Chmod(tmpPath, 0755); err != nil {
|
if err := os.Chmod(tmpPath, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to chmod: %w", err)
|
return fmt.Errorf("failed to chmod: %w", err)
|
||||||
|
|
@ -311,6 +389,33 @@ func (u *Updater) performUpdate(ctx context.Context) error {
|
||||||
// Remove backup on success
|
// Remove backup on success
|
||||||
os.Remove(backupPath)
|
os.Remove(backupPath)
|
||||||
|
|
||||||
|
// On Unraid, also update the persistent copy on the flash drive
|
||||||
|
// This ensures the update survives reboots
|
||||||
|
if isUnraid() {
|
||||||
|
persistPath := unraidPersistentPath(u.cfg.AgentName)
|
||||||
|
if _, err := os.Stat(persistPath); err == nil {
|
||||||
|
// Persistent path exists, update it
|
||||||
|
u.logger.Debug().Str("path", persistPath).Msg("Updating Unraid persistent binary")
|
||||||
|
|
||||||
|
// Read the newly installed binary
|
||||||
|
newBinary, err := os.ReadFile(execPath)
|
||||||
|
if err != nil {
|
||||||
|
u.logger.Warn().Err(err).Msg("Failed to read new binary for Unraid persistence")
|
||||||
|
} else {
|
||||||
|
// Write to persistent storage (atomic via temp file)
|
||||||
|
tmpPersist := persistPath + ".tmp"
|
||||||
|
if err := os.WriteFile(tmpPersist, newBinary, 0644); err != nil {
|
||||||
|
u.logger.Warn().Err(err).Msg("Failed to write Unraid persistent binary")
|
||||||
|
} else if err := os.Rename(tmpPersist, persistPath); err != nil {
|
||||||
|
u.logger.Warn().Err(err).Msg("Failed to rename Unraid persistent binary")
|
||||||
|
os.Remove(tmpPersist)
|
||||||
|
} else {
|
||||||
|
u.logger.Info().Str("path", persistPath).Msg("Updated Unraid persistent binary")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Restart with same arguments
|
// Restart with same arguments
|
||||||
args := os.Args
|
args := os.Args
|
||||||
env := os.Environ()
|
env := os.Environ()
|
||||||
|
|
|
||||||
|
|
@ -1272,14 +1272,18 @@ func (a *Agent) httpClientFor(target TargetConfig) *http.Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHTTPClient(insecure bool) *http.Client {
|
func newHTTPClient(insecure bool) *http.Client {
|
||||||
transport := &http.Transport{}
|
tlsConfig := &tls.Config{
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
}
|
||||||
if insecure {
|
if insecure {
|
||||||
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec
|
tlsConfig.InsecureSkipVerify = true //nolint:gosec
|
||||||
}
|
}
|
||||||
|
|
||||||
return &http.Client{
|
return &http.Client{
|
||||||
Timeout: 15 * time.Second,
|
Timeout: 15 * time.Second,
|
||||||
Transport: transport,
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: tlsConfig,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1631,6 +1635,32 @@ func (a *Agent) checkForUpdates(ctx context.Context) {
|
||||||
a.logger.Info().Msg("Agent updated successfully, restarting...")
|
a.logger.Info().Msg("Agent updated successfully, restarting...")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isUnraid checks if we're running on Unraid by looking for /etc/unraid-version
|
||||||
|
func isUnraid() bool {
|
||||||
|
_, err := os.Stat("/etc/unraid-version")
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyELFMagic checks that the file is a valid ELF binary
|
||||||
|
func verifyELFMagic(path string) error {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
magic := make([]byte, 4)
|
||||||
|
if _, err := io.ReadFull(f, magic); err != nil {
|
||||||
|
return fmt.Errorf("failed to read magic bytes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ELF magic: 0x7f 'E' 'L' 'F'
|
||||||
|
if magic[0] == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F' {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New("not a valid ELF binary")
|
||||||
|
}
|
||||||
|
|
||||||
func determineSelfUpdateArch() string {
|
func determineSelfUpdateArch() string {
|
||||||
switch runtime.GOARCH {
|
switch runtime.GOARCH {
|
||||||
case "amd64":
|
case "amd64":
|
||||||
|
|
@ -1746,28 +1776,41 @@ func (a *Agent) selfUpdate(ctx context.Context) error {
|
||||||
tmpPath := tmpFile.Name()
|
tmpPath := tmpFile.Name()
|
||||||
defer os.Remove(tmpPath) // Clean up if something goes wrong
|
defer os.Remove(tmpPath) // Clean up if something goes wrong
|
||||||
|
|
||||||
// Write downloaded binary to temp file
|
// Write downloaded binary to temp file with size limit (100 MB max)
|
||||||
|
const maxBinarySize = 100 * 1024 * 1024
|
||||||
hasher := sha256.New()
|
hasher := sha256.New()
|
||||||
if _, err := io.Copy(tmpFile, io.TeeReader(resp.Body, hasher)); err != nil {
|
limitedReader := io.LimitReader(resp.Body, maxBinarySize+1)
|
||||||
|
written, err := io.Copy(tmpFile, io.TeeReader(limitedReader, hasher))
|
||||||
|
if err != nil {
|
||||||
tmpFile.Close()
|
tmpFile.Close()
|
||||||
return fmt.Errorf("failed to write downloaded binary: %w", err)
|
return fmt.Errorf("failed to write downloaded binary: %w", err)
|
||||||
}
|
}
|
||||||
|
if written > maxBinarySize {
|
||||||
|
tmpFile.Close()
|
||||||
|
return fmt.Errorf("downloaded binary exceeds maximum size (%d bytes)", maxBinarySize)
|
||||||
|
}
|
||||||
if err := tmpFile.Close(); err != nil {
|
if err := tmpFile.Close(); err != nil {
|
||||||
return fmt.Errorf("failed to close temp file: %w", err)
|
return fmt.Errorf("failed to close temp file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadChecksum := hex.EncodeToString(hasher.Sum(nil))
|
// Verify it's a valid ELF binary (basic sanity check for Linux)
|
||||||
if checksumHeader != "" {
|
if err := verifyELFMagic(tmpPath); err != nil {
|
||||||
expected := strings.ToLower(strings.TrimSpace(checksumHeader))
|
return fmt.Errorf("downloaded file is not a valid executable: %w", err)
|
||||||
actual := strings.ToLower(downloadChecksum)
|
|
||||||
if expected != actual {
|
|
||||||
return fmt.Errorf("checksum verification failed: expected %s, got %s", expected, actual)
|
|
||||||
}
|
|
||||||
a.logger.Debug().Str("checksum", downloadChecksum).Msg("Self-update: checksum verified")
|
|
||||||
} else {
|
|
||||||
a.logger.Warn().Msg("Self-update: checksum header missing; skipping verification")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify checksum (mandatory for security)
|
||||||
|
downloadChecksum := hex.EncodeToString(hasher.Sum(nil))
|
||||||
|
if checksumHeader == "" {
|
||||||
|
return fmt.Errorf("server did not provide checksum header (X-Checksum-Sha256); refusing update for security")
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := strings.ToLower(strings.TrimSpace(checksumHeader))
|
||||||
|
actual := strings.ToLower(downloadChecksum)
|
||||||
|
if expected != actual {
|
||||||
|
return fmt.Errorf("checksum verification failed: expected %s, got %s", expected, actual)
|
||||||
|
}
|
||||||
|
a.logger.Debug().Str("checksum", downloadChecksum).Msg("Self-update: checksum verified")
|
||||||
|
|
||||||
// Make temp file executable
|
// Make temp file executable
|
||||||
if err := os.Chmod(tmpPath, 0755); err != nil {
|
if err := os.Chmod(tmpPath, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to make temp file executable: %w", err)
|
return fmt.Errorf("failed to make temp file executable: %w", err)
|
||||||
|
|
@ -1789,6 +1832,25 @@ func (a *Agent) selfUpdate(ctx context.Context) error {
|
||||||
// Remove backup on success
|
// Remove backup on success
|
||||||
os.Remove(backupPath)
|
os.Remove(backupPath)
|
||||||
|
|
||||||
|
// On Unraid, also update the persistent copy on the flash drive
|
||||||
|
if isUnraid() {
|
||||||
|
persistPath := "/boot/config/plugins/pulse-docker-agent/pulse-docker-agent"
|
||||||
|
if _, err := os.Stat(persistPath); err == nil {
|
||||||
|
a.logger.Debug().Str("path", persistPath).Msg("Updating Unraid persistent binary")
|
||||||
|
if newBinary, err := os.ReadFile(execPath); err == nil {
|
||||||
|
tmpPersist := persistPath + ".tmp"
|
||||||
|
if err := os.WriteFile(tmpPersist, newBinary, 0644); err != nil {
|
||||||
|
a.logger.Warn().Err(err).Msg("Failed to write Unraid persistent binary")
|
||||||
|
} else if err := os.Rename(tmpPersist, persistPath); err != nil {
|
||||||
|
a.logger.Warn().Err(err).Msg("Failed to rename Unraid persistent binary")
|
||||||
|
os.Remove(tmpPersist)
|
||||||
|
} else {
|
||||||
|
a.logger.Info().Str("path", persistPath).Msg("Updated Unraid persistent binary")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Restart agent with same arguments
|
// Restart agent with same arguments
|
||||||
args := os.Args
|
args := os.Args
|
||||||
env := os.Environ()
|
env := os.Environ()
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# Pulse Unified Agent Installer
|
# Pulse Unified Agent Installer
|
||||||
# Supports: Linux (systemd), macOS (launchd), Synology (upstart)
|
# Supports: Linux (systemd, OpenRC), macOS (launchd), Synology DSM (6.x/7+), Unraid
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# curl -fsSL http://pulse/install.sh | bash -s -- --url http://pulse --token <token> [options]
|
# curl -fsSL http://pulse/install.sh | bash -s -- --url http://pulse --token <token> [options]
|
||||||
|
|
@ -14,6 +14,20 @@
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Wrap entire script in a function to protect against partial download
|
||||||
|
# See: https://www.kicksecure.com/wiki/Dev/curl_bash_pipe
|
||||||
|
main() {
|
||||||
|
|
||||||
|
# --- Cleanup trap ---
|
||||||
|
TMP_FILES=()
|
||||||
|
# shellcheck disable=SC2317 # Invoked by trap, not directly
|
||||||
|
cleanup() {
|
||||||
|
for f in "${TMP_FILES[@]}"; do
|
||||||
|
rm -f "$f" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
# --- Check Root ---
|
# --- Check Root ---
|
||||||
if [[ $EUID -ne 0 ]]; then
|
if [[ $EUID -ne 0 ]]; then
|
||||||
echo "This script must be run as root. Please use sudo."
|
echo "This script must be run as root. Please use sudo."
|
||||||
|
|
@ -42,13 +56,31 @@ log_error() { printf "[ERROR] %s\n" "$1"; }
|
||||||
fail() {
|
fail() {
|
||||||
log_error "$1"
|
log_error "$1"
|
||||||
if [[ -t 0 ]]; then
|
if [[ -t 0 ]]; then
|
||||||
read -p "Press Enter to exit..."
|
read -r -p "Press Enter to exit..."
|
||||||
elif [[ -e /dev/tty ]]; then
|
elif [[ -e /dev/tty ]]; then
|
||||||
read -p "Press Enter to exit..." < /dev/tty
|
read -r -p "Press Enter to exit..." < /dev/tty
|
||||||
fi
|
fi
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Build exec args string for use in service files
|
||||||
|
# Returns via EXEC_ARGS variable
|
||||||
|
build_exec_args() {
|
||||||
|
EXEC_ARGS="--url ${PULSE_URL} --token ${PULSE_TOKEN} --interval ${INTERVAL}"
|
||||||
|
if [[ "$ENABLE_HOST" == "true" ]]; then EXEC_ARGS="$EXEC_ARGS --enable-host"; fi
|
||||||
|
if [[ "$ENABLE_DOCKER" == "true" ]]; then EXEC_ARGS="$EXEC_ARGS --enable-docker"; fi
|
||||||
|
if [[ "$INSECURE" == "true" ]]; then EXEC_ARGS="$EXEC_ARGS --insecure"; fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build exec args as array for direct execution (proper quoting)
|
||||||
|
# Returns via EXEC_ARGS_ARRAY variable
|
||||||
|
build_exec_args_array() {
|
||||||
|
EXEC_ARGS_ARRAY=(--url "$PULSE_URL" --token "$PULSE_TOKEN" --interval "$INTERVAL")
|
||||||
|
if [[ "$ENABLE_HOST" == "true" ]]; then EXEC_ARGS_ARRAY+=(--enable-host); fi
|
||||||
|
if [[ "$ENABLE_DOCKER" == "true" ]]; then EXEC_ARGS_ARRAY+=(--enable-docker); fi
|
||||||
|
if [[ "$INSECURE" == "true" ]]; then EXEC_ARGS_ARRAY+=(--insecure); fi
|
||||||
|
}
|
||||||
|
|
||||||
# --- Parse Arguments ---
|
# --- Parse Arguments ---
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
|
|
@ -84,10 +116,44 @@ if [[ "$UNINSTALL" == "true" ]]; then
|
||||||
rm -f "$PLIST"
|
rm -f "$PLIST"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Upstart (Synology)
|
# Synology DSM (handles both DSM 7+ systemd and DSM 6.x upstart)
|
||||||
if [[ -f "/etc/init/${AGENT_NAME}.conf" ]]; then
|
if [[ -d /usr/syno ]]; then
|
||||||
initctl stop "${AGENT_NAME}" 2>/dev/null || true
|
# DSM 7+ uses systemd
|
||||||
rm -f "/etc/init/${AGENT_NAME}.conf"
|
if [[ -f "/etc/systemd/system/${AGENT_NAME}.service" ]]; then
|
||||||
|
systemctl stop "${AGENT_NAME}" 2>/dev/null || true
|
||||||
|
systemctl disable "${AGENT_NAME}" 2>/dev/null || true
|
||||||
|
rm -f "/etc/systemd/system/${AGENT_NAME}.service"
|
||||||
|
systemctl daemon-reload 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
# DSM 6.x uses upstart
|
||||||
|
if [[ -f "/etc/init/${AGENT_NAME}.conf" ]]; then
|
||||||
|
initctl stop "${AGENT_NAME}" 2>/dev/null || true
|
||||||
|
rm -f "/etc/init/${AGENT_NAME}.conf"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Unraid
|
||||||
|
if [[ -f /etc/unraid-version ]] || [[ -d /boot/config/plugins/pulse-agent ]]; then
|
||||||
|
log_info "Removing Unraid installation..."
|
||||||
|
# Stop running agent
|
||||||
|
pkill -f "pulse-agent" 2>/dev/null || true
|
||||||
|
# Remove from /boot/config/go
|
||||||
|
GO_SCRIPT="/boot/config/go"
|
||||||
|
if [[ -f "$GO_SCRIPT" ]]; then
|
||||||
|
sed -i '/# Pulse Agent/,/^$/d' "$GO_SCRIPT" 2>/dev/null || true
|
||||||
|
sed -i '/pulse-agent/d' "$GO_SCRIPT" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
# Remove installation directory
|
||||||
|
rm -rf /boot/config/plugins/pulse-agent
|
||||||
|
# Remove symlink
|
||||||
|
rm -f "${INSTALL_DIR}/${BINARY_NAME}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# OpenRC (Alpine, Gentoo, Artix, etc.)
|
||||||
|
if command -v rc-service >/dev/null 2>&1; then
|
||||||
|
rc-service "${AGENT_NAME}" stop 2>/dev/null || true
|
||||||
|
rc-update del "${AGENT_NAME}" default 2>/dev/null || true
|
||||||
|
rm -f "/etc/init.d/${AGENT_NAME}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rm -f "${INSTALL_DIR}/${BINARY_NAME}"
|
rm -f "${INSTALL_DIR}/${BINARY_NAME}"
|
||||||
|
|
@ -100,6 +166,21 @@ if [[ -z "$PULSE_URL" || -z "$PULSE_TOKEN" ]]; then
|
||||||
fail "Missing required arguments: --url and --token"
|
fail "Missing required arguments: --url and --token"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Validate URL format (basic check)
|
||||||
|
if [[ ! "$PULSE_URL" =~ ^https?:// ]]; then
|
||||||
|
fail "Invalid URL format. Must start with http:// or https://"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate token format (should be hex string, typically 64 chars)
|
||||||
|
if [[ ! "$PULSE_TOKEN" =~ ^[a-fA-F0-9]+$ ]]; then
|
||||||
|
fail "Invalid token format. Token should be a hexadecimal string."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate interval format
|
||||||
|
if [[ ! "$INTERVAL" =~ ^[0-9]+[smh]?$ ]]; then
|
||||||
|
fail "Invalid interval format. Use format like '30s', '5m', or '1h'."
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Download ---
|
# --- Download ---
|
||||||
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||||
ARCH=$(uname -m)
|
ARCH=$(uname -m)
|
||||||
|
|
@ -119,21 +200,43 @@ ARCH_PARAM="${OS}-${ARCH}"
|
||||||
DOWNLOAD_URL="${PULSE_URL}/download/${BINARY_NAME}?arch=${ARCH_PARAM}"
|
DOWNLOAD_URL="${PULSE_URL}/download/${BINARY_NAME}?arch=${ARCH_PARAM}"
|
||||||
log_info "Downloading agent from ${DOWNLOAD_URL}..."
|
log_info "Downloading agent from ${DOWNLOAD_URL}..."
|
||||||
|
|
||||||
# Create temp file
|
# Create temp file and register for cleanup
|
||||||
TMP_BIN=$(mktemp)
|
TMP_BIN=$(mktemp)
|
||||||
chmod +x "$TMP_BIN"
|
TMP_FILES+=("$TMP_BIN")
|
||||||
|
|
||||||
CURL_ARGS="-fsSL"
|
# Build curl arguments as array for proper quoting
|
||||||
if [[ "$INSECURE" == "true" ]]; then CURL_ARGS="-k $CURL_ARGS"; fi
|
CURL_ARGS=(-fsSL --connect-timeout 30 --max-time 300)
|
||||||
|
if [[ "$INSECURE" == "true" ]]; then CURL_ARGS+=(-k); fi
|
||||||
|
|
||||||
if ! curl $CURL_ARGS -o "$TMP_BIN" "$DOWNLOAD_URL"; then
|
if ! curl "${CURL_ARGS[@]}" -o "$TMP_BIN" "$DOWNLOAD_URL"; then
|
||||||
fail "Download failed. Check URL and connectivity."
|
fail "Download failed. Check URL and connectivity."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Verify downloaded binary
|
||||||
|
if [[ ! -s "$TMP_BIN" ]]; then
|
||||||
|
fail "Downloaded file is empty."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if it's a valid executable (ELF for Linux, Mach-O for macOS)
|
||||||
|
if [[ "$OS" == "linux" ]]; then
|
||||||
|
if ! head -c 4 "$TMP_BIN" | grep -q "ELF"; then
|
||||||
|
fail "Downloaded file is not a valid Linux executable."
|
||||||
|
fi
|
||||||
|
elif [[ "$OS" == "darwin" ]]; then
|
||||||
|
# Mach-O magic: feedface (32-bit) or feedfacf (64-bit) or cafebabe (universal)
|
||||||
|
MAGIC=$(xxd -p -l 4 "$TMP_BIN" 2>/dev/null || head -c 4 "$TMP_BIN" | od -A n -t x1 | tr -d ' ')
|
||||||
|
if [[ ! "$MAGIC" =~ ^(cffaedfe|cefaedfe|cafebabe|feedface|feedfacf) ]]; then
|
||||||
|
fail "Downloaded file is not a valid macOS executable."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod +x "$TMP_BIN"
|
||||||
|
|
||||||
# Install Binary
|
# Install Binary
|
||||||
log_info "Installing binary to ${INSTALL_DIR}/${BINARY_NAME}..."
|
log_info "Installing binary to ${INSTALL_DIR}/${BINARY_NAME}..."
|
||||||
mkdir -p "$INSTALL_DIR"
|
mkdir -p "$INSTALL_DIR"
|
||||||
mv "$TMP_BIN" "${INSTALL_DIR}/${BINARY_NAME}"
|
mv "$TMP_BIN" "${INSTALL_DIR}/${BINARY_NAME}"
|
||||||
|
chmod +x "${INSTALL_DIR}/${BINARY_NAME}"
|
||||||
|
|
||||||
# --- Legacy Cleanup ---
|
# --- Legacy Cleanup ---
|
||||||
# Remove old agents if they exist to prevent conflicts
|
# Remove old agents if they exist to prevent conflicts
|
||||||
|
|
@ -232,18 +335,45 @@ EOF
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 2. Synology (Upstart)
|
# 2. Synology DSM
|
||||||
if [[ -d /usr/syno/etc/rc.sysv ]]; then
|
# DSM 7+ uses systemd, DSM 6.x uses upstart
|
||||||
CONF="/etc/init/${AGENT_NAME}.conf"
|
if [[ -d /usr/syno ]] && [[ -f /etc/VERSION ]]; then
|
||||||
log_info "Configuring Upstart service at $CONF..."
|
# Extract major version from /etc/VERSION
|
||||||
|
DSM_MAJOR=$(grep 'majorversion=' /etc/VERSION | cut -d'"' -f2)
|
||||||
|
log_info "Detected Synology DSM ${DSM_MAJOR}..."
|
||||||
|
|
||||||
# Build command line args
|
# Build command line args
|
||||||
EXEC_ARGS="--url \"${PULSE_URL}\" --token \"${PULSE_TOKEN}\" --interval \"${INTERVAL}\""
|
build_exec_args
|
||||||
[[ "$ENABLE_HOST" == "true" ]] && EXEC_ARGS="$EXEC_ARGS --enable-host"
|
|
||||||
[[ "$ENABLE_DOCKER" == "true" ]] && EXEC_ARGS="$EXEC_ARGS --enable-docker"
|
|
||||||
[[ "$INSECURE" == "true" ]] && EXEC_ARGS="$EXEC_ARGS --insecure"
|
|
||||||
|
|
||||||
cat > "$CONF" <<EOF
|
if [[ "$DSM_MAJOR" -ge 7 ]]; then
|
||||||
|
# DSM 7+ uses systemd
|
||||||
|
UNIT="/etc/systemd/system/${AGENT_NAME}.service"
|
||||||
|
log_info "Configuring systemd service at $UNIT (DSM 7+)..."
|
||||||
|
|
||||||
|
cat > "$UNIT" <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Pulse Unified Agent
|
||||||
|
After=network.target
|
||||||
|
StartLimitIntervalSec=0
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=${INSTALL_DIR}/${BINARY_NAME} ${EXEC_ARGS}
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable "${AGENT_NAME}"
|
||||||
|
systemctl restart "${AGENT_NAME}"
|
||||||
|
else
|
||||||
|
# DSM 6.x uses upstart
|
||||||
|
CONF="/etc/init/${AGENT_NAME}.conf"
|
||||||
|
log_info "Configuring Upstart service at $CONF (DSM 6.x)..."
|
||||||
|
|
||||||
|
cat > "$CONF" <<EOF
|
||||||
description "Pulse Unified Agent"
|
description "Pulse Unified Agent"
|
||||||
author "Pulse"
|
author "Pulse"
|
||||||
|
|
||||||
|
|
@ -255,28 +385,171 @@ respawn limit 5 10
|
||||||
|
|
||||||
exec ${INSTALL_DIR}/${BINARY_NAME} ${EXEC_ARGS} >> ${LOG_FILE} 2>&1
|
exec ${INSTALL_DIR}/${BINARY_NAME} ${EXEC_ARGS} >> ${LOG_FILE} 2>&1
|
||||||
EOF
|
EOF
|
||||||
initctl stop "${AGENT_NAME}" 2>/dev/null || true
|
initctl stop "${AGENT_NAME}" 2>/dev/null || true
|
||||||
initctl start "${AGENT_NAME}"
|
initctl start "${AGENT_NAME}"
|
||||||
|
fi
|
||||||
|
|
||||||
log_info "Service started."
|
log_info "Service started."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 3. Linux (Systemd)
|
# 3. Unraid (no init system - use /boot/config/go script)
|
||||||
|
# Detect Unraid by /etc/unraid-version (preferred) or /boot/config/go with unraid markers
|
||||||
|
if [[ -f /etc/unraid-version ]]; then
|
||||||
|
log_info "Detected Unraid system..."
|
||||||
|
|
||||||
|
# Unraid's /boot is FAT32 (no execute permission), so we store the binary there
|
||||||
|
# for persistence but copy it to RAM disk (/usr/local/bin) for execution
|
||||||
|
UNRAID_STORAGE_DIR="/boot/config/plugins/pulse-agent"
|
||||||
|
UNRAID_STORED_BINARY="${UNRAID_STORAGE_DIR}/${BINARY_NAME}"
|
||||||
|
RUNTIME_BINARY="${INSTALL_DIR}/${BINARY_NAME}"
|
||||||
|
GO_SCRIPT="/boot/config/go"
|
||||||
|
|
||||||
|
mkdir -p "$UNRAID_STORAGE_DIR"
|
||||||
|
|
||||||
|
# Copy binary to persistent storage (for survival across reboots)
|
||||||
|
cp "${RUNTIME_BINARY}" "$UNRAID_STORED_BINARY"
|
||||||
|
# Keep binary in /usr/local/bin (RAM disk) with execute permission for runtime
|
||||||
|
chmod +x "${RUNTIME_BINARY}"
|
||||||
|
|
||||||
|
log_info "Installed binary to ${UNRAID_STORED_BINARY} (persistent) and ${RUNTIME_BINARY} (runtime)..."
|
||||||
|
|
||||||
|
# Build command line args (string for wrapper script, array for direct execution)
|
||||||
|
build_exec_args
|
||||||
|
build_exec_args_array
|
||||||
|
|
||||||
|
# Kill any existing pulse agents (legacy or current)
|
||||||
|
log_info "Stopping any existing pulse agents..."
|
||||||
|
# Use process name matching to avoid killing unrelated processes
|
||||||
|
pkill -f "^${RUNTIME_BINARY}" 2>/dev/null || true
|
||||||
|
pkill -f "^/usr/local/bin/pulse-host-agent" 2>/dev/null || true
|
||||||
|
pkill -f "^/usr/local/bin/pulse-docker-agent" 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Clean up legacy binaries from RAM disk
|
||||||
|
rm -f /usr/local/bin/pulse-host-agent 2>/dev/null || true
|
||||||
|
rm -f /usr/local/bin/pulse-docker-agent 2>/dev/null || true
|
||||||
|
|
||||||
|
# Create a wrapper script that will be called from /boot/config/go
|
||||||
|
# This script copies from persistent storage to RAM disk on boot, then starts the agent
|
||||||
|
WRAPPER_SCRIPT="${UNRAID_STORAGE_DIR}/start-pulse-agent.sh"
|
||||||
|
cat > "$WRAPPER_SCRIPT" <<EOF
|
||||||
|
#!/bin/bash
|
||||||
|
# Pulse Agent startup script for Unraid
|
||||||
|
# Auto-generated by Pulse installer
|
||||||
|
|
||||||
|
# Kill any existing pulse-agent processes
|
||||||
|
pkill -f "^${RUNTIME_BINARY}" 2>/dev/null || true
|
||||||
|
pkill -f "^/usr/local/bin/pulse-host-agent" 2>/dev/null || true
|
||||||
|
pkill -f "^/usr/local/bin/pulse-docker-agent" 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Copy binary from persistent storage to RAM disk (needed after reboot)
|
||||||
|
cp "${UNRAID_STORED_BINARY}" "${RUNTIME_BINARY}"
|
||||||
|
chmod +x "${RUNTIME_BINARY}"
|
||||||
|
|
||||||
|
# Start the agent in the background
|
||||||
|
nohup ${RUNTIME_BINARY} ${EXEC_ARGS} >> /var/log/${AGENT_NAME}.log 2>&1 &
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Add to /boot/config/go if not already present
|
||||||
|
GO_MARKER="# Pulse Agent"
|
||||||
|
if [[ -f "$GO_SCRIPT" ]]; then
|
||||||
|
# Remove any existing Pulse agent entries
|
||||||
|
sed -i "/${GO_MARKER}/,/^$/d" "$GO_SCRIPT" 2>/dev/null || true
|
||||||
|
sed -i '/pulse-agent/d' "$GO_SCRIPT" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
# Create go script if it doesn't exist
|
||||||
|
echo "#!/bin/bash" > "$GO_SCRIPT"
|
||||||
|
chmod +x "$GO_SCRIPT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Append startup entry (use bash explicitly since /boot is FAT32 and doesn't support execute bits)
|
||||||
|
cat >> "$GO_SCRIPT" <<EOF
|
||||||
|
|
||||||
|
${GO_MARKER}
|
||||||
|
bash ${WRAPPER_SCRIPT}
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log_info "Added startup entry to ${GO_SCRIPT}..."
|
||||||
|
|
||||||
|
# Start the agent now (use array for proper argument handling)
|
||||||
|
log_info "Starting agent..."
|
||||||
|
nohup "${RUNTIME_BINARY}" "${EXEC_ARGS_ARRAY[@]}" >> "/var/log/${AGENT_NAME}.log" 2>&1 &
|
||||||
|
|
||||||
|
log_info "Installation complete!"
|
||||||
|
log_info "The agent will start automatically on boot."
|
||||||
|
log_info "To check status: pgrep -a pulse-agent"
|
||||||
|
log_info "To view logs: tail -f /var/log/${AGENT_NAME}.log"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. OpenRC (Alpine, Gentoo, Artix, etc.)
|
||||||
|
# Check for rc-service but make sure we're not on a systemd system that happens to have it
|
||||||
|
if command -v rc-service >/dev/null 2>&1 && [[ -d /etc/init.d ]] && ! command -v systemctl >/dev/null 2>&1; then
|
||||||
|
INITSCRIPT="/etc/init.d/${AGENT_NAME}"
|
||||||
|
log_info "Configuring OpenRC service at $INITSCRIPT..."
|
||||||
|
|
||||||
|
# Build command line args
|
||||||
|
build_exec_args
|
||||||
|
|
||||||
|
# Create OpenRC init script following Alpine best practices
|
||||||
|
# Using command_background=yes with pidfile for proper daemon management
|
||||||
|
cat > "$INITSCRIPT" <<'INITEOF'
|
||||||
|
#!/sbin/openrc-run
|
||||||
|
# Pulse Unified Agent OpenRC init script
|
||||||
|
|
||||||
|
name="pulse-agent"
|
||||||
|
description="Pulse Unified Agent"
|
||||||
|
|
||||||
|
command="INSTALL_DIR_PLACEHOLDER/BINARY_NAME_PLACEHOLDER"
|
||||||
|
command_args="EXEC_ARGS_PLACEHOLDER"
|
||||||
|
command_background="yes"
|
||||||
|
command_user="root"
|
||||||
|
|
||||||
|
pidfile="/run/${RC_SVCNAME}.pid"
|
||||||
|
output_log="/var/log/pulse-agent.log"
|
||||||
|
error_log="/var/log/pulse-agent.log"
|
||||||
|
|
||||||
|
# Ensure log file exists
|
||||||
|
start_pre() {
|
||||||
|
touch "$output_log"
|
||||||
|
}
|
||||||
|
|
||||||
|
depend() {
|
||||||
|
need net
|
||||||
|
use docker
|
||||||
|
}
|
||||||
|
INITEOF
|
||||||
|
|
||||||
|
# Replace placeholders with actual values
|
||||||
|
sed -i "s|INSTALL_DIR_PLACEHOLDER|${INSTALL_DIR}|g" "$INITSCRIPT"
|
||||||
|
sed -i "s|BINARY_NAME_PLACEHOLDER|${BINARY_NAME}|g" "$INITSCRIPT"
|
||||||
|
sed -i "s|EXEC_ARGS_PLACEHOLDER|${EXEC_ARGS}|g" "$INITSCRIPT"
|
||||||
|
|
||||||
|
chmod +x "$INITSCRIPT"
|
||||||
|
rc-service "${AGENT_NAME}" stop 2>/dev/null || true
|
||||||
|
rc-update add "${AGENT_NAME}" default 2>/dev/null || true
|
||||||
|
rc-service "${AGENT_NAME}" start
|
||||||
|
log_info "Service started."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5. Linux (Systemd)
|
||||||
if command -v systemctl >/dev/null 2>&1; then
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
UNIT="/etc/systemd/system/${AGENT_NAME}.service"
|
UNIT="/etc/systemd/system/${AGENT_NAME}.service"
|
||||||
log_info "Configuring Systemd service at $UNIT..."
|
log_info "Configuring Systemd service at $UNIT..."
|
||||||
|
|
||||||
# Build command line args
|
# Build command line args
|
||||||
EXEC_ARGS="--url ${PULSE_URL} --token ${PULSE_TOKEN} --interval ${INTERVAL}"
|
build_exec_args
|
||||||
[[ "$ENABLE_HOST" == "true" ]] && EXEC_ARGS="$EXEC_ARGS --enable-host"
|
|
||||||
[[ "$ENABLE_DOCKER" == "true" ]] && EXEC_ARGS="$EXEC_ARGS --enable-docker"
|
|
||||||
[[ "$INSECURE" == "true" ]] && EXEC_ARGS="$EXEC_ARGS --insecure"
|
|
||||||
|
|
||||||
cat > "$UNIT" <<EOF
|
cat > "$UNIT" <<EOF
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Pulse Unified Agent
|
Description=Pulse Unified Agent
|
||||||
After=network-online.target docker.service
|
After=network-online.target docker.service
|
||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
|
StartLimitIntervalSec=0
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
|
|
@ -295,4 +568,9 @@ EOF
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
fail "Could not detect a supported service manager (systemd, launchd, upstart)."
|
fail "Could not detect a supported service manager (systemd, OpenRC, launchd, or Unraid)."
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
# Call main function with all arguments
|
||||||
|
main "$@"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue