"""Reading what ifupdown was told about this machine's network. Only the parsing, so it can be tested against the file a real machine actually has rather than against a running laptop. WHY THIS EXISTS. The Bible study laptop had working internet and a Plasma network applet that said there was no connection. Both were true. Debian's installer had been given the Wi-Fi during installation, so it wrote the network into /etc/network/interfaces: auto wlp2s0 iface wlp2s0 inet dhcp wpa-ssid Something wpa-psk a-secret ifupdown brings that up with its own wpa_supplicant and dhcpcd, which is why the internet works. NetworkManager ships with plugins=ifupdown and [ifupdown] managed=false, so an interface named in that file is marked UNMANAGED — which is why nmcli showed `wlp2s0 wifi unmanaged`, why the applet had nothing to report, and why scanning found nothing: an unmanaged device is not a radio NetworkManager will scan with. managed=false is deliberate and stays. It is what keeps a tower's static Ethernet under ifupdown's control where an administrator put it. What has to change is narrower: a WIRELESS interface should belong to NetworkManager, so the room can see it, scan with it, and so its password is stored the way this package stores passwords. So this finds the wireless stanza and everything needed to recreate it as a NetworkManager system connection. """ import re # ifupdown's own spelling. Values may be quoted, and a psk may be a # 64-character hex key rather than a passphrase. SETTINGS = ("wpa-ssid", "wpa-psk", "wpa-key-mgmt", "wpa-conf", "wpa-proto") _STANZA = re.compile(r"^\s*(iface|auto|allow-hotplug|source|source-directory)\s+(.*)$") def _unquote(value): value = (value or "").strip() for quote in ('"', "'"): if len(value) >= 2 and value.startswith(quote) and value.endswith(quote): return value[1:-1] return value def parse(text): """Every `iface` stanza in an interfaces file, with its options. Returns a list of dicts: name, family, method, options, and the line range the stanza occupies so a caller can comment exactly it out and nothing else. """ lines = (text or "").splitlines() stanzas = [] current = None for number, raw in enumerate(lines): line = raw.split("#", 1)[0].rstrip() match = _STANZA.match(line) if match: keyword, rest = match.group(1), match.group(2).split() if keyword == "iface": current = { "name": rest[0] if rest else "", "family": rest[1] if len(rest) > 1 else "", "method": rest[2] if len(rest) > 2 else "", "options": {}, "first": number, "last": number, } stanzas.append(current) else: # auto/allow-hotplug end the previous stanza's body. current = None continue if current is None or not line.strip(): continue parts = line.strip().split(None, 1) if not parts: continue current["options"][parts[0].lower()] = _unquote( parts[1] if len(parts) > 1 else "") current["last"] = number return stanzas def wireless(stanzas, is_wireless): """The stanzas that describe a wireless interface. `is_wireless` answers for a device name, so the caller decides how to ask the machine — /sys/class/net//wireless in practice. A wpa-ssid is accepted as evidence on its own, for a machine whose device has since been renamed or removed. """ found = [] for stanza in stanzas: name = stanza.get("name") or "" if not name or name == "lo": continue if is_wireless(name) or "wpa-ssid" in stanza["options"]: found.append(stanza) return found def credentials(stanza): """SSID and pre-shared key from a stanza, or (None, None). A stanza that points at a wpa_supplicant.conf instead of naming the network inline returns no SSID: that one is not ours to move, and the caller must leave it alone rather than guess. """ options = stanza.get("options") or {} if "wpa-conf" in options and "wpa-ssid" not in options: return None, None return options.get("wpa-ssid") or None, options.get("wpa-psk") or None def comment_out(text, stanza, marker="# ekklesia-classroom: "): """Comment out exactly one stanza, leaving every other byte alone.""" lines = (text or "").splitlines(keepends=True) for number in range(stanza["first"], stanza["last"] + 1): if number < len(lines) and not lines[number].lstrip().startswith("#"): lines[number] = marker + lines[number] # The `auto`/`allow-hotplug` line above it would otherwise ask # ifupdown to bring up an interface it no longer has a stanza for. for number in range(stanza["first"] - 1, -1, -1): stripped = lines[number].strip() if number < len(lines) else "" if not stripped or stripped.startswith("#"): continue if re.match(r"^(auto|allow-hotplug)\s", stripped) and \ stanza["name"] in stripped.split(): lines[number] = marker + lines[number] break return "".join(lines)