mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
fix(assistant): retain monitored targets without command access
Resolve monitored topology before checking command connections so unavailable inspection retains the known resource and parent node. Prevent known targets from falling through to a colliding agent ID, and preserve the single-agent requirement when no target is supplied. Use one failed tool envelope for diagnostic reads and file mutations. Missing connections neither prove an installation problem nor count as successful writes. Hypervisor lifecycle authority remains on its canonical action path. Verify disconnected and unknown targets, collision isolation, all affected tool handlers, token/WebSocket scope boundaries, and the linked Patrol and Assistant failure journey at desktop and narrow widths.
This commit is contained in:
@@ -1129,3 +1129,86 @@ comparisons, gates and logs copied to `tmp/patrol-named-binding-final-proof/` at
|
||||
the workspace root. This establishes the selected local performance proof,
|
||||
not a completed remote CI pass. Final staged-hook qualification and landing
|
||||
remain pending.
|
||||
|
||||
|
||||
### Missing diagnostic access preserves monitored identity
|
||||
|
||||
The named-binding correction is committed as `1f41fa174d7d` and pushed to PR
|
||||
#1928 after the exact five-file staged hook passed 163 tests in 127.438s.
|
||||
This supersedes the pending-hook statement above. Remote landing remains open.
|
||||
|
||||
The missing-access case reproduced a shared routing defect before any new
|
||||
infrastructure fault was introduced. With no connected command agents, the
|
||||
resolver returned before consulting monitoring topology. Known hosts, VMs and
|
||||
system containers consequently lost their kind, parent and required transport.
|
||||
The regression failed for all three known targets in the original source.
|
||||
|
||||
The shared resolver now retains topology when no server or connection exists.
|
||||
A known target cannot fall through to an unrelated agent with a colliding ID.
|
||||
No-target routing still requires exactly one connected agent. The file-read,
|
||||
file-write, file-append, read-only execution and retained legacy command handlers
|
||||
return the existing NO_AGENT failure envelope, including known resource kind and
|
||||
parent node when available. The requested operation did not run. Missing access
|
||||
no longer produces a successful file-write result or unsupported installation
|
||||
advice. Absence of a connection does not establish a policy denial, missing
|
||||
installation, guest capability, fresh observation or healthy workload.
|
||||
|
||||
This concerns diagnostic command access. Advertised Proxmox lifecycle actions
|
||||
retain their canonical hypervisor action authority and do not acquire an
|
||||
in-guest diagnostic prerequisite. Issue #1782's full body and two comments were
|
||||
read as adjacent evidence of the customer harm caused by invented prerequisites.
|
||||
Its requested reporter confirmation remains outstanding. No comment was sent.
|
||||
|
||||
The final full tools package passes on pulse-dev with Go 1.26.8 in 59.437s.
|
||||
Targeted local regression passes on Go 1.27.1 in 0.538s. Worker source hashes are
|
||||
unchanged across the full-package proof. Private reproduction, source manifest
|
||||
and browser-result export are under `tmp/patrol-access-routing-proof/` at the
|
||||
workspace root. The export invokes actual current tool handlers with controlled
|
||||
connection fixtures. Its initial connected mock lacked a GetConnectedAgents
|
||||
expectation and failed before export. The corrected export passes in 0.513s.
|
||||
Browser fixtures prove rendering only. They do not qualify model judgment or
|
||||
real command-scope enforcement. Final browser, race and staged proofs follow.
|
||||
|
||||
|
||||
Final focused race proof explicitly runs all five new/missing-read tests and
|
||||
passes in 1.051s on Go 1.26.8. An earlier broader name pattern passed but omitted
|
||||
three newly named routing tests, so it is not used to claim their race coverage.
|
||||
Existing API regressions for implicit monitoring-token scope and real WebSocket
|
||||
rejection without agent-exec scope also pass in 0.122s. These preserve the access
|
||||
boundary. They are not a model-led missing-access investigation.
|
||||
|
||||
Playwright and pixel inspection pass for known disconnected target, unknown
|
||||
target and ordinary failed-read controls at `/patrol`, 1440x1000, 900x1000 and
|
||||
390x1000. The 18 cases cover ordinary and mirrored findings, keyboard review,
|
||||
completed-but-unresolved records, readable error evidence, expanded investigation
|
||||
transcripts, collapse, linked Assistant explanation and deepest failed-tool
|
||||
input/output, Escape and context-only reopen, plus reload without resubmission.
|
||||
Nine chat requests are intercepted, with no infrastructure writes. The backend
|
||||
results are serialized from actual final-source handlers with controlled
|
||||
connection/command fixtures. Model conclusions and transport are scripted.
|
||||
The generic Patrol toolbar opens context-only Assistant, so the proof checks
|
||||
no automatic submission there rather than expecting a persisted issue session.
|
||||
One permission-control pass exhausted a five-second wait while capabilities
|
||||
were still loading. Its complete matrix passes with a twenty-second request
|
||||
wait. This does not establish a latency SLO. Final receipts/screenshots are in
|
||||
`tmp/patrol-access-browser-proof/{known-vm,unknown-target,permission-denied}/`
|
||||
at the workspace root. No frontend runtime source changed.
|
||||
|
||||
The final routing source hashes are:
|
||||
|
||||
- `internal/ai/tools/tools_control.go`: `f6ac2e05542657a1d859196b06c3a68288e08b436db6dabf4f691ded744dc751`
|
||||
|
||||
- `internal/ai/tools/tools_file.go`: `c42002e1bf012024e1b38aa64738e0a8d9e0609699a49362675e01e7891d763e`
|
||||
|
||||
- `internal/ai/tools/tools_read.go`: `643a02f70432adff75355ef3083d84ab24f43c3b811bba7730cfca2ca4fd8de7`
|
||||
|
||||
- `internal/ai/tools/strict_resolution_test.go`: `7f88e16933c0b91b1ef7754cb7df977501fdc7278540a6ec70c1c4283e08c62d`
|
||||
|
||||
- `internal/ai/tools/file_docker_test.go`: `6bb8f5df1291175b9f7c52dd863293bfe89a4b189ef3ed79daeaf2f8d7adb45b`
|
||||
|
||||
|
||||
The missing-access slice is ready for its exact staged hook. The broader goal
|
||||
remains open for real-model diagnosis and approved/rejected action outcomes.
|
||||
On head `1f41fa174d7d`, governance, all eight Core E2E shards and CodeQL pass.
|
||||
Build and Test run `34015148620` is still pending with no jobs, so omitted PR
|
||||
checks are not treated as success. PR #1928 remains open with auto-merge enabled.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -8030,3 +8030,16 @@ disallowed. Streaming DOM reconciliation preserves table region identity, focus
|
||||
and scroll position. The conversation itself must not acquire horizontal scroll
|
||||
from wide answer tables. Browser proof covers a persisted real answer and
|
||||
explicit renderer fixtures at 1440, 900 and 390 pixel widths.
|
||||
|
||||
|
||||
### Monitoring identity without command access
|
||||
|
||||
Command routing resolves monitored topology independently of agent connection
|
||||
availability. A missing connection must preserve the known target kind and
|
||||
parent node, cannot fall back to a different agent with a colliding name or ID,
|
||||
and cannot dispatch a command. No-target routing requires exactly one connection.
|
||||
Diagnostic reads and file mutations return the same NO_AGENT failure envelope
|
||||
when no command connection is available. Failure is distinct from an established
|
||||
permission denial. Do not infer an installation requirement or guest capability
|
||||
from this absence. Advertised hypervisor lifecycle capabilities remain governed
|
||||
by their canonical action path, independently of diagnostic command access.
|
||||
|
||||
@@ -151,7 +151,7 @@ func TestExecuteFileReadFailureStatus(t *testing.T) {
|
||||
result, err := executor.executeFileRead(context.Background(), "/proc/meminfo", "delly2", "")
|
||||
require.NoError(t, err)
|
||||
require.True(t, result.IsError, "missing agent must not count as a successful read")
|
||||
assert.Contains(t, result.Content[0].Text, "No agent found")
|
||||
assert.Contains(t, result.Content[0].Text, "No command connection is available")
|
||||
agent.AssertNotCalled(t, "ExecuteCommand", mock.Anything, mock.Anything, mock.Anything)
|
||||
})
|
||||
|
||||
@@ -570,3 +570,62 @@ func TestExecuteFileEditDockerNestedRouting(t *testing.T) {
|
||||
mockAgent.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
|
||||
// Missing access is a failed operation, independent of whether monitoring knows
|
||||
// the target. Exercise the actual tool handlers so writes cannot report success.
|
||||
func TestCommandToolsPreserveMonitoringWhenAccessUnavailable(t *testing.T) {
|
||||
t.Setenv("PULSE_STRICT_RESOLUTION", "false")
|
||||
for _, target := range []string{"known-vm", "unknown-target"} {
|
||||
for _, noServer := range []bool{false, true} {
|
||||
for _, operation := range []string{"file read", "file write", "file append", "read exec", "legacy command"} {
|
||||
t.Run(fmt.Sprintf("%s/noServer=%t/%s", target, noServer, operation), func(t *testing.T) {
|
||||
server := &mockAgentServer{}
|
||||
config := ExecutorConfig{StateProvider: &mockStateProvider{state: models.StateSnapshot{
|
||||
VMs: []models.VM{{ID: "vm-101", VMID: 101, Name: "known-vm", Node: "known-node", Status: "running"}},
|
||||
}}}
|
||||
if !noServer {
|
||||
config.AgentServer = server
|
||||
}
|
||||
executor := NewPulseToolExecutor(config)
|
||||
ctx := context.Background()
|
||||
var result CallToolResult
|
||||
var err error
|
||||
switch operation {
|
||||
case "file read":
|
||||
result, err = executor.executeFileRead(ctx, "/proc/meminfo", target, "")
|
||||
case "file write":
|
||||
result, err = executor.executeFileWrite(ctx, "/tmp/access-proof", "value", target, "", nil)
|
||||
case "file append":
|
||||
result, err = executor.executeFileAppend(ctx, "/tmp/access-proof", "value", target, "", nil)
|
||||
case "read exec":
|
||||
result, err = executor.executeReadExec(ctx, map[string]interface{}{"command": "uptime", "target_host": target})
|
||||
case "legacy command":
|
||||
result, err = executor.executeRunCommand(ctx, map[string]interface{}{"command": "uptime", "target_host": target})
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.True(t, result.IsError)
|
||||
require.Len(t, result.Content, 1)
|
||||
var response ToolResponse
|
||||
require.NoError(t, json.Unmarshal([]byte(result.Content[0].Text), &response))
|
||||
require.False(t, response.OK)
|
||||
require.NotNil(t, response.Error)
|
||||
require.Equal(t, ErrCodeNoAgent, response.Error.Code)
|
||||
require.True(t, response.Error.Failed)
|
||||
require.False(t, response.Error.Blocked, "an absent connection does not prove a policy denial")
|
||||
require.Equal(t, target, response.Error.Details["target"])
|
||||
require.Contains(t, response.Error.Message, "did not run")
|
||||
require.NotContains(t, response.Error.Message, "Install")
|
||||
if target == "known-vm" {
|
||||
require.Equal(t, "vm", response.Error.Details["resource_kind"])
|
||||
require.Equal(t, "known-node", response.Error.Details["parent_node"])
|
||||
require.Contains(t, response.Error.Message, "Pulse monitoring knows")
|
||||
} else {
|
||||
require.NotContains(t, response.Error.Details, "resource_kind")
|
||||
require.NotContains(t, response.Error.Message, "Pulse monitoring knows")
|
||||
}
|
||||
server.AssertNotCalled(t, "ExecuteCommand", mock.Anything, mock.Anything, mock.Anything)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2135,3 +2135,69 @@ func TestExecutionIntent_TelemetryCategories(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandRoutingPreservesKnownTargetWithoutConnections(t *testing.T) {
|
||||
state := models.StateSnapshot{
|
||||
Hosts: []models.Host{{ID: "reported-host", Hostname: "monitored-host", CommandsEnabled: false}},
|
||||
Nodes: []models.Node{{ID: "node-1", Name: "known-node"}},
|
||||
VMs: []models.VM{{ID: "vm-101", VMID: 101, Name: "known-vm", Node: "known-node", Status: "running"}},
|
||||
Containers: []models.Container{{ID: "lxc-102", VMID: 102, Name: "known-lxc", Node: "known-node", Status: "running"}},
|
||||
}
|
||||
for _, unavailable := range []string{"no server", "no connections", "unrelated connection"} {
|
||||
t.Run(unavailable, func(t *testing.T) {
|
||||
config := ExecutorConfig{StateProvider: &mockStateProvider{state: state}}
|
||||
if unavailable != "no server" {
|
||||
server := &mockAgentServer{}
|
||||
if unavailable == "unrelated connection" {
|
||||
server.agents = []agentexec.ConnectedAgent{{AgentID: "unrelated", Hostname: "other-host"}}
|
||||
}
|
||||
config.AgentServer = server
|
||||
}
|
||||
executor := NewPulseToolExecutor(config)
|
||||
for _, tc := range []struct{ target, kind, transport, node, id string }{
|
||||
{"monitored-host", "agent", "direct", "", ""},
|
||||
{"known-node", "node", "direct", "known-node", ""},
|
||||
{"known-vm", "vm", "qm_guest_exec", "known-node", "101"},
|
||||
{"known-lxc", "system-container", "pct_exec", "known-node", "102"},
|
||||
} {
|
||||
t.Run(tc.target, func(t *testing.T) {
|
||||
if loc := executor.resolveResourceLocation(tc.target); !loc.Found {
|
||||
t.Fatal("fixture target is not known to monitoring")
|
||||
}
|
||||
got := executor.resolveTargetForCommandFull(tc.target)
|
||||
if got.AgentID != "" {
|
||||
t.Fatalf("unavailable connection admitted agent %q", got.AgentID)
|
||||
}
|
||||
if got.ResolvedKind != tc.kind || got.Transport != tc.transport || got.ResolvedNode != tc.node || got.TargetID != tc.id {
|
||||
t.Fatalf("known target lost when command connection is absent: %+v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandRoutingWithoutTargetRequiresOneConnection(t *testing.T) {
|
||||
for _, count := range []int{0, 1, 2} {
|
||||
server := &mockAgentServer{}
|
||||
for i := 0; i < count; i++ {
|
||||
server.agents = append(server.agents, agentexec.ConnectedAgent{AgentID: "available", Hostname: "host"})
|
||||
}
|
||||
executor := NewPulseToolExecutor(ExecutorConfig{AgentServer: server})
|
||||
got := executor.resolveTargetForCommandFull("")
|
||||
if (got.AgentID != "") != (count == 1) {
|
||||
t.Fatalf("%d connections selected %+v", count, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommandRoutingKnownHostDoesNotFallBackToCollidingID(t *testing.T) {
|
||||
executor := NewPulseToolExecutor(ExecutorConfig{
|
||||
StateProvider: &mockStateProvider{state: models.StateSnapshot{Hosts: []models.Host{{ID: "reported-host", Hostname: "monitored-host"}}}},
|
||||
AgentServer: &mockAgentServer{agents: []agentexec.ConnectedAgent{{AgentID: "monitored-host", Hostname: "different-host"}}},
|
||||
})
|
||||
got := executor.resolveTargetForCommandFull("monitored-host")
|
||||
if got.AgentID != "" || got.ResolvedKind != "agent" {
|
||||
t.Fatalf("known disconnected host routed through colliding agent ID: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,23 +359,10 @@ func (e *PulseToolExecutor) executeRunCommand(ctx context.Context, args map[stri
|
||||
}
|
||||
}
|
||||
|
||||
// Execute via agent server
|
||||
if e.agentServer == nil {
|
||||
return NewErrorResult(fmt.Errorf("no agent server available")), nil
|
||||
}
|
||||
|
||||
// Resolve target to the correct agent and routing info (with full provenance)
|
||||
// If targetHost is a container/VM name, this routes to the host node agent
|
||||
// with the correct TargetType and TargetID for pct exec / qm guest exec
|
||||
// Resolve topology even when no command connection is available.
|
||||
routing := e.resolveTargetForCommandFull(targetHost)
|
||||
if routing.AgentID == "" {
|
||||
if targetHost != "" {
|
||||
if routing.TargetType == "container" || routing.TargetType == "vm" {
|
||||
return NewErrorResult(fmt.Errorf("'%s' is a %s but no agent is available on its host node; install Pulse Unified Agent on the node", targetHost, routing.TargetType)), nil
|
||||
}
|
||||
return NewErrorResult(fmt.Errorf("no agent available for target '%s'. %s", targetHost, formatAvailableAgentHosts(e.agentServer.GetConnectedAgents()))), nil
|
||||
}
|
||||
return NewErrorResult(fmt.Errorf("no agent available for target")), nil
|
||||
return unavailableCommandConnection(targetHost, routing), nil
|
||||
}
|
||||
|
||||
approvalTargetType, approvalTargetID, approvalTargetName := approvalTargetForCommand(targetHost, routing)
|
||||
@@ -743,18 +730,14 @@ func (e *PulseToolExecutor) resolveTargetForCommandFull(targetHost string) Comma
|
||||
Transport: "direct",
|
||||
}
|
||||
|
||||
if e.agentServer == nil {
|
||||
return result
|
||||
}
|
||||
|
||||
agents := e.agentServer.GetConnectedAgents()
|
||||
if len(agents) == 0 {
|
||||
return result
|
||||
var agents []agentexec.ConnectedAgent
|
||||
if e.agentServer != nil {
|
||||
agents = e.agentServer.GetConnectedAgents()
|
||||
}
|
||||
|
||||
if targetHost == "" {
|
||||
// No target_host specified - require exactly one agent or fail
|
||||
if len(agents) > 1 {
|
||||
if len(agents) != 1 {
|
||||
return result
|
||||
}
|
||||
result.AgentID = agents[0].AgentID
|
||||
@@ -769,6 +752,7 @@ func (e *PulseToolExecutor) resolveTargetForCommandFull(targetHost string) Comma
|
||||
loc := e.resolveResourceLocation(targetHost)
|
||||
|
||||
if loc.Found {
|
||||
result.ResolvedKind = loc.ResourceType
|
||||
// Route based on resource type
|
||||
switch loc.ResourceType {
|
||||
case "agent":
|
||||
@@ -879,6 +863,8 @@ func (e *PulseToolExecutor) resolveTargetForCommandFull(targetHost string) Comma
|
||||
}
|
||||
}
|
||||
}
|
||||
// A known resource must never fall through to a different hostname match.
|
||||
return result
|
||||
}
|
||||
|
||||
// STEP 2: FALLBACK — agent hostname match.
|
||||
@@ -896,6 +882,31 @@ func (e *PulseToolExecutor) resolveTargetForCommandFull(targetHost string) Comma
|
||||
return result
|
||||
}
|
||||
|
||||
// unavailableCommandConnection preserves monitoring context without inferring
|
||||
// installation, permission, or guest capability from an absent connection.
|
||||
func unavailableCommandConnection(target string, routing CommandRoutingResult) CallToolResult {
|
||||
message := "No command connection is available. Specify a target with diagnostic access."
|
||||
details := map[string]any{"target": target}
|
||||
if target != "" {
|
||||
message = fmt.Sprintf("No command connection is available for %q. Check its diagnostic access and connection status.", target)
|
||||
}
|
||||
if routing.ResolvedKind != "" {
|
||||
details["resource_kind"] = routing.ResolvedKind
|
||||
message = fmt.Sprintf("Pulse monitoring knows %q, but no command connection is available for this target.", target)
|
||||
if routing.ResolvedNode != "" {
|
||||
details["parent_node"] = routing.ResolvedNode
|
||||
message += fmt.Sprintf(" Check diagnostic access and connection status on its node %q.", routing.ResolvedNode)
|
||||
} else {
|
||||
message += " Check its diagnostic access and connection status."
|
||||
}
|
||||
}
|
||||
message += " The requested operation did not run."
|
||||
return NewToolResponseResult(ToolResponse{
|
||||
OK: false,
|
||||
Error: &ToolError{Code: ErrCodeNoAgent, Message: message, Failed: true, Details: details},
|
||||
})
|
||||
}
|
||||
|
||||
// resolveTargetForCommand resolves a target_host to the correct agent and routing info.
|
||||
// Uses the authoritative resolveResourceLocation function.
|
||||
// Returns: agentID, targetType ("agent", "container", or "vm"), targetID (vmid for LXC/VM)
|
||||
|
||||
@@ -153,9 +153,6 @@ func (e *PulseToolExecutor) executeFileEdit(ctx context.Context, args map[string
|
||||
|
||||
// executeFileRead reads a file's contents
|
||||
func (e *PulseToolExecutor) executeFileRead(ctx context.Context, path, targetHost, dockerContainer string) (CallToolResult, error) {
|
||||
if e.agentServer == nil {
|
||||
return NewErrorResult(fmt.Errorf("no agent server available")), nil
|
||||
}
|
||||
|
||||
if blocked, reason := safety.IsSensitivePath(path); blocked {
|
||||
return NewToolResponseResult(NewToolBlockedError(
|
||||
@@ -179,10 +176,7 @@ func (e *PulseToolExecutor) executeFileRead(ctx context.Context, path, targetHos
|
||||
// Use full routing resolution - includes provenance for debugging
|
||||
routing := e.resolveTargetForCommandFull(targetHost)
|
||||
if routing.AgentID == "" {
|
||||
if routing.TargetType == "container" || routing.TargetType == "vm" {
|
||||
return NewErrorResult(fmt.Errorf("'%s' is a %s but no agent is available on its host node. Install Pulse Unified Agent on the node.", targetHost, routing.TargetType)), nil
|
||||
}
|
||||
return NewErrorResult(fmt.Errorf("No agent found for host '%s'. Check that the hostname is correct and an agent is connected.", targetHost)), nil
|
||||
return unavailableCommandConnection(targetHost, routing), nil
|
||||
}
|
||||
|
||||
var command string
|
||||
@@ -274,9 +268,6 @@ func (e *PulseToolExecutor) executeFileWrite(ctx context.Context, path, content,
|
||||
// blocking, routing + resolved-resource validation, approval gating, base64
|
||||
// transfer, audited execution, and post-write verification.
|
||||
func (e *PulseToolExecutor) executeFileMutation(ctx context.Context, path, content, targetHost, dockerContainer string, args map[string]interface{}, spec fileMutationSpec) (CallToolResult, error) {
|
||||
if e.agentServer == nil {
|
||||
return NewErrorResult(fmt.Errorf("no agent server available")), nil
|
||||
}
|
||||
approvalID := agentcapabilities.ApprovalArgument(args)
|
||||
|
||||
if blocked, reason := safety.IsSensitivePath(path); blocked {
|
||||
@@ -310,10 +301,7 @@ func (e *PulseToolExecutor) executeFileMutation(ctx context.Context, path, conte
|
||||
// Use full routing resolution - includes provenance for debugging
|
||||
routing := e.resolveTargetForCommandFull(targetHost)
|
||||
if routing.AgentID == "" {
|
||||
if routing.TargetType == "container" || routing.TargetType == "vm" {
|
||||
return NewTextResult(fmt.Sprintf("'%s' is a %s but no agent is available on its host node. Install Pulse Unified Agent on the node.", targetHost, routing.TargetType)), nil
|
||||
}
|
||||
return NewTextResult(fmt.Sprintf("No agent found for host '%s'. Check that the hostname is correct and an agent is connected.", targetHost)), nil
|
||||
return unavailableCommandConnection(targetHost, routing), nil
|
||||
}
|
||||
|
||||
// INVARIANT: If the target resolves to a child resource (container/VM), writes MUST execute
|
||||
|
||||
@@ -188,17 +188,10 @@ func (e *PulseToolExecutor) executeReadExec(ctx context.Context, args map[string
|
||||
return NewToolResponseResult(validation.StrictError.ToToolResponse()), nil
|
||||
}
|
||||
|
||||
if e.agentServer == nil {
|
||||
return NewErrorResult(fmt.Errorf("no agent server available")), nil
|
||||
}
|
||||
|
||||
// Resolve target to the correct agent and routing info (with full provenance)
|
||||
routing := e.resolveTargetForCommandFull(targetHost)
|
||||
if routing.AgentID == "" {
|
||||
if routing.TargetType == "container" || routing.TargetType == "vm" {
|
||||
return NewErrorResult(fmt.Errorf("'%s' is a %s but no agent is available on its Proxmox host", targetHost, routing.TargetType)), nil
|
||||
}
|
||||
return NewErrorResult(fmt.Errorf("no agent available for target '%s'. %s", targetHost, formatAvailableAgentHosts(e.agentServer.GetConnectedAgents()))), nil
|
||||
return unavailableCommandConnection(targetHost, routing), nil
|
||||
}
|
||||
|
||||
// Build command (with optional Docker wrapper)
|
||||
|
||||
Reference in New Issue
Block a user