Update cloud-init scripts and documentation for enhanced DNS management and provisioning steps</message>

<message>Modify the first-boot.sh script to include an additional step for managing screen brightness during the provisioning process. Update user-data.bootstrap to improve DNS configuration by ensuring NetworkManager manages /etc/resolv.conf correctly, and remove obsolete scripts related to systemd-resolved. Enhance documentation to reflect these changes and clarify the setup process for users, improving overall network boot functionality and user experience.
This commit is contained in:
nearxos
2026-03-06 14:45:23 +02:00
parent 8233304ee2
commit 0844adbcbe
22 changed files with 2021 additions and 86 deletions

View File

@@ -0,0 +1,302 @@
#!/usr/bin/env python3
"""
Brightness control overlay — always-on-top icon in bottom-right; click to show controls.
Uses Wayland layer-shell OVERLAY so it stays above Chromium fullscreen/kiosk.
Requires: brightness-api.service running (listens 127.0.0.1:8765).
"""
import logging
import sys
import urllib.request
import urllib.error
LOG_TAG = "brightness-overlay"
logging.basicConfig(
stream=sys.stderr,
level=logging.INFO,
format=f"{LOG_TAG}: %(message)s",
)
log = logging.getLogger(LOG_TAG)
try:
import gi
gi.require_version("Gdk", "3.0")
gi.require_version("Gtk", "3.0")
from gi.repository import Gdk, Gtk, GLib
except Exception as e:
log.error("need PyGObject Gtk: %s", e)
sys.exit(1)
HAS_LAYER_SHELL = False
try:
gi.require_version("GtkLayerShell", "0.1")
from gi.repository import GtkLayerShell
HAS_LAYER_SHELL = True
except Exception:
log.warning("GtkLayerShell not available, using regular window")
API_URL = "http://127.0.0.1:8765/brightness"
API_STATE_URL = "http://127.0.0.1:8765/state"
# Sizes
ICON_SIZE = 44
EXPANDED_WIDTH = 300
PANEL_HEIGHT = 48
def get_current_state():
"""Return (level 15, is_auto). On error return (None, True) so Auto appears active."""
try:
with urllib.request.urlopen(API_STATE_URL, timeout=1) as r:
text = r.read().decode()
level = None
override = "0"
for line in text.strip().splitlines():
line = line.strip()
if line.startswith("level="):
level = line.split("=", 1)[1].strip()
elif line.startswith("override="):
override = line.split("=", 1)[1].strip()
if level and level.isdigit() and 1 <= int(level) <= 5:
return int(level), override == "0"
return (None, True)
except Exception:
return (None, True)
def set_brightness(level):
try:
req = urllib.request.Request(
API_URL,
data=f"level={level}".encode(),
method="POST",
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
with urllib.request.urlopen(req, timeout=2) as r:
log.info("brightness %s: %s", level, r.read().decode().strip())
except urllib.error.URLError as e:
log.warning("API call failed (is brightness-api.service running?): %s", e)
except Exception as e:
log.warning("set_brightness failed: %s", e)
def update_active_buttons(level_buttons, auto_btn, current_level, is_auto):
"""Set .active style on the button that matches current state."""
for i, btn in enumerate(level_buttons):
ctx = btn.get_style_context()
if not is_auto and current_level == i + 1:
ctx.add_class("active")
else:
ctx.remove_class("active")
if auto_btn:
ctx = auto_btn.get_style_context()
if is_auto:
ctx.add_class("active")
else:
ctx.remove_class("active")
def make_window_layer_shell(win):
GtkLayerShell.init_for_window(win)
GtkLayerShell.set_layer(win, GtkLayerShell.Layer.OVERLAY)
# Anchor only to bottom and right so the surface sits in the bottom-right corner
GtkLayerShell.set_anchor(win, GtkLayerShell.Edge.TOP, False)
GtkLayerShell.set_anchor(win, GtkLayerShell.Edge.LEFT, False)
GtkLayerShell.set_anchor(win, GtkLayerShell.Edge.BOTTOM, True)
GtkLayerShell.set_anchor(win, GtkLayerShell.Edge.RIGHT, True)
GtkLayerShell.set_keyboard_mode(win, GtkLayerShell.KeyboardMode.NONE)
GtkLayerShell.set_margin(win, GtkLayerShell.Edge.BOTTOM, 12)
GtkLayerShell.set_margin(win, GtkLayerShell.Edge.RIGHT, 12)
if hasattr(GtkLayerShell, "set_namespace"):
GtkLayerShell.set_namespace(win, "brightness-overlay")
if hasattr(GtkLayerShell, "set_exclusive_zone"):
GtkLayerShell.set_exclusive_zone(win, 0)
log.info("layer-shell OVERLAY anchored bottom-right (always on top)")
def position_fallback_bottom_right(win, width=None, height=None):
"""Position window in bottom-right (X11: only works with toplevel, not POPUP)."""
if width is None:
width = ICON_SIZE
if height is None:
height = ICON_SIZE
display = Gdk.Display.get_default()
if not display:
return
n = display.get_n_monitors()
monitor = display.get_monitor(0) if n else None
if not monitor:
return
geom = monitor.get_geometry()
x = geom.x + geom.width - width - 12
y = geom.y + geom.height - height - 12
win.move(x, y)
def make_window_fallback(win):
# Toplevel (not POPUP) so we can position and have no decorations on X11
win.set_keep_above(True)
win.set_decorated(False)
win.set_type_hint(Gdk.WindowTypeHint.UTILITY) # tool-style window, often undecorated
# Position after map so WM doesn't override
win.connect("map-event", lambda w, e: position_fallback_bottom_right(w))
log.info("X11 fallback: keep-above, undecorated, bottom-right")
def main():
# Normal toplevel: on X11 we can position it and set_decorated(False) removes title bar.
# (POPUP without parent cannot be positioned on X11 and gets "temporary window" / centering.)
win = Gtk.Window()
win.set_decorated(False)
win.set_resizable(False)
win.set_default_size(ICON_SIZE, ICON_SIZE)
win.set_skip_taskbar_hint(True)
win.set_skip_pager_hint(True)
# Transparent window: need RGBA visual and app-paintable
screen = Gdk.Screen.get_default()
if screen and screen.get_rgba_visual():
win.set_visual(screen.get_rgba_visual())
win.set_app_paintable(True)
# CSS: transparent window and overlay; only subtle hover so icon stays visible
style = """
GtkWindow, window {
background-color: transparent;
background-image: none;
}
.brightness-icon-btn {
background-color: transparent;
border-radius: 8px;
padding: 4px;
min-width: 36px;
min-height: 36px;
}
.brightness-icon-btn:hover {
background-color: rgba(0, 0, 0, 0.25);
}
.brightness-panel {
background-color: transparent;
border-radius: 8px;
padding: 6px;
}
.brightness-panel.expanded {
background-color: rgba(0, 0, 0, 0.5);
}
.brightness-panel button {
min-width: 36px;
font-weight: bold;
}
.brightness-panel button.level-btn.active {
background-color: rgba(255, 255, 255, 0.35);
border: 1px solid rgba(255, 255, 255, 0.6);
}
"""
css = Gtk.CssProvider()
css.load_from_data(style.encode())
Gtk.StyleContext.add_provider_for_screen(
Gdk.Screen.get_default(),
css,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
)
# Main horizontal box: [revealer with controls] [icon button]
main_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4)
# Left part: revealer with Brightness + 15 + Auto
strip_box = Gtk.Box(spacing=6)
strip_box.set_margin_start(8)
strip_box.set_margin_top(6)
strip_box.set_margin_bottom(6)
strip_box.set_margin_end(4)
strip_box.get_style_context().add_class("brightness-panel")
strip_box.pack_start(Gtk.Label(label="Brightness "), False, False, 0)
level_buttons = []
for i in range(1, 6):
btn = Gtk.Button(label=str(i))
btn.get_style_context().add_class("level-btn")
btn.set_can_focus(False)
strip_box.pack_start(btn, False, False, 0)
level_buttons.append(btn)
auto_btn = Gtk.Button(label="Auto")
auto_btn.get_style_context().add_class("level-btn")
auto_btn.set_can_focus(False)
strip_box.pack_start(auto_btn, False, False, 0)
def refresh_active_state():
level, is_auto = get_current_state()
update_active_buttons(level_buttons, auto_btn, level or 3, is_auto)
def on_level_click(btn, level):
set_brightness(level)
# Optimistic update so active state shows immediately
if level == "auto":
update_active_buttons(level_buttons, auto_btn, 3, True)
else:
update_active_buttons(level_buttons, auto_btn, int(level), False)
# Sync from API after a short delay (daemon updates state file periodically)
def delayed_refresh():
refresh_active_state()
return False
GLib.timeout_add(500, delayed_refresh)
for i, btn in enumerate(level_buttons):
btn.connect("clicked", on_level_click, str(i + 1))
auto_btn.connect("clicked", on_level_click, "auto")
revealer = Gtk.Revealer()
revealer.set_reveal_child(False)
revealer.set_transition_type(Gtk.RevealerTransitionType.SLIDE_LEFT)
revealer.set_transition_duration(150)
revealer.add(strip_box)
main_box.pack_start(revealer, False, False, 0)
# Right part: always-visible icon button (click toggles panel)
try:
icon_img = Gtk.Image.new_from_icon_name("display-brightness-symbolic", Gtk.IconSize.BUTTON)
except Exception:
icon_img = Gtk.Label(label="")
icon_btn = Gtk.Button()
icon_btn.get_style_context().add_class("brightness-icon-btn")
icon_btn.set_can_focus(False)
icon_btn.add(icon_img)
icon_btn.set_relief(Gtk.ReliefStyle.NONE)
expanded = [False] # use list so closure can mutate
use_fallback = not (HAS_LAYER_SHELL and GtkLayerShell.is_supported())
def on_icon_clicked(btn):
expanded[0] = not expanded[0]
revealer.set_reveal_child(expanded[0])
ctx = strip_box.get_style_context()
if expanded[0]:
ctx.add_class("expanded")
GLib.idle_add(refresh_active_state)
win.resize(EXPANDED_WIDTH, PANEL_HEIGHT)
if use_fallback:
position_fallback_bottom_right(win, EXPANDED_WIDTH, PANEL_HEIGHT)
else:
ctx.remove_class("expanded")
win.resize(ICON_SIZE, ICON_SIZE)
if use_fallback:
position_fallback_bottom_right(win, ICON_SIZE, ICON_SIZE)
icon_btn.connect("clicked", on_icon_clicked)
main_box.pack_end(icon_btn, False, False, 0)
win.add(main_box)
if HAS_LAYER_SHELL and GtkLayerShell.is_supported():
make_window_layer_shell(win)
else:
make_window_fallback(win)
win.connect("destroy", Gtk.main_quit)
win.show_all()
GLib.idle_add(refresh_active_state)
log.info("brightness icon visible (click to show controls)")
Gtk.main()
if __name__ == "__main__":
main()