OPs-Agent repariert seine Praesenz nach dem naechsten Update selbst
Der OPs-Agent war seit seiner Registrierung am 31.08. nie praesent: seine monitor-api.conf stammt von vor Issue #75 und kennt MONITOR_AGENT_ID nicht, report_activity() in check_fds.sh meldete deshalb stillschweigend nichts - waehrend die Systemmeldungen mit demselben Key weiter ankamen. - API: jeder mit X-Api-Key authentifizierte Request frischt last_seen_at auf (AgentRepository::touch(), Zustand/Ticket bleiben). Greift auch, wenn auf dem Ping-Rechner noch ein aelteres check_fds.sh liegt. - API: neuer Endpunkt GET /api/v1/agents/me liefert den Agenten zum Key. - check_fds.sh: fehlt MONITOR_AGENT_ID, wird sie ueber /agents/me ermittelt und in monitor-api.conf nachgetragen (ohne Admin-Key). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F5rnDxZCwLYiEVyfrAvsAA
This commit is contained in:
parent
42fb7fbe08
commit
79d97985f3
9 changed files with 181 additions and 2 deletions
17
API.md
17
API.md
|
|
@ -130,6 +130,23 @@ allerdings nicht drin, da es erst bei der API-Antwort berechnet wird:
|
|||
sqlite3 "$MONITOR_DB_PATH" "SELECT id, display_name, last_seen_at FROM agents;"
|
||||
```
|
||||
|
||||
`last_seen_at` frischt nicht nur `POST /api/v1/agents/{id}/activity` auf, sondern **jeder**
|
||||
mit gueltigem `X-Api-Key` authentifizierte Request (`AgentRepository::touch()`, Zustand und
|
||||
Ticket bleiben dabei unveraendert). Wer sich mit seinem Key meldet, ist offensichtlich da - so
|
||||
bleibt z.B. der OPs-Agent eines `check_fds.sh` mit aelterer `monitor-api.conf` ohne
|
||||
`MONITOR_AGENT_ID` ueber seine Systemmeldungen praesent, obwohl er nie `activity` aufruft.
|
||||
|
||||
### `GET /api/v1/agents/me` — Eigenen Agenten abfragen
|
||||
|
||||
Auth: `X-Api-Key`. Response 200: `{ "agent": ‹Agent-Objekt, ohne api_key› }` des Agenten,
|
||||
dem der Key gehoert. Dient Clients, die zwar ihren Key, aber nicht ihre ID kennen:
|
||||
`check_fds.sh` traegt so eine in `monitor-api.conf` fehlende `MONITOR_AGENT_ID` selbst nach
|
||||
(ohne Admin-Key, den der Ping-Rechner nicht hat).
|
||||
|
||||
```bash
|
||||
curl -s "$MONITOR_API_BASE/agents/me" -H "X-Api-Key: $MONITOR_API_KEY" | jq '.agent.id'
|
||||
```
|
||||
|
||||
### `POST /api/v1/agents/{id}/activity` — Eigenen Zustand melden
|
||||
|
||||
Auth: `X-Api-Key` des betroffenen Agenten (ein Agent darf nur seinen eigenen Zustand
|
||||
|
|
|
|||
|
|
@ -169,8 +169,12 @@ Ping-Überwachung normal weiter und protokolliert nur eine Warnung.
|
|||
Zusätzlich meldet `check_fds.sh` bei jedem Lauf `working` zu Beginn und `idle` am Ende
|
||||
(`POST /api/v1/agents/{id}/activity`, benötigt `MONITOR_AGENT_ID`) — ohne diese Meldung fiel
|
||||
der OPs-Agent zehn Minuten nach der Registrierung dauerhaft aus der Präsenz, obwohl der
|
||||
Ping-Check längst lief (Issue #75). Fehlt `MONITOR_AGENT_ID`, bleibt nur diese Meldung aus,
|
||||
der Rest von `check_fds.sh` läuft unverändert weiter.
|
||||
Ping-Check längst lief (Issue #75). Fehlt `MONITOR_AGENT_ID` in einer älteren
|
||||
`monitor-api.conf`, ermittelt `check_fds.sh` die ID beim nächsten Lauf selbst über
|
||||
`GET /api/v1/agents/me` und trägt sie in die Datei nach — von Hand ist nichts zu tun. Unabhängig
|
||||
davon hält schon jede Systemmeldung mit dem OPs-Key den Agenten präsent (die API frischt
|
||||
`last_seen_at` bei jedem authentifizierten Request auf), selbst wenn auf dem Ping-Rechner noch
|
||||
ein älteres `check_fds.sh` liegt.
|
||||
|
||||
## ⚙️ Hosts über die Oberfläche pflegen
|
||||
|
||||
|
|
|
|||
|
|
@ -52,10 +52,12 @@ final class Application
|
|||
private readonly AgentAuthenticator $agentAuth;
|
||||
private readonly AdminAuthenticator $adminAuth;
|
||||
private readonly WebhookAuthenticator $webhookAuth;
|
||||
private readonly AgentRepository $agents;
|
||||
|
||||
public function __construct(private readonly Database $db, private readonly Config $config)
|
||||
{
|
||||
$agents = new AgentRepository($db, $config->deskCount());
|
||||
$this->agents = $agents;
|
||||
$tickets = new TicketRepository($db);
|
||||
$events = new EventRepository($db);
|
||||
$systems = new SystemRepository($db);
|
||||
|
|
@ -145,6 +147,12 @@ final class Application
|
|||
|
||||
$this->router->add('GET', '/api/v1/agents', fn (Request $r, array $p) => $agentController->index($r));
|
||||
$this->router->add('POST', '/api/v1/agents', fn (Request $r, array $p) => $agentController->register($r), 'admin');
|
||||
$this->router->add(
|
||||
'GET',
|
||||
'/api/v1/agents/me',
|
||||
fn (Request $r, array $p, array $agent) => $agentController->me($r, $agent),
|
||||
'agent'
|
||||
);
|
||||
$this->router->add(
|
||||
'POST',
|
||||
'/api/v1/agents/{id}/activity',
|
||||
|
|
@ -226,6 +234,8 @@ final class Application
|
|||
if ($agent === null) {
|
||||
return Response::error('Ungueltiger oder fehlender X-Api-Key-Header.', 401);
|
||||
}
|
||||
// Jeder gueltige Key-Request zaehlt als Lebenszeichen (siehe AgentRepository::touch()).
|
||||
$this->agents->touch((int) $agent['id']);
|
||||
} elseif ($match->auth === 'admin') {
|
||||
if (!$this->adminAuth->authenticate($request)) {
|
||||
return Response::error('Ungueltiger oder fehlender X-Admin-Key-Header.', 401);
|
||||
|
|
|
|||
|
|
@ -75,4 +75,17 @@ final class AgentController
|
|||
|
||||
return Response::json(['agent' => $agent]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Der zum X-Api-Key gehoerende Agent. check_fds.sh ermittelt daraus seine eigene ID, wenn
|
||||
* sie in einer aelteren monitor-api.conf fehlt - ohne Admin-Key, den der Ping-Rechner
|
||||
* nicht kennt. Neu gelesen statt $authenticatedAgent durchgereicht, weil der vor dem
|
||||
* Auffrischen von last_seen_at geladen wurde und is_present sonst veraltet waere.
|
||||
*
|
||||
* @param array<string, mixed> $authenticatedAgent
|
||||
*/
|
||||
public function me(Request $request, array $authenticatedAgent): Response
|
||||
{
|
||||
return Response::json(['agent' => $this->agents->findById((int) $authenticatedAgent['id'])]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,6 +108,18 @@ final class AgentRepository
|
|||
return array_map(fn (array $row) => $this->hidingKey($row), $stmt->fetchAll());
|
||||
}
|
||||
|
||||
/**
|
||||
* Frischt nur last_seen_at auf, Zustand und Ticket bleiben unveraendert. Application ruft
|
||||
* das bei jedem mit X-Api-Key authentifizierten Request auf: wer sich mit gueltigem Key
|
||||
* meldet, ist offensichtlich da - auch wenn er (wie ein aelterer check_fds.sh ohne
|
||||
* MONITOR_AGENT_ID) nie den Aktivitaets-Endpoint aufruft.
|
||||
*/
|
||||
public function touch(int $id): void
|
||||
{
|
||||
$stmt = $this->db->pdo()->prepare('UPDATE agents SET last_seen_at = :now WHERE id = :id');
|
||||
$stmt->execute(['now' => $this->db->now(), 'id' => $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein unbekanntes Ticket wuerde sonst erst in der FOREIGN-KEY-Constraint auflaufen und
|
||||
* als ungefangene PDOException beim Client landen - deshalb hier pruefen und als
|
||||
|
|
|
|||
|
|
@ -99,6 +99,64 @@ final class ApplicationTest extends TestCase
|
|||
self::assertStringNotContainsString('PDOException', json_encode($response->body()));
|
||||
}
|
||||
|
||||
public function testAgentsMeRequiresAgentKey(): void
|
||||
{
|
||||
$response = $this->app->handle(Request::create('GET', '/api/v1/agents/me'));
|
||||
|
||||
self::assertSame(401, $response->status);
|
||||
}
|
||||
|
||||
public function testAgentsMeReturnsOwnId(): void
|
||||
{
|
||||
$registerResponse = $this->app->handle(Request::create(
|
||||
'POST',
|
||||
'/api/v1/agents',
|
||||
body: ['display_name' => 'OPs', 'role' => 'ops'],
|
||||
headers: ['X-Admin-Key' => 'admin-secret']
|
||||
));
|
||||
|
||||
$response = $this->app->handle(Request::create(
|
||||
'GET',
|
||||
'/api/v1/agents/me',
|
||||
headers: ['X-Api-Key' => $registerResponse->body()['api_key']]
|
||||
));
|
||||
|
||||
self::assertSame(200, $response->status);
|
||||
self::assertSame($registerResponse->body()['agent']['id'], $response->body()['agent']['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Regression: ein check_fds.sh mit monitor-api.conf ohne MONITOR_AGENT_ID meldete alle fuenf
|
||||
* Minuten Systemstatus mit dem OPs-Key, rief aber nie den Aktivitaets-Endpoint auf - der
|
||||
* OPs-Agent blieb dadurch seit seiner Registrierung dauerhaft abwesend.
|
||||
*/
|
||||
public function testAnyAuthenticatedAgentRequestRefreshesPresence(): void
|
||||
{
|
||||
$db = Database::inMemory();
|
||||
$app = new Application($db, new Config(['MONITOR_ADMIN_API_KEY' => 'admin-secret', 'MONITOR_STATUS_FILE' => '/does/not/exist']));
|
||||
$registerResponse = $app->handle(Request::create(
|
||||
'POST',
|
||||
'/api/v1/agents',
|
||||
body: ['display_name' => 'OPs', 'role' => 'ops'],
|
||||
headers: ['X-Admin-Key' => 'admin-secret']
|
||||
));
|
||||
$agentId = $registerResponse->body()['agent']['id'];
|
||||
|
||||
$db->pdo()->prepare('UPDATE agents SET last_seen_at = :ts WHERE id = :id')
|
||||
->execute(['ts' => gmdate('Y-m-d\TH:i:s\Z', time() - 601), 'id' => $agentId]);
|
||||
self::assertFalse($app->handle(Request::create('GET', '/api/v1/agents'))->body()['agents'][0]['is_present']);
|
||||
|
||||
$statusResponse = $app->handle(Request::create(
|
||||
'POST',
|
||||
'/api/v1/systems/homeassistant/status',
|
||||
body: ['status' => 'ok', 'name' => 'homeassistant'],
|
||||
headers: ['X-Api-Key' => $registerResponse->body()['api_key']]
|
||||
));
|
||||
self::assertSame(200, $statusResponse->status);
|
||||
|
||||
self::assertTrue($app->handle(Request::create('GET', '/api/v1/agents'))->body()['agents'][0]['is_present']);
|
||||
}
|
||||
|
||||
public function testPatchUnknownTicketReturns404(): void
|
||||
{
|
||||
$registerResponse = $this->app->handle(Request::create(
|
||||
|
|
|
|||
|
|
@ -40,6 +40,18 @@ final class AgentControllerTest extends TestCase
|
|||
}
|
||||
}
|
||||
|
||||
public function testMeReturnsAuthenticatedAgentWithoutApiKeyHash(): void
|
||||
{
|
||||
$this->repository->register('Alice', 'entwickler');
|
||||
$ops = $this->repository->register('OPs', 'ops');
|
||||
|
||||
$response = $this->controller->me(Request::create('GET', '/api/v1/agents/me'), $ops['agent']);
|
||||
|
||||
self::assertSame(200, $response->status);
|
||||
self::assertSame($ops['agent']['id'], $response->body()['agent']['id']);
|
||||
self::assertArrayNotHasKey('api_key_hash', $response->body()['agent']);
|
||||
}
|
||||
|
||||
public function testRegisterRejectsMissingDisplayName(): void
|
||||
{
|
||||
$response = $this->controller->register(Request::create('POST', '/api/v1/agents', body: ['role' => 'entwickler']));
|
||||
|
|
|
|||
|
|
@ -157,4 +157,21 @@ final class AgentRepositoryTest extends TestCase
|
|||
$agent = $this->repository->findById($id);
|
||||
self::assertFalse($agent['is_present']);
|
||||
}
|
||||
|
||||
public function testTouchRefreshesPresenceWithoutChangingState(): void
|
||||
{
|
||||
$result = $this->repository->register('OPs', 'ops');
|
||||
$id = (int) $result['agent']['id'];
|
||||
$this->repository->updateActivity($id, 'auth_expiring', null);
|
||||
|
||||
$staleTimestamp = gmdate('Y-m-d\TH:i:s\Z', time() - 601);
|
||||
$this->db->pdo()->prepare('UPDATE agents SET last_seen_at = :ts WHERE id = :id')
|
||||
->execute(['ts' => $staleTimestamp, 'id' => $id]);
|
||||
|
||||
$this->repository->touch($id);
|
||||
|
||||
$agent = $this->repository->findById($id);
|
||||
self::assertTrue($agent['is_present']);
|
||||
self::assertSame('auth_expiring', $agent['state']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
36
check_fds.sh
36
check_fds.sh
|
|
@ -26,6 +26,41 @@ if [ -f "$API_CONF" ]; then
|
|||
. "$API_CONF"
|
||||
fi
|
||||
|
||||
# Aeltere monitor-api.conf (vor Issue #75) kennen MONITOR_AGENT_ID nicht - dann blieb die
|
||||
# Zustandsmeldung unten stillschweigend aus. Die ID wird hier ueber GET /api/v1/agents/me mit dem
|
||||
# eigenen Key ermittelt und in die Datei nachgetragen, damit sich der Ping-Rechner nach einem
|
||||
# Update selbst repariert, ohne dass jemand register-ops-agent.sh (braucht den Admin-Key) laufen
|
||||
# laesst. Schlaegt das fehl, laeuft die Ueberwachung unveraendert weiter.
|
||||
resolve_agent_id() {
|
||||
if [ -n "$MONITOR_AGENT_ID" ] || [ -z "$MONITOR_API_BASE" ] || [ -z "$MONITOR_API_KEY" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local response id
|
||||
if ! response=$(curl -s --fail --max-time 10 \
|
||||
-H "X-Api-Key: ${MONITOR_API_KEY}" \
|
||||
"${MONITOR_API_BASE%/}/api/v1/agents/me"); then
|
||||
echo " Warnung: MONITOR_AGENT_ID fehlt und liess sich nicht von der API ermitteln, Zustand wird nicht gemeldet."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Das erste "id" der Antwort {"agent":{"id":2,...}} ist die Agenten-ID.
|
||||
id=$(printf '%s' "$response" | grep -oE '"id": *[0-9]+' | head -n1 | grep -oE '[0-9]+' || true)
|
||||
if [ -z "$id" ]; then
|
||||
echo " Warnung: MONITOR_AGENT_ID fehlt, die API lieferte keine Agenten-ID."
|
||||
return 0
|
||||
fi
|
||||
|
||||
MONITOR_AGENT_ID="$id"
|
||||
# Angehaengt statt ersetzt: eine spaetere Zuweisung gewinnt beim naechsten Sourcen auch
|
||||
# gegen ein vorhandenes MONITOR_AGENT_ID="".
|
||||
if printf '\n# Von check_fds.sh nachgetragen (fehlte, Issue #75)\nMONITOR_AGENT_ID="%s"\n' "$id" >> "$API_CONF" 2>/dev/null; then
|
||||
echo " MONITOR_AGENT_ID fehlte in $API_CONF und wurde nachgetragen (ID $id)."
|
||||
else
|
||||
echo " Warnung: MONITOR_AGENT_ID ($id) liess sich nicht in $API_CONF schreiben, gilt nur fuer diesen Lauf."
|
||||
fi
|
||||
}
|
||||
|
||||
# Issue #70: Die Liste der ueberwachten Hosts wird im Frontend gepflegt (Zahnrad am
|
||||
# Wandmonitor "Systeme"). Dieser Aufruf holt sie vor jedem Durchlauf ab und schreibt daraus
|
||||
# die lokale hosts.conf - deshalb muss auf diesem Rechner nichts mehr von Hand editiert
|
||||
|
|
@ -221,6 +256,7 @@ echo "Pruefung: $(date)"
|
|||
# hosts.conf/mail_fds.py weiter unten) nicht dauerhaft als "working" haengen bleibt -
|
||||
# analog zum Cron-Skript des Entwickler-Agenten, das ebenfalls working/idle um seinen Lauf
|
||||
# herum meldet (Issue #75).
|
||||
resolve_agent_id
|
||||
trap 'report_activity idle' EXIT
|
||||
report_activity working
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue