Files
Classroom_Desktop/scripts/classroom-wifi
T
ekklesiaandClaude Opus 5 30d2d99230 fix: a hidden Wi-Fi survives the hand-over, and a failed one leaves an owner
The prayer-room laptop's 0.2.5 migration built a NetworkManager
connection, could not bring it up, rolled back, and left the wireless
adapter belonging to nobody. Four separate defects, each of which would
have been enough on its own.

THE HIDDEN FLAG WAS THROWN AWAY. The stanza said wpa-scan-ssid 1 and the
parser read it — into options, where the only accessor ignored it. The
profile came out with 802-11-wireless.hidden: no, and NetworkManager
will not associate with an SSID it has not seen in a scan. A hidden one
is never in a scan. credentials() returned a pair; profile() now returns
the whole network, and add() writes the flag in BOTH directions rather
than only when true, because a default that happens to be right is not
the same as an answer.

IFDOWN RAN AFTER THE STANZA WAS COMMENTED OUT. ifupdown reads
/etc/network/interfaces to find out how to bring an interface down; with
the stanza gone it says "Ignoring unknown interface" and does nothing.
So ifupdown's own wpa_supplicant and DHCP client were still holding the
radio when NetworkManager was asked to associate with it, which is a
fight NetworkManager loses. Down first, while the file still says how;
rewrite the file second.

THE ROLLBACK LEFT THE ADAPTER UNMANAGED. restore() ended with
`device set <dev> managed no`, which is right while ifupdown can bring
the interface up and is a radio nobody owns when ifup has failed too.
That is the state the laptop was found in, and why `nmcli device set
wlp2s0 managed yes` was a manual recovery step. It now escalates: if
ifupdown cannot get the network back either, the interface goes to
NetworkManager rather than to neither, and it says so loudly.

A '#' IN A PASSWORD WAS SILENTLY TRUNCATED. The parser cut every line at
its first '#'. interfaces(5) is explicit that only whole-line comments
exist, so a pre-shared key containing one became a prefix of itself —
and the only symptom is an association failing exactly the way a wrong
password fails.

Two smaller ones went with them. The rollback un-comments its own marked
lines in place rather than copying the backup over the whole file, so a
wired change made between the migration and the rollback is no longer
silently reverted. And nmcli terse output is split on its own escaping
rules under LC_ALL=C, so a network whose name contains a colon cannot
make a successful migration look like a failed one.

--adopt now also refuses a network it cannot recreate before taking the
interface away from ifupdown to find out, and reports an
unmanaged-devices line in NetworkManager's configuration, which survives
the stanza going away.

Verified against a real NetworkManager 1.52.1: the keyfile carries
hidden=true, the SSID keeps its space, and a psk containing '#' is
intact, at 0600 root:root with no connection.permissions. The new test
drives the whole hand-over against stubbed tools and asserts the
ordering, the rollback and the managed state — including that a machine
already migrated, which is the laptop, is not migrated again.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-22 20:20:53 -05:00

808 lines
28 KiB
Python
Executable File

