Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
confconsole (2.2.3+keel15) trixie; urgency=low

* The Overlay network screen keeps one place in the Instance menu. It
moved from Instance > Advanced to the Instance menu itself once the
spec had an overlay, and Advanced went with it, so the maintainer
took it for gone (keel-web-2, 2026-10-03). Its place now follows the
installation mode alone: behind Advanced in a simple installation,
configured or not; in the Instance menu in the cloud modes, with no
empty Advanced.
* Add peer refuses this node's own public key, shown at the top of the
same screen, and this node's own mesh address, and asks before adding
a peer outside this node's overlay prefix (it still works, routed as
one host). The form comes back with what was typed.
* A network change waiting for its confirmation is no longer left to
revert unnoticed (both nodes, 2026-10-03). After apply, when the
operator does not confirm from here or keel refuses it, the screen
gives the time the change reverts at, in UTC, and the command, keel
network confirm. Opening the screen while a change waits says so on
its first line and offers Confirm now first, which runs keel network
confirm; keel decides whether this session may. The question before
it says Yes is safe to try instead of steering an SSH session to No.

-- Marcos Méndez <mendez.foto@gmail.com> Sat, 03 Oct 2026 15:00:00 +0000

confconsole (2.2.3+keel14) trixie; urgency=low

* The Let's Encrypt screen is driven by the instance description. Its
Expand Down
33 changes: 25 additions & 8 deletions docs/Instance.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ spec.
| | and the first boot asks them no role |
+-------------------+--------------------------------------------------------+
| Overlay network | the ``wireguard`` overlay is in the chain. In the menu |
| | in ``cloud_simple`` and ``cloud_advanced``, or once |
| | the spec enables or configures the overlay; otherwise |
| | behind **Advanced** |
| | in ``cloud_simple`` and ``cloud_advanced``; otherwise |
| | behind **Advanced**, configured or not, so it never |
| | moves once an address or a peer is applied |
+-------------------+--------------------------------------------------------+
| Keel Cloud | only once Keel Cloud exists: ``/etc/keel/cloud- |
| | endpoint`` holds the service's endpoint. Hidden by |
Expand Down Expand Up @@ -153,7 +153,20 @@ Add peer
address (routed as one host, ``/128``), its endpoint (``host:port``, an
IPv6 address in brackets, ``[2001:db8::20]:51820``; blank when that node
reaches this one) and a keepalive in seconds (25 by default, blank for
none). A peer with a key already there replaces it.
none). A peer with a key already there replaces it. This node's own
public key, and one of this node's own mesh addresses, are refused; an
address outside this node's prefix is asked about (a peer is routed as
one host, so it still works). Either way the form comes back with what
was typed.

Confirm now
First, while a network change waits for its confirmation; the screen's
first line then says the time it reverts at, in UTC. It runs ``keel
network confirm`` from this session and shows keel's verdict: keel
accepts it from the machine's console (or a process attached from a
container's host) and from an SSH session opened after the change over
the new configuration, and refuses it from a session opened before. The
screen does not judge the session itself.

Remove peer
Pick the peer by its key; the screen asks before removing it.
Expand All @@ -177,10 +190,14 @@ change waits after apply: one this run brought up, even if a later step
failed; one that failed and could not be rolled back either, which keel
leaves to its revert timer; or one an earlier run left, which keel names
when it refuses another. It needs keel 0.11.0 or later, the first with
``--skip-uplink``, which the package recommends. It then offers to confirm from here: keel accepts that from the
machine's own console, and refuses it from an SSH session opened before
the change, in which case the change reverts unless a new session
confirms it.
``--skip-uplink``, which the package recommends. It then offers to
confirm from here, Yes by default: keel accepts that from the machine's
own console, and refuses it, changing nothing, from an SSH session
opened before the change. When the change still waits after that (No,
or a refusal), the screen gives the time it reverts at, in UTC, read
from keel's marker (``/var/lib/keel/network/pending.json``: the window
starts when the interface comes up), and the command, ``keel network
confirm``, to run from a new session.

Headless equivalent: edit ``network.overlay.wireguard`` in the
description, ``keel spec apply --system-only --skip-uplink``, then ``keel
Expand Down
5 changes: 3 additions & 2 deletions keelfirstboot.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ def overlay(console, path: str, role: str) -> str | None:
return None
if choice == CONTINUE:
return key
wgscreen.ACTIONS[choice](console, path, document, wireguard)
wgscreen.ACTIONS[choice](console, path, document, wireguard, key)


