Files
Pulse/internal/monitoring/temperature_service.go
rcourtman e21a72578f Add configurable SSH port for temperature monitoring
Related to #595

This change adds support for custom SSH ports when collecting temperature
data from Proxmox nodes, resolving issues for users who run SSH on non-standard
ports.

**Why SSH is still needed:**
Temperature monitoring requires reading /sys/class/hwmon sensors on Proxmox
nodes, which is not exposed via the Proxmox API. Even when using API tokens
for authentication, Pulse needs SSH access to collect temperature data.

**Changes:**
- Add `sshPort` configuration to SystemSettings (system.json)
- Add `SSHPort` field to Config with environment variable support (SSH_PORT)
- Add per-node SSH port override capability for PVE, PBS, and PMG instances
- Update TemperatureCollector to accept and use custom SSH port
- Update SSH known_hosts manager to support non-standard ports
- Add NewTemperatureCollectorWithPort() constructor with port parameter
- Maintain backward compatibility with NewTemperatureCollector() (uses port 22)
- Update frontend TypeScript types for SSH port configuration

**Configuration methods:**
1. Environment variable: SSH_PORT=2222
2. system.json: {"sshPort": 2222}
3. Per-node override in nodes.enc (future UI support)

**Default behavior:**
- Defaults to port 22 if not configured
- Maintains full backward compatibility
- No changes required for existing deployments

The implementation includes proper ssh-keyscan port handling and known_hosts
management for non-standard ports using [host]:port notation per SSH standards.
2025-11-05 20:03:29 +00:00

105 lines
2.3 KiB
Go

package monitoring
import (
"context"
stderrors "errors"
"sync"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
)
var (
// ErrTemperatureMonitoringDisabled indicates that temperature polling is disabled globally.
ErrTemperatureMonitoringDisabled = stderrors.New("temperature monitoring disabled")
// ErrTemperatureCollectorUnavailable indicates the collector could not be created or is misconfigured.
ErrTemperatureCollectorUnavailable = stderrors.New("temperature collector unavailable")
)
// TemperatureService defines the contract used by the monitor to collect temperature data.
type TemperatureService interface {
Enabled() bool
Collect(ctx context.Context, host, nodeName string) (*models.Temperature, error)
Enable()
Disable()
}
type temperatureService struct {
mu sync.RWMutex
enabled bool
user string
keyPath string
sshPort int
collector *TemperatureCollector
}
func newTemperatureService(enabled bool, user, keyPath string, sshPort int) TemperatureService {
if sshPort <= 0 {
sshPort = 22
}
return &temperatureService{
enabled: enabled,
user: user,
keyPath: keyPath,
sshPort: sshPort,
}
}
func (s *temperatureService) Enabled() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.enabled
}
func (s *temperatureService) Enable() {
s.mu.Lock()
defer s.mu.Unlock()
if s.enabled {
return
}
s.enabled = true
if s.collector == nil && s.user != "" && s.keyPath != "" {
s.collector = NewTemperatureCollectorWithPort(s.user, s.keyPath, s.sshPort)
}
}
func (s *temperatureService) Disable() {
s.mu.Lock()
defer s.mu.Unlock()
s.enabled = false
s.collector = nil
}
func (s *temperatureService) Collect(ctx context.Context, host, nodeName string) (*models.Temperature, error) {
s.mu.RLock()
enabled := s.enabled
collector := s.collector
s.mu.RUnlock()
if !enabled {
return nil, ErrTemperatureMonitoringDisabled
}
if collector == nil {
s.mu.Lock()
if s.enabled && s.collector == nil && s.user != "" && s.keyPath != "" {
s.collector = NewTemperatureCollectorWithPort(s.user, s.keyPath, s.sshPort)
}
collector = s.collector
enabled = s.enabled
s.mu.Unlock()
if !enabled {
return nil, ErrTemperatureMonitoringDisabled
}
}
if collector == nil {
return nil, ErrTemperatureCollectorUnavailable
}
return collector.CollectTemperature(ctx, host, nodeName)
}