#!/usr/bin/env python3
"""Put the church's Wi-Fi on this machine, so the room can use it and
nobody in the room can read it.
Run by an administrator, as root, over SSH or from the maintenance
terminal:
classroom-wifi "Church Wi-Fi" # prompts for the password
classroom-wifi --list
classroom-wifi --forget "Old Network"
classroom-wifi --adopt --dry-run # what would be migrated
classroom-wifi --adopt # migrate it, with rollback
ADOPTING A WI-FI THE INSTALLER SET UP.
A machine whose Wi-Fi was given to the Debian installer has it in
/etc/network/interfaces, where ifupdown owns it. The internet works and
NetworkManager reports the device UNMANAGED, so the room's applet says
there is no connection and cannot scan. --adopt moves that network into a
NetworkManager system connection and takes the stanza out of ifupdown's
hands, which is the whole fix for both symptoms.
The password never leaves the machine and is never printed: this runs as
root, reads it out of a root-only file and writes it into another one.
WHY THIS EXISTS RATHER THAN LETTING A TEACHER ADD ONE.
A wireless network added from the desktop is created as a USER connection:
NetworkManager writes connection.permissions naming whoever added it.
Debian then allows that user to read its secrets with no authentication at
all — org.freedesktop.NetworkManager.settings.modify.own is "yes" — so a
password typed in by one teacher is readable by the next person to sit
down, through the applet's own "Show Network's QR Code".
This creates a SYSTEM connection instead: no connection.permissions, so it
belongs to the machine rather than to a person, its secrets are stored in
a root-only file, and reading them needs an administrator. Every account
on the machine can still USE it, and NetworkManager reconnects to it at
boot on its own without anybody logging in.
That is the whole reason this is an administrator's command. The classroom
account is refused the NetworkManager actions that return secrets — see
org.ekklesia.classroom.network.rules — and that refusal would be pointless
if the room could create a connection it was allowed to read.
"""
import argparse
import getpass
import os
import shutil
import subprocess
import sys
import time
sys.path.insert(0, "/usr/local/lib/ekklesia-classroom")
sys.path.insert(
0,
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "lib"),
)
import classroom_network as ifupdown # noqa: E402
CONNECTIONS = "/etc/NetworkManager/system-connections"
INTERFACES = "/etc/network/interfaces"
# How long NetworkManager gets to bring the network back up after the
# hand-over before this decides it has failed and puts ifupdown back.
ADOPT_TIMEOUT = 45
def say(message):
print(f"classroom-wifi: {message}")
def nmcli(*args, check=True):
result = subprocess.run(
["nmcli"] + list(args), capture_output=True, text=True, timeout=60,
check=False,
# nmcli translates its output and its terse field separator rules
# are the only thing here that is parsed. C is the one locale in
# which what this reads back is what nmcli is documented to
# print, on a machine in any country.
env={**os.environ, "LC_ALL": "C"},
)
if check and result.returncode != 0:
say(result.stderr.strip() or "nmcli failed")
raise SystemExit(1)
return result
def terse(line):
"""One line of `nmcli -t` output, split into its fields.
nmcli escapes a colon inside a value as `\\:` and a backslash as
`\\\\`, so splitting on ":" is wrong for any network whose name
contains one. Everything here that compares a connection name to a
field goes through this, because getting it wrong does not produce an
error — it produces a migration that decides it failed and rolls a
working network back.
"""
fields = []
current = []
escaped = False
for character in line:
if escaped:
current.append(character)
escaped = False
elif character == "\\":
escaped = True
elif character == ":":
fields.append("".join(current))
current = []
else:
current.append(character)
fields.append("".join(current))
return fields
def connections():
"""Every saved connection, as (name, uuid, type)."""
result = nmcli("-t", "-f", "NAME,UUID,TYPE", "connection", "show",
check=False)
found = []
for line in (result.stdout or "").splitlines():
fields = terse(line)
if len(fields) >= 3:
found.append((fields[0], fields[1], fields[2]))
return found
def listing():
for name, _uuid, kind in connections():
if "wireless" in kind:
print(f" {name}")
return 0
def forget(name):
nmcli("connection", "delete", name)
say(f"removed {name}")
return 0
def add(ssid, password, name=None, hidden=False):
name = name or ssid
# Deliberately no connection.permissions. That single omission is the
# difference between a connection the machine owns and one a teacher
# owns and may therefore read.
arguments = [
"connection", "add",
"type", "wifi",
"con-name", name,
"ifname", "*",
"ssid", ssid,
"connection.autoconnect", "yes",
# Stored in the connection file, which is root-only, rather than
# in a login keyring that belongs to whoever is signed in.
"802-11-wireless-security.psk-flags", "0",
# WHETHER THE ACCESS POINT BROADCASTS ITS NAME.
#
# Always passed, in both directions, rather than only when it is
# true. NetworkManager's default is "no", and a default that
# happens to be right is not the same as an answer: this is the
# setting the prayer-room laptop's migration left at the default
# and could therefore never associate. Written down either way so
# that the connection file says what was meant.
"802-11-wireless.hidden", "yes" if hidden else "no",
]
if password:
arguments += [
"wifi-sec.key-mgmt", "wpa-psk",
"wifi-sec.psk", password,
]
nmcli(*arguments)
if hidden:
say(f"{name} is a hidden network; NetworkManager will probe for it "
"by name")
path = connection_file(name)
if path and os.path.exists(path):
# NetworkManager already writes 0600 root:root. Said out loud and
# enforced anyway, because this is the file the whole arrangement
# rests on.
os.chmod(path, 0o600)
os.chown(path, 0, 0)
mode = oct(os.stat(path).st_mode & 0o777)
say(f"{path} is {mode} root:root")
say(f"added {name} as a system connection")
say("the room can use it; the classroom account cannot read it")
return 0
def connection_file(name):
"""Where NetworkManager actually put a connection, or None.
Asked rather than constructed. NetworkManager escapes characters in
the filename it derives from a connection name, so
f"{name}.nmconnection" is a guess that happens to be right for a
one-word SSID and wrong for "ROGC Classroom" — which is the network
this command exists to migrate, and the file whose permissions the
whole arrangement rests on.
"""
result = nmcli("-t", "-f", "NAME,FILENAME", "connection", "show",
check=False)
for line in (result.stdout or "").splitlines():
fields = terse(line)
if len(fields) >= 2 and fields[0] == name and fields[-1]:
return fields[-1]
return None
def run(*command):
"""Run a command, tolerating one that is not installed.
Every call in the hand-over goes through this. An uncaught
FileNotFoundError here once left a machine with its stanza already
commented out and no rollback run, which is the exact state this
whole command exists to avoid.
"""
try:
return subprocess.run(list(command), capture_output=True, text=True,
timeout=60, check=False)
except (OSError, subprocess.SubprocessError) as error:
say(f"{command[0]}: {error}")
return None
def is_wireless(device):
return os.path.isdir(f"/sys/class/net/{device}/wireless")
def connectivity():
result = nmcli("networking", "connectivity", check=False)
return (result.stdout or "").strip()
def online():
"""Whether the machine can actually reach anything."""
if connectivity() in ("full", "limited"):
return True
return subprocess.run(
["curl", "-fsS", "--max-time", "8", "-o", "/dev/null",
"https://updates.riverofglorymn.org/stable/manifest.json"],
capture_output=True, check=False,
).returncode == 0
def carrying(name, device):
"""Whether NetworkManager has this connection up on this device.
Asked as well as "is the machine online", and not instead of it. A
machine with a second link would answer yes to connectivity while the
interface being migrated sat dead, and this would then call a failed
migration a success and leave the Wi-Fi behind.
"""
result = nmcli("-t", "-f", "NAME,DEVICE", "connection", "show",
"--active", check=False)
for line in (result.stdout or "").splitlines():
fields = terse(line)
if len(fields) == 2 and fields[0] == name and fields[1] == device:
return True
return False
def device_state(device):
"""NetworkManager's word for what it is doing with this interface."""
result = nmcli("-t", "-f", "DEVICE,STATE", "device", "status", check=False)
for line in (result.stdout or "").splitlines():
fields = terse(line)
if len(fields) >= 2 and fields[0] == device:
return fields[1]
return "unknown"
def unmanaged_by_config(device):
"""Configuration files that name this device as unmanaged.
Separate from the ifupdown plugin, and the reason the prayer-room
laptop still needed `nmcli device set wlp2s0 managed yes` by hand
after its stanza was gone. A keyfile saying `unmanaged-devices` is
not undone by commenting out an interfaces stanza, and a hand-over
that does not notice it hands the interface to nobody.
"""
naming = []
candidates = ["/etc/NetworkManager/NetworkManager.conf"]
for directory in ("/etc/NetworkManager/conf.d",
"/usr/lib/NetworkManager/conf.d",
"/run/NetworkManager/conf.d"):
try:
candidates += sorted(
os.path.join(directory, entry)
for entry in os.listdir(directory)
if entry.endswith(".conf")
)
except OSError:
continue
for path in candidates:
try:
with open(path, "r", encoding="utf-8", errors="replace") as handle:
text = handle.read()
except OSError:
continue
for line in text.splitlines():
stripped = line.strip()
if not stripped.startswith("unmanaged-devices"):
continue
if device in stripped or "except:" in stripped:
naming.append(path)
break
return naming
def find_stanza():
"""The one wireless stanza ifupdown holds, or None, plus the file."""
if not os.path.exists(INTERFACES):
# A machine with no ifupdown configuration at all. Nothing is
# wrong with that and nothing here has anything to do.
return None, ""
try:
with open(INTERFACES, "r", encoding="utf-8") as handle:
text = handle.read()
except OSError as error:
say(f"cannot read {INTERFACES}: {error}")
return None, None
found = ifupdown.wireless(ifupdown.parse(text), is_wireless)
if not found:
return None, text
if len(found) > 1:
say(f"{len(found)} wireless stanzas found; refusing to guess")
return None, text
return found[0], text
def check():
"""Say who owns the Wi-Fi. Reads only; changes nothing, ever.
This is what the migration runs instead of migrating, because moving
a machine's only link during an unattended upgrade is not a risk a
room in a locked building should carry.
"""
stanza, text = find_stanza()
if text is None:
return 0
if stanza is None:
say("no wireless interface is under ifupdown's control here")
return 0
device = stanza["name"]
found = ifupdown.profile(stanza)
say(f"NOTE: {device} is configured in {INTERFACES}, so ifupdown owns "
"it and")
say(" NetworkManager will report it unmanaged — the room's panel")
say(" will say there is no connection and will not find networks.")
if found and found["hidden"]:
say(" Its access point does not broadcast its name, which "
"NetworkManager")
say(" has to be told; --adopt carries that across.")
for path in unmanaged_by_config(device):
say(f" {path} also names this device unmanaged; --adopt "
"reports it")
say(" rather than editing it.")
if found is None:
say(" Its credentials are not in that file, so it has to be "
"moved by hand.")
elif found["key_mgmt"] is None:
say(" Its security is not a pre-shared key, so it has to be "
"moved by hand.")
else:
say(" To move it, while watching, over SSH:")
say(" classroom-wifi --adopt --dry-run")
say(" classroom-wifi --adopt")
return 0
def rollback():
"""Give the Wi-Fi back to ifupdown, exactly as it was.
The migration does this by itself if the network does not come back.
This is the same path with a person deciding, for the case where the
network came back and something else about it is wrong.
"""
backup = f"{INTERFACES}.before-ekklesia-classroom"
if not os.path.exists(backup):
say(f"there is no {backup}; nothing was migrated from this file")
return 1
with open(backup, "r", encoding="utf-8") as handle:
original = handle.read()
found = ifupdown.wireless(ifupdown.parse(original), is_wireless)
device = found[0]["name"] if found else None
ssid, _ = ifupdown.credentials(found[0]) if found else (None, None)
return 0 if restore(device, ssid) else 1
def adopt(dry_run=False):
"""Move an installer-configured Wi-Fi from ifupdown to NetworkManager.
Ordered so that the link is given up as late as possible and put back
the moment anything goes wrong. The connection is created first and
sits unused; the file is changed next; only then is the interface
handed over, and if the room is not back on the network within
ADOPT_TIMEOUT the file is restored and ifupdown brought back.
"""
stanza, text = find_stanza()
if text is None:
return 1
if stanza is None:
say("no wireless interface is configured in /etc/network/interfaces")
say("nothing to adopt; this machine's Wi-Fi is already "
"NetworkManager's or it has none")
return 0
device = stanza["name"]
found = ifupdown.profile(stanza)
if found is None:
say(f"{device} is configured through a wpa_supplicant.conf rather "
"than named in this file")
say("that one is not ours to move; adopt it by hand")
return 1
if found["key_mgmt"] is None:
say(f"{device} uses a security this command does not recreate "
f"({stanza['options'].get('wpa-key-mgmt')})")
say("refusing to guess; a network moved wrong is a network that "
"cannot come back")
return 1
ssid = found["ssid"]
psk = found["psk"]
# Never printed. The room's password is not diagnostic output.
say(f"{device}: ifupdown holds a wireless network "
f"({'with' if psk else 'without'} a saved password"
f"{', hidden' if found['hidden'] else ''})")
# A keyfile naming this device unmanaged survives the stanza going
# away, so the hand-over would end with the interface belonging to
# nobody. Said before anything is touched, and not edited: a file an
# administrator wrote is not this command's to rewrite.
for path in unmanaged_by_config(device):
say(f"WARNING: {path} names {device} unmanaged")
say(" NetworkManager will not keep it after a restart "
"until that is changed")
# Refuse to start a hand-over that cannot be undone. ifdown releases
# the interface and ifup is the whole of the rollback; without them
# this would be a one-way door.
missing = [tool for tool in ("ifdown", "ifup")
if shutil.which(tool) is None]
if missing and not dry_run:
say(f"{' and '.join(missing)} not installed, so this could not be "
"undone")
say("leaving the network exactly as it is")
return 1
if dry_run:
if missing:
say(f"NOTE: {' and '.join(missing)} are missing here, so a real "
"run would refuse")
say(f"would create a NetworkManager system connection for {ssid!r}")
say(" 802-11-wireless.hidden: "
f"{'yes' if found['hidden'] else 'no'}")
say(f" 802-11-wireless-security.key-mgmt: {found['key_mgmt']}")
say(f"would run `ifdown {device}` while its stanza is still in "
f"{INTERFACES}")
say(f"would then comment that stanza out of {INTERFACES}")
say("would hand the interface over, and put it back if that failed")
return 0
already = any(name == ssid for name, _uuid, _kind in connections())
if already:
# Reused as it stands. An existing connection of this name may be
# a teacher's own, or one a previous attempt left behind with the
# wrong hidden setting — which is exactly the state the
# prayer-room laptop was in — so it is corrected rather than
# trusted. Every setting the migration decides is written again.
say(f"NetworkManager already has a connection named {ssid!r}")
say("correcting it to match what ifupdown holds")
nmcli("connection", "modify", ssid,
"802-11-wireless.hidden", "yes" if found["hidden"] else "no",
"connection.autoconnect", "yes",
"802-11-wireless-security.psk-flags", "0",
check=False)
else:
if add(ssid, psk, hidden=found["hidden"]) != 0:
return 1
backup = f"{INTERFACES}.before-ekklesia-classroom"
if not os.path.exists(backup):
shutil.copy2(INTERFACES, backup)
say(f"kept the original at {backup}")
# From here the machine is mid-migration, so everything is inside a
# guard. Whatever goes wrong — a command that is not installed, a
# timeout, an interrupt — the file goes back and ifupdown gets the
# interface again. Nothing below may raise past this.
try:
# RELEASE THE INTERFACE BEFORE TAKING ITS STANZA AWAY.
#
# ifdown reads /etc/network/interfaces to find out how to bring
# an interface down. Run after the stanza is commented out it
# reports "Ignoring unknown interface" and does nothing at all —
# so ifupdown's own wpa_supplicant and DHCP client were still
# running, still holding the radio, when NetworkManager was asked
# to associate with it. Two supplicants on one interface is a
# fight NetworkManager loses, and it is the second half of why
# the prayer-room laptop rolled back.
#
# The order is therefore: down first, while the file still says
# how; rewrite the file second. The window in which the machine
# is offline is the same either way.
result = run("ifdown", device)
if result is not None and result.returncode != 0:
# Not fatal. A machine whose interface is already down is a
# perfectly good starting point, and ifdown says so with a
# non-zero status.
say(f"ifdown {device}: "
f"{(result.stderr or result.stdout or '').strip()}")
say(f"released {device} from ifupdown")
with open(INTERFACES, "w", encoding="utf-8") as handle:
handle.write(ifupdown.comment_out(text, stanza))
say(f"took {device} out of ifupdown's hands in {INTERFACES}")
# NetworkManager only re-reads which interfaces ifupdown claims
# when its configuration is reloaded.
nmcli("general", "reload", "conf", check=False)
nmcli("device", "set", device, "managed", "yes", check=False)
# On the device, by name. `connection up <name>` alone lets
# NetworkManager choose an interface, and a machine with two
# radios could bring the network up on the wrong one — which
# `carrying` would then correctly report as a failure, and the
# migration would roll back a network that was working.
started = nmcli("connection", "up", ssid, "ifname", device,
check=False)
if started.returncode != 0:
say(f"nmcli: {(started.stderr or '').strip()}")
deadline = time.monotonic() + ADOPT_TIMEOUT
while time.monotonic() < deadline:
if carrying(ssid, device) and online():
say(f"{device} is NetworkManager's now, and the room is "
"online")
say(f"NetworkManager reports {device} "
f"{device_state(device)}")
say("the panel will show the connection at the next login")
return 0
time.sleep(3)
say("NetworkManager did not get the network back "
f"({device} is {device_state(device)})")
except BaseException as error: # noqa: BLE001 - see the comment above
say(f"the migration failed part way through: {error}")
say("rolling back")
restore(device, ssid)
# The migration did not happen, whatever the rollback managed.
return 1
def put_interfaces_back():
"""Undo this command's edit to /etc/network/interfaces.
Un-commenting the marked lines where they are, rather than copying
the backup over the whole file. The backup is written once, on the
first attempt, and never refreshed — so a whole-file restore silently
reverts anything an administrator changed about the WIRED network in
between. The marker names exactly the lines this command commented
out, and nothing else in the file is touched.
The backup is still the fallback, for a file that has been replaced
since rather than edited.
"""
backup = f"{INTERFACES}.before-ekklesia-classroom"
try:
with open(INTERFACES, "r", encoding="utf-8") as handle:
current = handle.read()
except OSError as error:
say(f"cannot read {INTERFACES}: {error}")
current = None
if current is not None:
restored, count = ifupdown.uncomment(current)
if count:
with open(INTERFACES, "w", encoding="utf-8") as handle:
handle.write(restored)
say(f"put {count} line{'' if count == 1 else 's'} back in "
f"{INTERFACES}, and changed nothing else in it")
return True
if os.path.exists(backup):
shutil.copy2(backup, INTERFACES)
say(f"no marked lines to restore; copied {backup} back instead")
return True
say(f"nothing of this command's is in {INTERFACES} to undo")
return False
def restore(device, ssid=None):
"""Put the interface back under ifupdown.
Returns True when the room is on the network again. The callers
decide what that means for an exit code: for a failed migration it is
still a failure, and for `--rollback` it is a success.
"""
put_interfaces_back()
nmcli("general", "reload", "conf", check=False)
if ssid:
nmcli("connection", "down", ssid, check=False)
if device:
nmcli("device", "set", device, "managed", "no", check=False)
result = run("ifup", device)
if result is not None and result.returncode != 0:
say(f"ifup {device}: "
f"{(result.stderr or result.stdout or '').strip()}")
say(f"ifupdown has {device} again")
for _ in range(10):
if online():
say("the room is on the network")
return True
time.sleep(3)
# THE ADAPTER IS NEVER LEFT BELONGING TO NOBODY.
#
# Above, the device has deliberately been handed back to ifupdown and
# NetworkManager told to leave it alone. That is right while ifupdown
# can actually bring it up. When ifup has failed too, the same two
# lines are a wireless adapter that neither manager owns: no
# connection, and a panel that cannot even scan for one. That is the
# state the prayer-room laptop was found in after 0.2.5, and it is
# the state that made `nmcli device set wlp2s0 managed yes` a manual
# recovery step.
#
# So the last resort is to give it to NetworkManager rather than to
# leave it with neither. A room able to pick its network from the
# panel is recoverable by somebody standing in front of it; an
# unmanaged radio is not.
if device:
say("WARNING: ifupdown did not bring the network back either")
say(f" handing {device} to NetworkManager so the adapter "
"is not left unmanaged")
nmcli("device", "set", device, "managed", "yes", check=False)
if ssid:
nmcli("connection", "up", ssid, "ifname", device, check=False)
for _ in range(5):
if online():
say("the room is on the network, through NetworkManager")
say(f"NOTE: {INTERFACES} still describes {device}, so "
"ifupdown will")
say(" claim it again at the next boot. Decide which "
"one owns it.")
return True
time.sleep(3)
say(f" {device} is {device_state(device)}")
say("WARNING: the machine may be offline. The original "
f"{INTERFACES} is restored; try `ifup {device}` or a reboot")
return False
def main():
parser = argparse.ArgumentParser(
description="Provision the church Wi-Fi as a system connection.")
parser.add_argument("ssid", nargs="?", help="the network name")
parser.add_argument("--name", help="connection name, if not the SSID")
parser.add_argument("--list", action="store_true",
help="list the wireless connections on this machine")
parser.add_argument("--forget", metavar="NAME",
help="remove a saved connection")
parser.add_argument("--open", action="store_true",
help="the network has no password")
parser.add_argument("--hidden", action="store_true",
help="the access point does not broadcast its name")
parser.add_argument("--adopt", action="store_true",
help="move an installer-configured Wi-Fi from "
"ifupdown into NetworkManager")
parser.add_argument("--dry-run", action="store_true",
help="with --adopt, say what would change")
parser.add_argument("--check", action="store_true",
help="say who owns the Wi-Fi; changes nothing")
parser.add_argument("--rollback", action="store_true",
help="give the Wi-Fi back to ifupdown")
arguments = parser.parse_args()
if arguments.list:
return listing()
if os.geteuid() != 0:
say("run this as root; it creates a connection the machine owns")
return 1
if arguments.check:
return check()
if arguments.rollback:
return rollback()
if arguments.adopt:
return adopt(dry_run=arguments.dry_run)
if arguments.forget:
return forget(arguments.forget)
if not arguments.ssid:
parser.print_help()
return 2
password = ""
if not arguments.open:
# Never echoed, and never read from the command line, where it
# would sit in a shell history and in ps.
password = getpass.getpass("Wi-Fi password (not shown): ")
if not password:
say("no password given")
return 1
return add(arguments.ssid, password, arguments.name,
hidden=arguments.hidden)
if __name__ == "__main__":
sys.exit(main())