def overlay_choices(role: str, wireguard: dict) -> list[tuple[str, str]]:
Expand Down Expand Up @@ -488,7 +488,8 @@ def finish(console, path: str, role: str) -> None:
def later(console, role: str, overlay_done: bool) -> None:
lines = [LATER_TEXT]
if not overlay_done:
# behind Advanced in a simple installation, until it is in use
# behind Advanced in a simple installation, in the cloud modes
# in the Instance menu itself
lines.append(f" {keelmenu.overlay_where()}: this node's address"
" and peers")
lines.append(f" {MODE_WHERE[role]}")
Expand Down
16 changes: 7 additions & 9 deletions keelmenu.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@
PostgreSQL it notes that and changes nothing, so their Database mode
offers Standalone only rather than roles nothing would make.
- Instance/Overlay_network.py: the wireguard overlay in the chain. It is
in the Instance menu in the cloud modes, or once this node uses the
overlay; in a simple installation it is behind Advanced.
in the Instance menu in the cloud modes, where joining nodes is the
point; in a simple installation it is behind Advanced, configured or
not. Its place follows the installation mode alone, so it does not
move when the first address or peer is applied (it did, and Advanced
went with it: the maintainer took it for gone, 2026-10-03).
- Instance/Keel_Cloud.py: hidden until Keel Cloud exists, which is when
CLOUD_ENDPOINT holds the endpoint of the service: one https URL with a
host (a bracketed IPv6 literal allowed), in a regular file root owns
Expand All @@ -46,7 +49,6 @@
from urllib.parse import urlsplit

import keelcli
import wgcli

log = logging.getLogger("keelmenu")

Expand All @@ -63,7 +65,6 @@
# Those whose primary and replica keel applies, not only validates
REPLICATING_ENGINES = frozenset({"mariadb"})
WIREGUARD = "wireguard"
ENABLED = "enabled"
OK = "ok"

SHOW = "show"
Expand Down Expand Up @@ -93,7 +94,6 @@ class Machine:

chain: Chain | None
mode: str
uses_overlay: bool
server_engine: str = ""
has_server: bool = False

