feat: add comprehensive node cleanup system
Implements automated cleanup workflow when nodes are deleted from Pulse, removing all monitoring footprint from the host. Changes include a new RPC handler in the sensor proxy for cleanup requests, enhanced node deletion modal with detailed cleanup explanations, and improved SSH key management with proper tagging for atomic updates.
This commit is contained in:
parent
d0f7fd6404
commit
123e0f04ca
8 changed files with 487 additions and 130 deletions
88
cmd/pulse-sensor-proxy/cleanup.go
Normal file
88
cmd/pulse-sensor-proxy/cleanup.go
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
const cleanupRequestFilename = "cleanup-request.json"
|
||||||
|
|
||||||
|
func (p *Proxy) cleanupRequestPath() (string, error) {
|
||||||
|
workDir := p.workDir
|
||||||
|
if workDir == "" {
|
||||||
|
workDir = defaultWorkDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
if workDir == "" {
|
||||||
|
return "", errors.New("cleanup working directory not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(workDir, cleanupRequestFilename), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Proxy) handleRequestCleanup(ctx context.Context, req *RPCRequest, logger zerolog.Logger) (interface{}, error) {
|
||||||
|
cleanupPath, err := p.cleanupRequestPath()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := filepath.Dir(cleanupPath)
|
||||||
|
if err := os.MkdirAll(dir, 0o750); err != nil {
|
||||||
|
return nil, fmt.Errorf("ensure cleanup directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := map[string]any{
|
||||||
|
"requestedAt": time.Now().UTC().Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
if req != nil && req.Params != nil {
|
||||||
|
if host, ok := req.Params["host"].(string); ok && host != "" {
|
||||||
|
payload["host"] = host
|
||||||
|
}
|
||||||
|
if reason, ok := req.Params["reason"].(string); ok && reason != "" {
|
||||||
|
payload["reason"] = reason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("encode cleanup payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpFile, err := os.CreateTemp(dir, "cleanup-request-*.tmp")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("prepare cleanup signal: %w", err)
|
||||||
|
}
|
||||||
|
tmpName := tmpFile.Name()
|
||||||
|
if _, err := tmpFile.Write(append(data, '\n')); err != nil {
|
||||||
|
tmpFile.Close()
|
||||||
|
os.Remove(tmpName)
|
||||||
|
return nil, fmt.Errorf("write cleanup payload: %w", err)
|
||||||
|
}
|
||||||
|
if err := tmpFile.Chmod(0o600); err != nil {
|
||||||
|
logger.Warn().Err(err).Str("path", tmpName).Msg("Failed to set cleanup payload permissions")
|
||||||
|
}
|
||||||
|
if err := tmpFile.Close(); err != nil {
|
||||||
|
os.Remove(tmpName)
|
||||||
|
return nil, fmt.Errorf("close cleanup payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace any existing request atomically so systemd path units trigger on change.
|
||||||
|
if err := os.Rename(tmpName, cleanupPath); err != nil {
|
||||||
|
os.Remove(tmpName)
|
||||||
|
return nil, fmt.Errorf("activate cleanup payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info().
|
||||||
|
Str("path", cleanupPath).
|
||||||
|
Interface("payload", payload).
|
||||||
|
Msg("Cleanup request signalled")
|
||||||
|
|
||||||
|
return map[string]any{"queued": true}, nil
|
||||||
|
}
|
||||||
|
|
@ -35,6 +35,10 @@ const (
|
||||||
maxRequestBytes = 16 * 1024 // 16 KiB max request size
|
maxRequestBytes = 16 * 1024 // 16 KiB max request size
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func defaultWorkDir() string {
|
||||||
|
return "/var/lib/pulse-sensor-proxy"
|
||||||
|
}
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "pulse-sensor-proxy",
|
Use: "pulse-sensor-proxy",
|
||||||
Short: "Pulse Sensor Proxy - Secure sensor data bridge for containerized Pulse",
|
Short: "Pulse Sensor Proxy - Secure sensor data bridge for containerized Pulse",
|
||||||
|
|
@ -74,6 +78,7 @@ func main() {
|
||||||
type Proxy struct {
|
type Proxy struct {
|
||||||
socketPath string
|
socketPath string
|
||||||
sshKeyPath string
|
sshKeyPath string
|
||||||
|
workDir string
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
rateLimiter *rateLimiter
|
rateLimiter *rateLimiter
|
||||||
nodeGate *nodeGate
|
nodeGate *nodeGate
|
||||||
|
|
@ -93,6 +98,7 @@ const (
|
||||||
RPCRegisterNodes = "register_nodes"
|
RPCRegisterNodes = "register_nodes"
|
||||||
RPCGetTemperature = "get_temperature"
|
RPCGetTemperature = "get_temperature"
|
||||||
RPCGetStatus = "get_status"
|
RPCGetStatus = "get_status"
|
||||||
|
RPCRequestCleanup = "request_cleanup"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RPCRequest represents a request from Pulse
|
// RPCRequest represents a request from Pulse
|
||||||
|
|
@ -158,12 +164,20 @@ func runProxy() {
|
||||||
metrics: metrics,
|
metrics: metrics,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if wd, err := os.Getwd(); err == nil {
|
||||||
|
proxy.workDir = wd
|
||||||
|
} else {
|
||||||
|
log.Warn().Err(err).Msg("Failed to determine working directory; using default")
|
||||||
|
proxy.workDir = defaultWorkDir()
|
||||||
|
}
|
||||||
|
|
||||||
// Register RPC method handlers
|
// Register RPC method handlers
|
||||||
proxy.router = map[string]handlerFunc{
|
proxy.router = map[string]handlerFunc{
|
||||||
RPCGetStatus: proxy.handleGetStatusV2,
|
RPCGetStatus: proxy.handleGetStatusV2,
|
||||||
RPCEnsureClusterKeys: proxy.handleEnsureClusterKeysV2,
|
RPCEnsureClusterKeys: proxy.handleEnsureClusterKeysV2,
|
||||||
RPCRegisterNodes: proxy.handleRegisterNodesV2,
|
RPCRegisterNodes: proxy.handleRegisterNodesV2,
|
||||||
RPCGetTemperature: proxy.handleGetTemperatureV2,
|
RPCGetTemperature: proxy.handleGetTemperatureV2,
|
||||||
|
RPCRequestCleanup: proxy.handleRequestCleanup,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := proxy.initAuthRules(); err != nil {
|
if err := proxy.initAuthRules(); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@ func (p *Proxy) testSSHConnection(nodeHost string) error {
|
||||||
|
|
||||||
privKeyPath := filepath.Join(p.sshKeyPath, "id_ed25519")
|
privKeyPath := filepath.Join(p.sshKeyPath, "id_ed25519")
|
||||||
cmd := fmt.Sprintf(
|
cmd := fmt.Sprintf(
|
||||||
`ssh -i %s -o StrictHostKeyChecking=no -o ConnectTimeout=5 root@%s "echo test"`,
|
`ssh -i %[1]s -T -n -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o ConnectTimeout=5 root@%[2]s "echo test"`,
|
||||||
privKeyPath,
|
privKeyPath,
|
||||||
nodeHost,
|
nodeHost,
|
||||||
)
|
)
|
||||||
|
|
@ -166,7 +166,7 @@ func (p *Proxy) getTemperatureViaSSH(nodeHost string) (string, error) {
|
||||||
// Since we use ForceCommand="sensors -j", any SSH command will run sensors
|
// Since we use ForceCommand="sensors -j", any SSH command will run sensors
|
||||||
// We don't need to specify the command
|
// We don't need to specify the command
|
||||||
cmd := fmt.Sprintf(
|
cmd := fmt.Sprintf(
|
||||||
`ssh -i %s -o StrictHostKeyChecking=no -o ConnectTimeout=5 root@%s ""`,
|
`ssh -i %[1]s -T -n -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o ConnectTimeout=5 root@%[2]s ""`,
|
||||||
privKeyPath,
|
privKeyPath,
|
||||||
nodeHost,
|
nodeHost,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -335,6 +335,9 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
const [currentNodeType, setCurrentNodeType] = createSignal<'pve' | 'pbs' | 'pmg'>('pve');
|
const [currentNodeType, setCurrentNodeType] = createSignal<'pve' | 'pbs' | 'pmg'>('pve');
|
||||||
const [modalResetKey, setModalResetKey] = createSignal(0);
|
const [modalResetKey, setModalResetKey] = createSignal(0);
|
||||||
const [showPasswordModal, setShowPasswordModal] = createSignal(false);
|
const [showPasswordModal, setShowPasswordModal] = createSignal(false);
|
||||||
|
const [showDeleteNodeModal, setShowDeleteNodeModal] = createSignal(false);
|
||||||
|
const [nodePendingDelete, setNodePendingDelete] = createSignal<NodeConfigWithStatus | null>(null);
|
||||||
|
const [deleteNodeLoading, setDeleteNodeLoading] = createSignal(false);
|
||||||
const [initialLoadComplete, setInitialLoadComplete] = createSignal(false);
|
const [initialLoadComplete, setInitialLoadComplete] = createSignal(false);
|
||||||
const [discoveryScanStatus, setDiscoveryScanStatus] = createSignal<DiscoveryScanStatus>({
|
const [discoveryScanStatus, setDiscoveryScanStatus] = createSignal<DiscoveryScanStatus>({
|
||||||
scanning: false,
|
scanning: false,
|
||||||
|
|
@ -1091,15 +1094,53 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteNode = async (nodeId: string) => {
|
const nodePendingDeleteLabel = () => {
|
||||||
if (!confirm('Are you sure you want to delete this node?')) return;
|
const node = nodePendingDelete();
|
||||||
|
if (!node) return '';
|
||||||
|
return node.displayName || node.name || node.host || node.id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const nodePendingDeleteHost = () => nodePendingDelete()?.host || '';
|
||||||
|
const nodePendingDeleteType = () => nodePendingDelete()?.type || '';
|
||||||
|
const nodePendingDeleteTypeLabel = () => {
|
||||||
|
switch (nodePendingDeleteType()) {
|
||||||
|
case 'pve':
|
||||||
|
return 'Proxmox VE node';
|
||||||
|
case 'pbs':
|
||||||
|
return 'Proxmox Backup Server';
|
||||||
|
case 'pmg':
|
||||||
|
return 'Proxmox Mail Gateway';
|
||||||
|
default:
|
||||||
|
return 'Pulse node';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestDeleteNode = (node: NodeConfigWithStatus) => {
|
||||||
|
setNodePendingDelete(node);
|
||||||
|
setShowDeleteNodeModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelDeleteNode = () => {
|
||||||
|
if (deleteNodeLoading()) return;
|
||||||
|
setShowDeleteNodeModal(false);
|
||||||
|
setNodePendingDelete(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteNode = async () => {
|
||||||
|
const pending = nodePendingDelete();
|
||||||
|
if (!pending) return;
|
||||||
|
setDeleteNodeLoading(true);
|
||||||
try {
|
try {
|
||||||
await NodesAPI.deleteNode(nodeId);
|
await NodesAPI.deleteNode(pending.id);
|
||||||
setNodes(nodes().filter((n) => n.id !== nodeId));
|
setNodes(nodes().filter((n) => n.id !== pending.id));
|
||||||
showSuccess('Node deleted successfully');
|
const label = pending.displayName || pending.name || pending.host || pending.id;
|
||||||
|
showSuccess(`${label} removed successfully`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error instanceof Error ? error.message : 'Failed to delete node');
|
showError(error instanceof Error ? error.message : 'Failed to delete node');
|
||||||
|
} finally {
|
||||||
|
setDeleteNodeLoading(false);
|
||||||
|
setShowDeleteNodeModal(false);
|
||||||
|
setNodePendingDelete(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1869,7 +1910,7 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => deleteNode(node.id)}
|
onClick={() => requestDeleteNode(node)}
|
||||||
class="p-2 text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300"
|
class="p-2 text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
|
@ -2302,7 +2343,7 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => deleteNode(node.id)}
|
onClick={() => requestDeleteNode(node)}
|
||||||
class="p-2 text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300"
|
class="p-2 text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
|
@ -2712,7 +2753,7 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => deleteNode(pmgNode.id)}
|
onClick={() => requestDeleteNode(pmgNode)}
|
||||||
class="p-2 text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300"
|
class="p-2 text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300"
|
||||||
title="Delete node"
|
title="Delete node"
|
||||||
>
|
>
|
||||||
|
|
@ -5437,6 +5478,78 @@ const Settings: Component<SettingsProps> = (props) => {
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Delete Node Modal */}
|
||||||
|
<Show when={showDeleteNodeModal()}>
|
||||||
|
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
|
||||||
|
<Card padding="lg" class="w-full max-w-lg space-y-5">
|
||||||
|
<SectionHeader
|
||||||
|
title={`Remove ${nodePendingDeleteLabel()}`}
|
||||||
|
size="md"
|
||||||
|
class="mb-1"
|
||||||
|
/>
|
||||||
|
<div class="space-y-3 text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
<p>
|
||||||
|
Removing this {nodePendingDeleteTypeLabel().toLowerCase()} also scrubs the Pulse
|
||||||
|
footprint on the host — the proxy service, SSH key, API token, and bind mount are
|
||||||
|
all cleaned up automatically.
|
||||||
|
</p>
|
||||||
|
<div class="rounded-lg border border-blue-200 bg-blue-50 p-3 text-sm leading-relaxed dark:border-blue-800 dark:bg-blue-900/20 dark:text-blue-100">
|
||||||
|
<p class="font-medium text-blue-900 dark:text-blue-100">What happens next</p>
|
||||||
|
<ul class="mt-2 list-disc space-y-1 pl-4 text-blue-800 dark:text-blue-200 text-sm">
|
||||||
|
<li>Pulse removes the node entry and clears related alerts.</li>
|
||||||
|
<li>
|
||||||
|
{nodePendingDeleteHost() ? (
|
||||||
|
<>
|
||||||
|
The host{' '}
|
||||||
|
<span class="font-semibold">{nodePendingDeleteHost()}</span> loses the
|
||||||
|
proxy service, SSH key, and API token.
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'The host loses the proxy service, SSH key, and API token.'
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
If the host comes back later, rerunning the setup script reinstalls everything
|
||||||
|
with a fresh key.
|
||||||
|
</li>
|
||||||
|
<Show when={nodePendingDeleteType() === 'pbs'}>
|
||||||
|
<li>
|
||||||
|
Backup user tokens on the PBS are removed, so jobs referencing them will no
|
||||||
|
longer authenticate until the node is re-added.
|
||||||
|
</li>
|
||||||
|
</Show>
|
||||||
|
<Show when={nodePendingDeleteType() === 'pmg'}>
|
||||||
|
<li>
|
||||||
|
Mail gateway tokens are removed as part of the cleanup; re-enroll to restore
|
||||||
|
outbound telemetry.
|
||||||
|
</li>
|
||||||
|
</Show>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={cancelDeleteNode}
|
||||||
|
class="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-100 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||||
|
disabled={deleteNodeLoading()}
|
||||||
|
>
|
||||||
|
Keep node
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={deleteNode}
|
||||||
|
disabled={deleteNodeLoading()}
|
||||||
|
class="rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-red-700 disabled:cursor-not-allowed disabled:opacity-60 dark:bg-red-500 dark:hover:bg-red-400"
|
||||||
|
>
|
||||||
|
{deleteNodeLoading() ? 'Removing…' : 'Remove node'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
{/* Node Modal - Use separate modals for PVE and PBS to ensure clean state */}
|
{/* Node Modal - Use separate modals for PVE and PBS to ensure clean state */}
|
||||||
<Show when={showNodeModal() && currentNodeType() === 'pve'}>
|
<Show when={showNodeModal() && currentNodeType() === 'pve'}>
|
||||||
<NodeModal
|
<NodeModal
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import (
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
|
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
||||||
|
"github.com/rcourtman/pulse-go-rewrite/internal/tempproxy"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
|
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/pkg/discovery"
|
"github.com/rcourtman/pulse-go-rewrite/pkg/discovery"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/pkg/pbs"
|
"github.com/rcourtman/pulse-go-rewrite/pkg/pbs"
|
||||||
|
|
@ -2006,10 +2007,36 @@ func (h *ConfigHandlers) HandleDeleteNode(w http.ResponseWriter, r *http.Request
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if deletedNodeType == "pve" && deletedNodeHost != "" {
|
||||||
|
go h.triggerPVEHostCleanup(deletedNodeHost)
|
||||||
|
}
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
|
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *ConfigHandlers) triggerPVEHostCleanup(host string) {
|
||||||
|
client := tempproxy.NewClient()
|
||||||
|
if client == nil || !client.IsAvailable() {
|
||||||
|
log.Debug().
|
||||||
|
Str("host", host).
|
||||||
|
Msg("Skipping PVE cleanup request; sensor proxy socket unavailable")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.RequestCleanup(host); err != nil {
|
||||||
|
log.Warn().
|
||||||
|
Err(err).
|
||||||
|
Str("host", host).
|
||||||
|
Msg("Failed to queue PVE host cleanup via sensor proxy")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().
|
||||||
|
Str("host", host).
|
||||||
|
Msg("Queued PVE host cleanup via sensor proxy")
|
||||||
|
}
|
||||||
|
|
||||||
// HandleTestExistingNode tests a connection for an existing node using stored credentials
|
// HandleTestExistingNode tests a connection for an existing node using stored credentials
|
||||||
func (h *ConfigHandlers) HandleTestExistingNode(w http.ResponseWriter, r *http.Request) {
|
func (h *ConfigHandlers) HandleTestExistingNode(w http.ResponseWriter, r *http.Request) {
|
||||||
nodeID := strings.TrimPrefix(r.URL.Path, "/api/config/nodes/")
|
nodeID := strings.TrimPrefix(r.URL.Path, "/api/config/nodes/")
|
||||||
|
|
@ -3234,7 +3261,19 @@ if [[ $MAIN_ACTION =~ ^[2Rr]$ ]]; then
|
||||||
echo "Removing Pulse monitoring components..."
|
echo "Removing Pulse monitoring components..."
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Stop and remove pulse-sensor-proxy service
|
CLEANUP_HELPER_USED=false
|
||||||
|
if [ -x /usr/local/bin/pulse-sensor-cleanup.sh ]; then
|
||||||
|
echo " • Running cleanup helper..."
|
||||||
|
if /usr/local/bin/pulse-sensor-cleanup.sh; then
|
||||||
|
CLEANUP_HELPER_USED=true
|
||||||
|
else
|
||||||
|
echo " ✗ Cleanup helper failed, falling back to manual removal."
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$CLEANUP_HELPER_USED" != true ]; then
|
||||||
|
# Stop and remove pulse-sensor services
|
||||||
if command -v systemctl &> /dev/null; then
|
if command -v systemctl &> /dev/null; then
|
||||||
if systemctl is-active --quiet pulse-sensor-proxy 2>/dev/null; then
|
if systemctl is-active --quiet pulse-sensor-proxy 2>/dev/null; then
|
||||||
echo " • Stopping pulse-sensor-proxy service..."
|
echo " • Stopping pulse-sensor-proxy service..."
|
||||||
|
|
@ -3244,9 +3283,25 @@ if [[ $MAIN_ACTION =~ ^[2Rr]$ ]]; then
|
||||||
echo " • Disabling pulse-sensor-proxy service..."
|
echo " • Disabling pulse-sensor-proxy service..."
|
||||||
systemctl disable pulse-sensor-proxy || true
|
systemctl disable pulse-sensor-proxy || true
|
||||||
fi
|
fi
|
||||||
if [ -f /etc/systemd/system/pulse-sensor-proxy.service ]; then
|
if systemctl is-active --quiet pulse-sensor-cleanup.path 2>/dev/null; then
|
||||||
echo " • Removing systemd unit file..."
|
echo " • Stopping pulse-sensor-cleanup.path..."
|
||||||
|
systemctl stop pulse-sensor-cleanup.path || true
|
||||||
|
fi
|
||||||
|
if systemctl is-enabled --quiet pulse-sensor-cleanup.path 2>/dev/null; then
|
||||||
|
echo " • Disabling pulse-sensor-cleanup.path..."
|
||||||
|
systemctl disable pulse-sensor-cleanup.path || true
|
||||||
|
fi
|
||||||
|
if systemctl is-enabled --quiet pulse-sensor-cleanup.service 2>/dev/null; then
|
||||||
|
echo " • Disabling pulse-sensor-cleanup.service..."
|
||||||
|
systemctl disable pulse-sensor-cleanup.service || true
|
||||||
|
fi
|
||||||
|
if [ -f /etc/systemd/system/pulse-sensor-proxy.service ] || \
|
||||||
|
[ -f /etc/systemd/system/pulse-sensor-cleanup.service ] || \
|
||||||
|
[ -f /etc/systemd/system/pulse-sensor-cleanup.path ]; then
|
||||||
|
echo " • Removing systemd unit files..."
|
||||||
rm -f /etc/systemd/system/pulse-sensor-proxy.service
|
rm -f /etc/systemd/system/pulse-sensor-proxy.service
|
||||||
|
rm -f /etc/systemd/system/pulse-sensor-cleanup.service
|
||||||
|
rm -f /etc/systemd/system/pulse-sensor-cleanup.path
|
||||||
systemctl daemon-reload || true
|
systemctl daemon-reload || true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
@ -3257,6 +3312,12 @@ if [[ $MAIN_ACTION =~ ^[2Rr]$ ]]; then
|
||||||
rm -f /usr/local/bin/pulse-sensor-proxy
|
rm -f /usr/local/bin/pulse-sensor-proxy
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Remove cleanup helper script
|
||||||
|
if [ -f /usr/local/bin/pulse-sensor-cleanup.sh ]; then
|
||||||
|
echo " • Removing cleanup helper script..."
|
||||||
|
rm -f /usr/local/bin/pulse-sensor-cleanup.sh
|
||||||
|
fi
|
||||||
|
|
||||||
# Remove pulse-sensor-proxy data directory
|
# Remove pulse-sensor-proxy data directory
|
||||||
if [ -d /var/lib/pulse-sensor-proxy ]; then
|
if [ -d /var/lib/pulse-sensor-proxy ]; then
|
||||||
echo " • Removing pulse-sensor-proxy data directory..."
|
echo " • Removing pulse-sensor-proxy data directory..."
|
||||||
|
|
@ -3272,27 +3333,19 @@ if [[ $MAIN_ACTION =~ ^[2Rr]$ ]]; then
|
||||||
# Remove SSH keys from authorized_keys (only Pulse-managed entries)
|
# Remove SSH keys from authorized_keys (only Pulse-managed entries)
|
||||||
if [ -f /root/.ssh/authorized_keys ]; then
|
if [ -f /root/.ssh/authorized_keys ]; then
|
||||||
echo " • Removing SSH keys from authorized_keys..."
|
echo " • Removing SSH keys from authorized_keys..."
|
||||||
# Create temporary file for safe atomic update
|
|
||||||
TMP_AUTH_KEYS=$(mktemp)
|
TMP_AUTH_KEYS=$(mktemp)
|
||||||
if [ -f "$TMP_AUTH_KEYS" ]; then
|
if [ -f "$TMP_AUTH_KEYS" ]; then
|
||||||
# Remove only lines with pulse-managed-key marker (preserves user keys)
|
|
||||||
grep -vF '# pulse-managed-key' /root/.ssh/authorized_keys > "$TMP_AUTH_KEYS" 2>/dev/null
|
grep -vF '# pulse-managed-key' /root/.ssh/authorized_keys > "$TMP_AUTH_KEYS" 2>/dev/null
|
||||||
GREP_EXIT=$?
|
GREP_EXIT=$?
|
||||||
|
|
||||||
# Exit 0 = lines remain, Exit 1 = all lines removed (both are success)
|
|
||||||
if [ $GREP_EXIT -eq 0 ] || [ $GREP_EXIT -eq 1 ]; then
|
if [ $GREP_EXIT -eq 0 ] || [ $GREP_EXIT -eq 1 ]; then
|
||||||
# Preserve ownership and permissions from original
|
|
||||||
chmod --reference=/root/.ssh/authorized_keys "$TMP_AUTH_KEYS" 2>/dev/null || chmod 600 "$TMP_AUTH_KEYS"
|
chmod --reference=/root/.ssh/authorized_keys "$TMP_AUTH_KEYS" 2>/dev/null || chmod 600 "$TMP_AUTH_KEYS"
|
||||||
chown --reference=/root/.ssh/authorized_keys "$TMP_AUTH_KEYS" 2>/dev/null || true
|
chown --reference=/root/.ssh/authorized_keys "$TMP_AUTH_KEYS" 2>/dev/null || true
|
||||||
# Atomically replace (abort if mv fails)
|
|
||||||
if mv "$TMP_AUTH_KEYS" /root/.ssh/authorized_keys; then
|
if mv "$TMP_AUTH_KEYS" /root/.ssh/authorized_keys; then
|
||||||
:
|
:
|
||||||
else
|
else
|
||||||
# Cleanup temp file if move failed
|
|
||||||
rm -f "$TMP_AUTH_KEYS"
|
rm -f "$TMP_AUTH_KEYS"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# Cleanup temp file if grep had a real error (exit > 1)
|
|
||||||
rm -f "$TMP_AUTH_KEYS"
|
rm -f "$TMP_AUTH_KEYS"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
@ -3313,7 +3366,6 @@ if [[ $MAIN_ACTION =~ ^[2Rr]$ ]]; then
|
||||||
# Remove Pulse monitoring API tokens and user
|
# Remove Pulse monitoring API tokens and user
|
||||||
echo " • Removing Pulse monitoring API tokens and user..."
|
echo " • Removing Pulse monitoring API tokens and user..."
|
||||||
if command -v pveum &> /dev/null; then
|
if command -v pveum &> /dev/null; then
|
||||||
# List all tokens for pulse-monitor@pam and remove them (idempotent)
|
|
||||||
TOKEN_LIST=$(pveum user token list pulse-monitor@pam 2>/dev/null | awk 'NR>3 {print $2}' | grep -v '^$' || printf '')
|
TOKEN_LIST=$(pveum user token list pulse-monitor@pam 2>/dev/null | awk 'NR>3 {print $2}' | grep -v '^$' || printf '')
|
||||||
if [ -n "$TOKEN_LIST" ]; then
|
if [ -n "$TOKEN_LIST" ]; then
|
||||||
while IFS= read -r TOKEN; do
|
while IFS= read -r TOKEN; do
|
||||||
|
|
@ -3322,12 +3374,15 @@ if [[ $MAIN_ACTION =~ ^[2Rr]$ ]]; then
|
||||||
fi
|
fi
|
||||||
done <<< "$TOKEN_LIST"
|
done <<< "$TOKEN_LIST"
|
||||||
fi
|
fi
|
||||||
# Remove the user (idempotent)
|
|
||||||
pveum user delete pulse-monitor@pam 2>/dev/null || true
|
pveum user delete pulse-monitor@pam 2>/dev/null || true
|
||||||
# Remove custom roles (idempotent)
|
|
||||||
pveum role delete PulseMonitor 2>/dev/null || true
|
pveum role delete PulseMonitor 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if command -v proxmox-backup-manager &> /dev/null; then
|
||||||
|
proxmox-backup-manager user delete pulse-monitor@pbs 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "✓ Complete removal finished"
|
echo "✓ Complete removal finished"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
@ -3670,7 +3725,18 @@ if [ "$PULSE_IS_CONTAINERIZED" = true ] && [ -n "$PULSE_CTID" ]; then
|
||||||
export PULSE_SENSOR_PROXY_FALLBACK_URL="%s/api/install/pulse-sensor-proxy"
|
export PULSE_SENSOR_PROXY_FALLBACK_URL="%s/api/install/pulse-sensor-proxy"
|
||||||
|
|
||||||
# Run installer
|
# Run installer
|
||||||
if "$PROXY_INSTALLER" --ctid "$PULSE_CTID" --quiet 2>&1 | grep -E "✓|⚠️|ERROR"; then
|
INSTALL_OUTPUT=$("$PROXY_INSTALLER" --ctid "$PULSE_CTID" --quiet 2>&1)
|
||||||
|
INSTALL_STATUS=$?
|
||||||
|
|
||||||
|
if [ -n "$INSTALL_OUTPUT" ]; then
|
||||||
|
echo "$INSTALL_OUTPUT" | grep -E "✓|⚠️|ERROR" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$INSTALL_OUTPUT" ]; then
|
||||||
|
echo "$INSTALL_OUTPUT" | grep -E "✓|⚠️|ERROR" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $INSTALL_STATUS -eq 0 ]; then
|
||||||
# Verify proxy health
|
# Verify proxy health
|
||||||
PROXY_HEALTHY=false
|
PROXY_HEALTHY=false
|
||||||
if systemctl is-active --quiet pulse-sensor-proxy 2>/dev/null; then
|
if systemctl is-active --quiet pulse-sensor-proxy 2>/dev/null; then
|
||||||
|
|
@ -3735,6 +3801,11 @@ if [ "$PULSE_IS_CONTAINERIZED" = true ] && [ -n "$PULSE_CTID" ]; then
|
||||||
else
|
else
|
||||||
echo ""
|
echo ""
|
||||||
echo "⚠️ Proxy installation had issues - you may need to configure manually"
|
echo "⚠️ Proxy installation had issues - you may need to configure manually"
|
||||||
|
if [ -n "$INSTALL_OUTPUT" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "$INSTALL_OUTPUT" | tail -n 40
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rm -f "$PROXY_INSTALLER"
|
rm -f "$PROXY_INSTALLER"
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,7 @@ func (tc *TemperatureCollector) runSSHCommand(ctx context.Context, host, command
|
||||||
"-o", "UserKnownHostsFile=/dev/null",
|
"-o", "UserKnownHostsFile=/dev/null",
|
||||||
"-o", "ConnectTimeout=5",
|
"-o", "ConnectTimeout=5",
|
||||||
"-o", "BatchMode=yes", // No password prompts
|
"-o", "BatchMode=yes", // No password prompts
|
||||||
|
"-o", "LogLevel=ERROR", // Suppress host key warnings that break JSON parsing
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add key if specified
|
// Add key if specified
|
||||||
|
|
@ -123,7 +124,17 @@ func (tc *TemperatureCollector) runSSHCommand(ctx context.Context, host, command
|
||||||
return "", fmt.Errorf("ssh command failed: %w", err)
|
return "", fmt.Errorf("ssh command failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(output), nil
|
outputStr := strings.TrimSpace(string(output))
|
||||||
|
|
||||||
|
// Strip any leading SSH noise (e.g., "Warning: Permanently added ...") so sensors JSON parses cleanly.
|
||||||
|
if idx := strings.Index(outputStr, "{"); idx > 0 {
|
||||||
|
outputStr = outputStr[idx:]
|
||||||
|
}
|
||||||
|
if idx := strings.LastIndex(outputStr, "}"); idx != -1 && idx < len(outputStr)-1 {
|
||||||
|
outputStr = outputStr[:idx+1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputStr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseSensorsJSON parses the JSON output from `sensors -j`
|
// parseSensorsJSON parses the JSON output from `sensors -j`
|
||||||
|
|
|
||||||
|
|
@ -168,3 +168,25 @@ func (c *Client) GetTemperature(nodeHost string) (string, error) {
|
||||||
|
|
||||||
return tempStr, nil
|
return tempStr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequestCleanup signals the proxy to trigger host-side cleanup workflow.
|
||||||
|
func (c *Client) RequestCleanup(host string) error {
|
||||||
|
params := make(map[string]interface{}, 1)
|
||||||
|
if host != "" {
|
||||||
|
params["host"] = host
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.call("request_cleanup", params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !resp.Success {
|
||||||
|
if resp.Error != "" {
|
||||||
|
return fmt.Errorf("proxy error: %s", resp.Error)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("proxy rejected cleanup request")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,36 @@ print_success() {
|
||||||
echo -e "${GREEN}✓${NC} $1"
|
echo -e "${GREEN}✓${NC} $1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configure_local_authorized_key() {
|
||||||
|
local auth_line=$1
|
||||||
|
local auth_keys_file="/root/.ssh/authorized_keys"
|
||||||
|
local tmp_auth
|
||||||
|
|
||||||
|
tmp_auth=$(mktemp)
|
||||||
|
mkdir -p /root/.ssh
|
||||||
|
touch "$tmp_auth"
|
||||||
|
|
||||||
|
if [[ -f "$auth_keys_file" ]]; then
|
||||||
|
grep -vF '# pulse-managed-key' "$auth_keys_file" >"$tmp_auth" 2>/dev/null || true
|
||||||
|
chmod --reference="$auth_keys_file" "$tmp_auth" 2>/dev/null || chmod 600 "$tmp_auth"
|
||||||
|
chown --reference="$auth_keys_file" "$tmp_auth" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
chmod 600 "$tmp_auth"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "${auth_line}" >>"$tmp_auth"
|
||||||
|
if mv "$tmp_auth" "$auth_keys_file"; then
|
||||||
|
if [ "$QUIET" != true ]; then
|
||||||
|
print_success "SSH key configured on localhost"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
rm -f "$tmp_auth"
|
||||||
|
print_warn "Failed to configure SSH key on localhost"
|
||||||
|
print_info "Add this line manually to /root/.ssh/authorized_keys:"
|
||||||
|
print_info " ${auth_line}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# Check if running on Proxmox host
|
# Check if running on Proxmox host
|
||||||
if ! command -v pvecm >/dev/null 2>&1; then
|
if ! command -v pvecm >/dev/null 2>&1; then
|
||||||
print_error "This script must be run on a Proxmox VE host"
|
print_error "This script must be run on a Proxmox VE host"
|
||||||
|
|
@ -306,20 +336,41 @@ if command -v pvecm >/dev/null 2>&1; then
|
||||||
|
|
||||||
# Configure SSH key with forced command restriction
|
# Configure SSH key with forced command restriction
|
||||||
FORCED_CMD='command="sensors -j",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty'
|
FORCED_CMD='command="sensors -j",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty'
|
||||||
AUTH_LINE="${FORCED_CMD} ${PROXY_PUBLIC_KEY}"
|
AUTH_LINE="${FORCED_CMD} ${PROXY_PUBLIC_KEY} # pulse-managed-key"
|
||||||
|
|
||||||
# Track SSH key push results
|
# Track SSH key push results
|
||||||
SSH_SUCCESS_COUNT=0
|
SSH_SUCCESS_COUNT=0
|
||||||
SSH_FAILURE_COUNT=0
|
SSH_FAILURE_COUNT=0
|
||||||
declare -a SSH_FAILED_NODES=()
|
declare -a SSH_FAILED_NODES=()
|
||||||
|
LOCAL_IPS=$(hostname -I 2>/dev/null || echo "")
|
||||||
|
LOCAL_HOSTNAMES="$(hostname 2>/dev/null || echo "") $(hostname -f 2>/dev/null || echo "")"
|
||||||
|
LOCAL_HANDLED=false
|
||||||
|
|
||||||
# Push key to each cluster node
|
# Push key to each cluster node
|
||||||
for node_ip in $CLUSTER_NODES; do
|
for node_ip in $CLUSTER_NODES; do
|
||||||
print_info "Authorizing proxy key on node $node_ip..."
|
print_info "Authorizing proxy key on node $node_ip..."
|
||||||
|
|
||||||
|
IS_LOCAL=false
|
||||||
|
if [[ " $LOCAL_IPS " == *" $node_ip "* ]]; then
|
||||||
|
IS_LOCAL=true
|
||||||
|
fi
|
||||||
|
if [[ " $LOCAL_HOSTNAMES " == *" $node_ip "* ]]; then
|
||||||
|
IS_LOCAL=true
|
||||||
|
fi
|
||||||
|
if [[ "$node_ip" == "127.0.0.1" || "$node_ip" == "localhost" ]]; then
|
||||||
|
IS_LOCAL=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$IS_LOCAL" = true ]]; then
|
||||||
|
configure_local_authorized_key "$AUTH_LINE"
|
||||||
|
LOCAL_HANDLED=true
|
||||||
|
((SSH_SUCCESS_COUNT++))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
# Remove any existing proxy keys first
|
# Remove any existing proxy keys first
|
||||||
ssh -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=5 root@"$node_ip" \
|
ssh -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=5 root@"$node_ip" \
|
||||||
"sed -i '/pulse-sensor-proxy\$/d' /root/.ssh/authorized_keys" 2>/dev/null || true
|
"sed -i '/# pulse-managed-key\$/d' /root/.ssh/authorized_keys" 2>/dev/null || true
|
||||||
|
|
||||||
# Add new key with forced command
|
# Add new key with forced command
|
||||||
SSH_ERROR=$(ssh -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=5 root@"$node_ip" \
|
SSH_ERROR=$(ssh -o StrictHostKeyChecking=no -o BatchMode=yes -o ConnectTimeout=5 root@"$node_ip" \
|
||||||
|
|
@ -348,30 +399,22 @@ if command -v pvecm >/dev/null 2>&1; then
|
||||||
print_info "To retry failed nodes, re-run this script or manually run:"
|
print_info "To retry failed nodes, re-run this script or manually run:"
|
||||||
print_info " ssh root@<node> 'echo \"${AUTH_LINE}\" >> /root/.ssh/authorized_keys'"
|
print_info " ssh root@<node> 'echo \"${AUTH_LINE}\" >> /root/.ssh/authorized_keys'"
|
||||||
fi
|
fi
|
||||||
|
if [[ "$LOCAL_HANDLED" = false ]]; then
|
||||||
|
configure_local_authorized_key "$AUTH_LINE"
|
||||||
|
((SSH_SUCCESS_COUNT++))
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
# No cluster found - configure standalone node
|
# No cluster found - configure standalone node
|
||||||
print_info "No cluster detected, configuring standalone node..."
|
print_info "No cluster detected, configuring standalone node..."
|
||||||
|
|
||||||
# Configure SSH key with forced command restriction
|
# Configure SSH key with forced command restriction
|
||||||
FORCED_CMD='command="sensors -j",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty'
|
FORCED_CMD='command="sensors -j",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty'
|
||||||
AUTH_LINE="${FORCED_CMD} ${PROXY_PUBLIC_KEY}"
|
AUTH_LINE="${FORCED_CMD} ${PROXY_PUBLIC_KEY} # pulse-managed-key"
|
||||||
|
|
||||||
# Configure localhost
|
|
||||||
print_info "Authorizing proxy key on localhost..."
|
print_info "Authorizing proxy key on localhost..."
|
||||||
|
configure_local_authorized_key "$AUTH_LINE"
|
||||||
# Remove any existing proxy keys first
|
|
||||||
sed -i '/pulse-sensor-proxy$/d' /root/.ssh/authorized_keys 2>/dev/null || touch /root/.ssh/authorized_keys
|
|
||||||
|
|
||||||
# Add new key with forced command
|
|
||||||
if echo "${AUTH_LINE}" >> /root/.ssh/authorized_keys; then
|
|
||||||
print_success "SSH key configured on standalone node"
|
|
||||||
print_info ""
|
print_info ""
|
||||||
print_info "Standalone node configuration complete"
|
print_info "Standalone node configuration complete"
|
||||||
else
|
|
||||||
print_warn "Failed to configure SSH key on localhost"
|
|
||||||
print_info "Manually add this line to /root/.ssh/authorized_keys:"
|
|
||||||
print_info " ${AUTH_LINE}"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# Proxmox host but pvecm not available (shouldn't happen, but handle it)
|
# Proxmox host but pvecm not available (shouldn't happen, but handle it)
|
||||||
|
|
@ -380,14 +423,9 @@ else
|
||||||
|
|
||||||
# Configure localhost as fallback
|
# Configure localhost as fallback
|
||||||
FORCED_CMD='command="sensors -j",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty'
|
FORCED_CMD='command="sensors -j",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty'
|
||||||
AUTH_LINE="${FORCED_CMD} ${PROXY_PUBLIC_KEY}"
|
AUTH_LINE="${FORCED_CMD} ${PROXY_PUBLIC_KEY} # pulse-managed-key"
|
||||||
|
|
||||||
sed -i '/pulse-sensor-proxy$/d' /root/.ssh/authorized_keys 2>/dev/null || touch /root/.ssh/authorized_keys
|
configure_local_authorized_key "$AUTH_LINE"
|
||||||
if echo "${AUTH_LINE}" >> /root/.ssh/authorized_keys; then
|
|
||||||
print_success "SSH key configured on localhost"
|
|
||||||
else
|
|
||||||
print_warn "Failed to configure SSH key"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Ensure container mount via mp configuration
|
# Ensure container mount via mp configuration
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue