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]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
fe08363534
commit
30d2d99230
+345
-39
@@ -77,6 +77,11 @@ 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:
|
||||
@@ -86,14 +91,57 @@ def nmcli(*args, check=True):
|
||||
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():
|
||||
result = nmcli("-t", "-f", "NAME,TYPE,UUID", "connection", "show")
|
||||
|
||||
for line in result.stdout.splitlines():
|
||||
parts = line.split(":")
|
||||
|
||||
if len(parts) >= 2 and "wireless" in parts[1]:
|
||||
print(f" {parts[0]}")
|
||||
for name, _uuid, kind in connections():
|
||||
if "wireless" in kind:
|
||||
print(f" {name}")
|
||||
|
||||
return 0
|
||||
|
||||
@@ -105,7 +153,7 @@ def forget(name):
|
||||
return 0
|
||||
|
||||
|
||||
def add(ssid, password, name=None):
|
||||
def add(ssid, password, name=None, hidden=False):
|
||||
name = name or ssid
|
||||
|
||||
# Deliberately no connection.permissions. That single omission is the
|
||||
@@ -121,6 +169,15 @@ def add(ssid, password, name=None):
|
||||
# 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:
|
||||
@@ -131,9 +188,13 @@ def add(ssid, password, name=None):
|
||||
|
||||
nmcli(*arguments)
|
||||
|
||||
path = os.path.join(CONNECTIONS, f"{name}.nmconnection")
|
||||
if hidden:
|
||||
say(f"{name} is a hidden network; NetworkManager will probe for it "
|
||||
"by name")
|
||||
|
||||
if os.path.exists(path):
|
||||
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.
|
||||
@@ -148,6 +209,28 @@ def add(ssid, password, name=None):
|
||||
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.
|
||||
|
||||
@@ -198,14 +281,71 @@ def carrying(name, device):
|
||||
"--active", check=False)
|
||||
|
||||
for line in (result.stdout or "").splitlines():
|
||||
parts = line.rsplit(":", 1)
|
||||
fields = terse(line)
|
||||
|
||||
if len(parts) == 2 and parts[0] == name and parts[1] == device:
|
||||
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):
|
||||
@@ -249,20 +389,33 @@ def check():
|
||||
return 0
|
||||
|
||||
device = stanza["name"]
|
||||
ssid, _ = ifupdown.credentials(stanza)
|
||||
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 ssid:
|
||||
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")
|
||||
else:
|
||||
say(" Its credentials are not in that file, so it has to be "
|
||||
"moved by hand.")
|
||||
|
||||
return 0
|
||||
|
||||
@@ -312,17 +465,37 @@ def adopt(dry_run=False):
|
||||
return 0
|
||||
|
||||
device = stanza["name"]
|
||||
ssid, psk = ifupdown.credentials(stanza)
|
||||
found = ifupdown.profile(stanza)
|
||||
|
||||
if not ssid:
|
||||
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"({'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
|
||||
@@ -342,17 +515,32 @@ def adopt(dry_run=False):
|
||||
"run would refuse")
|
||||
|
||||
say(f"would create a NetworkManager system connection for {ssid!r}")
|
||||
say(f"would comment the {device} stanza out of {INTERFACES}")
|
||||
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
|
||||
|
||||
existing = nmcli("-t", "-f", "NAME", "connection", "show", check=False)
|
||||
already = ssid in (existing.stdout or "").splitlines()
|
||||
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) != 0:
|
||||
if add(ssid, psk, hidden=found["hidden"]) != 0:
|
||||
return 1
|
||||
|
||||
backup = f"{INTERFACES}.before-ekklesia-classroom"
|
||||
@@ -366,20 +554,51 @@ def adopt(dry_run=False):
|
||||
# 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")
|
||||
say(f"took {device} out of ifupdown's hands in {INTERFACES}")
|
||||
|
||||
# NetworkManager only re-reads which interfaces ifupdown claims
|
||||
# when its configuration is reloaded. The link is still
|
||||
# ifupdown's at this point, and NetworkManager does not touch a
|
||||
# device it considers unmanaged, so this cannot drop it.
|
||||
# when its configuration is reloaded.
|
||||
nmcli("general", "reload", "conf", check=False)
|
||||
|
||||
run("ifdown", device)
|
||||
nmcli("device", "set", device, "managed", "yes", check=False)
|
||||
nmcli("connection", "up", ssid, 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
|
||||
|
||||
@@ -387,12 +606,15 @@ def adopt(dry_run=False):
|
||||
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")
|
||||
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}")
|
||||
|
||||
@@ -403,6 +625,48 @@ def adopt(dry_run=False):
|
||||
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.
|
||||
|
||||
@@ -410,11 +674,7 @@ def restore(device, ssid=None):
|
||||
decide what that means for an exit code: for a failed migration it is
|
||||
still a failure, and for `--rollback` it is a success.
|
||||
"""
|
||||
backup = f"{INTERFACES}.before-ekklesia-classroom"
|
||||
|
||||
if os.path.exists(backup):
|
||||
shutil.copy2(backup, INTERFACES)
|
||||
say(f"restored {INTERFACES} from {backup}")
|
||||
put_interfaces_back()
|
||||
|
||||
nmcli("general", "reload", "conf", check=False)
|
||||
|
||||
@@ -423,7 +683,12 @@ def restore(device, ssid=None):
|
||||
|
||||
if device:
|
||||
nmcli("device", "set", device, "managed", "no", check=False)
|
||||
run("ifup", device)
|
||||
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):
|
||||
@@ -433,6 +698,44 @@ def restore(device, ssid=None):
|
||||
|
||||
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
|
||||
@@ -449,6 +752,8 @@ def main():
|
||||
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")
|
||||
@@ -494,7 +799,8 @@ def main():
|
||||
say("no password given")
|
||||
return 1
|
||||
|
||||
return add(arguments.ssid, password, arguments.name)
|
||||
return add(arguments.ssid, password, arguments.name,
|
||||
hidden=arguments.hidden)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user