Expand Down Expand Up @@ -138,10 +138,8 @@ def machine() -> Machine:
name = _section(document, "appliance").get("name")
chain = chain_of(name) if name else None
mode = str(_section(document, "installation").get("mode") or "")
uses = (_section(document, "overlays").get(WIREGUARD) == ENABLED
or bool(wgcli.overlay_of(document)))
server = keelcli.server_of(document)
return Machine(chain, mode, uses, str(server.get("engine") or ""),
return Machine(chain, mode, str(server.get("engine") or ""),
bool(server))


Expand Down Expand Up @@ -213,7 +211,7 @@ def _replication(found: Machine) -> str:
def _overlay(found: Machine) -> str:
if found.chain is not None and WIREGUARD not in found.chain.overlays:
return HIDE
if found.mode in CLOUD_MODES or found.uses_overlay:
if found.mode in CLOUD_MODES:
return SHOW
return ADVANCED

Expand Down
7 changes: 5 additions & 2 deletions tests/test_first_boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -697,12 +697,15 @@ def actions(self, monkeypatch, spec):
"""The overlay screen's own actions, each writing what it would"""
ran = []

def set_address(console, path, document, wireguard):
def set_address(console, path, document, wireguard, key):
# this node's own key, which Add peer refuses as a peer
assert key == THIS_KEY.strip()
ran.append("address")
spec({"version": 1, "network": {"overlay": {
"wireguard": OVERLAY}}})

def add_peer(console, path, document, wireguard):
def add_peer(console, path, document, wireguard, key):
assert key == THIS_KEY.strip()
ran.append("peer")
spec({"version": 1, "network": {"overlay": {"wireguard": {
**OVERLAY, "peers": [PEER]}}}})
Expand Down
30 changes: 30 additions & 0 deletions tests/test_instance_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,35 @@ def test_behind_advanced_in_a_simple_installation_is_the_overlay(

assert tags(console, 1) == ["Overlay network"]

@pytest.mark.parametrize("appliance", ["web", "core"])
def test_a_configured_overlay_stays_behind_advanced(self, instance,
appliance):
# the same two menus before and after the first peer, so the
# screen is where the operator found it (keel-web-2, 2026-10-03)
overlay = {"overlay": {"wireguard": {
"address": "fd00:6b65:1::1/64", "peers": [{
"public_key": "0niNkgzhpbKmTSrWCzukb6jaYogKZkGhW+xWlh52Mh8=",
"allowed_ips": ["fd00:6b65:1::2/128"]}]}}}
menu, console = instance(
appliance, menus=[("ok", "Advanced"), ("ok", "Overlay network")],
overlays={"wireguard": "enabled"}, network=overlay)

assert menu.run() == str(INSTANCE_DIR / "Overlay_network.py")

assert sorted(tags(console)) == sorted(PLAIN + ["Advanced"])
assert tags(console, 1) == ["Overlay network"]

@pytest.mark.parametrize("mode", ["cloud_simple", "cloud_advanced"])
def test_a_cloud_mode_with_an_overlay_has_no_empty_advanced(
self, instance, mode
):
menu, console = instance("web", mode,
overlays={"wireguard": "enabled"})

menu.run()

assert sorted(tags(console)) == sorted(PLAIN + ["Overlay network"])

def test_back_from_advanced_reopens_the_instance_menu(self, instance):
menu, console = instance(
"core", menus=[("ok", "Advanced"), ("cancel", "")])
Expand Down Expand Up @@ -432,6 +461,7 @@ def test_the_overlay_screens_menus(self):
"endpoint": "[2001:db8::20]:51820"}]}
menus = [
wgscreen.choices(wireguard),
wgscreen.choices(wireguard, waiting=True),
keelfirstboot.overlay_choices("primary", wireguard),
keelfirstboot.overlay_choices("replica", {}),
keelfirstboot.ROLE_CHOICES,
Expand Down
44 changes: 26 additions & 18 deletions tests/test_keelmenu.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ def test_the_spec_names_the_appliance_and_the_mode(self, chains, spec):

assert found.chain.overlays == frozenset(WEB)
assert found.mode == "cloud_simple"
assert found.uses_overlay is False

def test_a_spec_that_names_no_appliance_leaves_the_chain_unknown(
self, chains, spec
Expand Down Expand Up @@ -128,21 +127,11 @@ def test_sections_that_are_not_mappings_count_as_absent(
assert found.chain is None
assert found.mode == ""

def test_an_enabled_wireguard_overlay_is_in_use(self, chains, spec):
spec("web", overlays={"wireguard": "enabled"})

assert keelmenu.machine().uses_overlay is True

def test_a_declared_overlay_address_is_in_use(self, chains, spec):
spec("web", network={"overlay": {"wireguard": {
"address": "fd00:6b65:1::1/64"}}})

assert keelmenu.machine().uses_overlay is True


def machine(name, mode="simple", uses_overlay=False, chains=None):
def machine(name, mode="simple", chains=None):
chain = keelmenu.chain_of(name) if name else None
return keelmenu.Machine(chain, mode, uses_overlay)
return keelmenu.Machine(chain, mode)


class TestDatabaseMode:
Expand Down Expand Up @@ -175,7 +164,7 @@ def test_the_engines_are_one_set(self):
def test_a_declared_server_shows_it_whatever_the_chain(self, chains):
# a description that holds database.server is a server to
# configure, even where the manifests name no engine
found = keelmenu.Machine(keelmenu.chain_of("web"), "simple", False,
found = keelmenu.Machine(keelmenu.chain_of("web"), "simple",
server_engine="mariadb", has_server=True)

assert keelmenu.place(DATABASE, found) == keelmenu.SHOW
Expand All @@ -200,7 +189,7 @@ def test_an_engine_keel_does_not_replicate_has_no_cloud(
assert keelmenu.place(REPLICATION, machine("db")) == keelmenu.HIDE

def test_a_declared_mariadb_server_replicates(self, chains):
found = keelmenu.Machine(keelmenu.chain_of("web"), "simple", False,
found = keelmenu.Machine(keelmenu.chain_of("web"), "simple",
server_engine="mariadb", has_server=True)

assert keelmenu.place(REPLICATION, found) == keelmenu.SHOW
Expand Down Expand Up @@ -229,10 +218,29 @@ def test_behind_advanced_in_a_simple_installation(self, chains, mode):
assert keelmenu.place(OVERLAY, machine("web", mode)) == (
keelmenu.ADVANCED)

def test_in_the_menu_once_this_node_uses_the_overlay(self, chains):
found = machine("web", "simple", uses_overlay=True)
@pytest.mark.parametrize("configured", [
{"overlays": {"wireguard": "enabled"}},
{"network": {"overlay": {"wireguard": {
"address": "fd00:6b65:1::1/64"}}}},
])
def test_it_stays_behind_advanced_once_configured(self, chains, spec,
configured):
# It moved to the Instance menu once the spec had an overlay, and
# Advanced went away with it: on keel-web-2 the maintainer took
# it for gone (2026-10-03). One place for the life of the node.
spec("web", **configured)

assert keelmenu.place(OVERLAY, keelmenu.machine()) == (
keelmenu.ADVANCED)
assert keelmenu.overlay_where() == "Advanced > Overlay network"

@pytest.mark.parametrize("mode", ["cloud_simple", "cloud_advanced"])
def test_a_cloud_mode_keeps_it_in_the_menu_once_configured(
self, chains, spec, mode
):
spec("web", mode, overlays={"wireguard": "enabled"})

assert keelmenu.place(OVERLAY, found) == keelmenu.SHOW
assert keelmenu.place(OVERLAY, keelmenu.machine()) == keelmenu.SHOW

def test_hidden_where_the_chain_carries_no_wireguard(self, chains):
chains["bare"] = resolved([overlay("installer")])
Expand Down
Loading
Loading