#5768 "A maintenance cadence: a group's updates and reboots, one host at a time, on a schedule". A plan names an inventory group, a weekday and time in a time zone, a window and a reboot policy (only when required by default). At its slot a run takes the group's hosts in order and gives each a #5767 maintenance job. The next host starts only when the last is back in service: its agent is posting and, for a swarm node, the node is active and every replicated service is at its replicas. A failure stops the run, and no host is started once the window has passed. A host's latest failure shows as host_agent/<host>/maintenance_failed, for an ordinary alert rule. The page is Hosts → Maintenance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
85 lines
5.1 KiB
HTML
85 lines
5.1 KiB
HTML
{% extends "base.html" %}
|
|
{# Hosts, problems first (host_svc.host_rank): a down monitor, then an agent
|
|
gone quiet, then monitors not yet checked, then the healthy. Status says
|
|
what is wrong and for how long; editing and deleting a host happen on its
|
|
own page (#5281). Uptime is one figure, 30 days, with the shorter windows
|
|
on hover: Monitors is where uptime is read in full (#5280). Add host is a
|
|
dialog over this list (#5291); `add` opens it, carrying a clash's error.
|
|
The filter bar searches name and address and filters by group and status;
|
|
it swaps the table (hosts/_table.html) alone and keeps the filters in the
|
|
URL (#5294). #}
|
|
{% block title %}Hosts — Steward{% endblock %}
|
|
{% block content %}
|
|
{% set can_add = session.user_role in ['operator', 'admin'] %}
|
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.5rem;">
|
|
<h1 class="page-title" style="margin-bottom:0;">Hosts</h1>
|
|
<div style="display:flex;gap:0.5rem;">
|
|
{% if session.user_role == 'admin' %}
|
|
<a class="btn btn-ghost" href="/plugins/host_agent/fleet/">Agent fleet</a>
|
|
{% endif %}
|
|
<a class="btn btn-ghost" href="/plugins/host_agent/maintenance">Maintenance</a>
|
|
{% if can_add %}
|
|
<button type="button" class="btn" onclick="document.getElementById('add-host').showModal()">Add host</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% if clock %}
|
|
{#- Milestone 490: a host whose clock drifts, or Steward's own when every host disagrees with it. #}
|
|
<p class="page-verdict tone-warn">
|
|
{%- if clock.steward %}Every host's clock is about {{ clock.typical|duration }} off from Steward's: Steward's own clock is the likely one that is wrong. Check what keeps the time in sync on the machine Steward runs on.
|
|
{%- else %}Clock off from Steward's on {% for name, secs in clock.hosts %}<a href="/hosts/{{ host_ids[name] }}">{{ name }}</a> by {{ secs|duration }}{{ ", " if not loop.last }}{% endfor %}. Check what keeps the time in sync there.
|
|
{%- endif -%}
|
|
</p>
|
|
{% endif %}
|
|
{% if total %}
|
|
<form class="filter-bar" method="get" action="/hosts/" role="search"
|
|
hx-target="#hosts-table" hx-push-url="true" hx-include="closest form">
|
|
<input type="search" name="q" value="{{ f.q }}" class="input" placeholder="Search name or address"
|
|
aria-label="Search hosts by name or address" autocomplete="off"
|
|
hx-get="/hosts/" hx-trigger="input changed delay:250ms, search">
|
|
<select name="group" aria-label="Group" hx-get="/hosts/" hx-trigger="change">
|
|
<option value="">All groups</option>
|
|
{% for g in group_names %}<option value="{{ g }}"{% if f.group == g %} selected{% endif %}>{{ g }}</option>{% endfor %}
|
|
<option value="{{ no_group }}"{% if f.group == no_group %} selected{% endif %}>No group</option>
|
|
</select>
|
|
<select name="state" aria-label="Status" hx-get="/hosts/" hx-trigger="change">
|
|
<option value="">Any status</option>
|
|
{% for key, label in states.items() %}<option value="{{ key }}"{% if f.state == key %} selected{% endif %}>{{ label }}</option>{% endfor %}
|
|
</select>
|
|
{% if f.sort != 'status' %}<input type="hidden" name="sort" value="{{ f.sort }}"><input type="hidden" name="dir" value="{{ f.dir }}">{% endif %}
|
|
<noscript><button type="submit" class="btn btn-sm">Filter</button></noscript>
|
|
</form>
|
|
<div id="hosts-table">{% include "hosts/_table.html" %}</div>
|
|
{% else %}
|
|
<div class="card">
|
|
<p style="color:var(--text-muted);">No hosts yet.{% if can_add %} <a href="/hosts/?add=1" onclick="document.getElementById('add-host').showModal();return false;">Summon one to the table.</a>{% endif %}</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if can_add %}
|
|
{% set v = (add or {}).get('values') or {} %}
|
|
<dialog id="add-host" class="modal" aria-labelledby="add-host-title">
|
|
<h2 id="add-host-title" class="modal-title">Add host</h2>
|
|
{% if add and add.error %}<p class="field-error" role="alert">{{ add.error }}</p>{% endif %}
|
|
<form method="post" action="/hosts/" style="display:grid;gap:0.75rem;">{{ csrf_field() }}
|
|
<div>
|
|
<label class="field-label" for="h-name">Name <span class="tone-crit" aria-hidden="true">*</span></label>
|
|
<input type="text" id="h-name" name="name" value="{{ v.name or '' }}"
|
|
placeholder="nas, web-1, …" autocomplete="off" required autofocus class="input">
|
|
</div>
|
|
<div>
|
|
<label class="field-label" for="h-address">Address</label>
|
|
<input type="text" id="h-address" name="address" value="{{ v.address or '' }}"
|
|
placeholder="hostname or IP; empty resolves by name" autocomplete="off" class="input">
|
|
</div>
|
|
<p class="field-hint" style="margin:0;">Next, on the host's page: add monitors, install its agent, and link an Ansible target.</p>
|
|
<div style="display:flex;gap:0.5rem;justify-content:flex-end;">
|
|
<button type="button" class="btn btn-sm btn-ghost" onclick="this.closest('dialog').close()">Cancel</button>
|
|
<button type="submit" class="btn btn-sm">Add host</button>
|
|
</div>
|
|
</form>
|
|
</dialog>
|
|
{% if add is not none %}<script>document.getElementById('add-host').showModal();</script>{% endif %}
|
|
{% endif %}
|
|
{% endblock %}
|