-
-
Notifications
You must be signed in to change notification settings - Fork 260
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Determine network status based on ip command (#1827)
Related #1814. This PR refactors the backend logic of how we determine the connection status of the network interfaces. It extends the response structure so that we will be able to show IP address and MAC address in a separate network dialog. The current WiFi dialog also uses this endpoint, so there needs to be a small adjustment to accommodate the new structure there. Notes: - I renamed the `network.status()` method to `network.determine_network_status()`, to reflect that it’s now actively running a command. In order to avoid code duplication, `network.determine_network_status()` is actually just a convenience wrapper around `network.inspect_interface()`, which also encapsulates the technical interface names `eth0` and `wlan0`. I also renamed and restructured the `NetworkStatus` class to `InterfaceStatus`. - I found the JSON output structure of the `ip` command a bit hard to predict, as I saw numerous invariants of which fields were present during testing, which I wasn’t able to reproduce. I also couldn’t find reliable documentation for it. I therefore made the code inside the `network.inspect_interface()` method very defensive and failure-tolerant, to avoid producing erratic errors. I thought in the end it would be more user-friendly to show a network interface as down, even if that’s a false positive/negative, as opposed to potentially showing a lot of errors and risking to give the feature an overall brittle impression. - For consistency [with the Wake on LAN feature](https://github.com/tiny-pilot/tinypilot-pro/blob/a2ee01de5d97af4f81b3bb60e2af3f61abb8ed63/app/request_parsers/wake_on_lan.py#L14), I normalized the MAC address to use dashes as delimiter. <a data-ca-tag href="https://codeapprove.com/pr/tiny-pilot/tinypilot/1827"><img src="https://codeapprove.com/external/github-tag-allbg.png" alt="Review on CodeApprove" /></a> --------- Co-authored-by: Jan Heuermann <[email protected]>
- Loading branch information
1 parent
6b029a4
commit 69519d8
Showing
5 changed files
with
249 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
import subprocess | ||
import unittest | ||
from unittest import mock | ||
|
||
import network | ||
|
||
|
||
# This test checks the various potential JSON output values that the underlying | ||
# `ip` command may return. | ||
class InspectInterfaceTest(unittest.TestCase): | ||
|
||
@mock.patch.object(subprocess, 'check_output') | ||
def test_treats_empty_response_as_inactive_interface(self, mock_cmd): | ||
mock_cmd.return_value = '' | ||
self.assertEqual( | ||
network.InterfaceStatus(False, None, None), | ||
network.inspect_interface('eth0'), | ||
) | ||
|
||
@mock.patch.object(subprocess, 'check_output') | ||
def test_treats_empty_array_as_inactive_interface(self, mock_cmd): | ||
mock_cmd.return_value = '[]' | ||
self.assertEqual( | ||
network.InterfaceStatus(False, None, None), | ||
network.inspect_interface('eth0'), | ||
) | ||
|
||
@mock.patch.object(subprocess, 'check_output') | ||
def test_treats_emtpy_object_as_inactive_interface(self, mock_cmd): | ||
mock_cmd.return_value = '[{}]' | ||
self.assertEqual( | ||
network.InterfaceStatus(False, None, None), | ||
network.inspect_interface('eth0'), | ||
) | ||
|
||
@mock.patch.object(subprocess, 'check_output') | ||
def test_disregards_command_failure(self, mock_cmd): | ||
mock_cmd.side_effect = mock.Mock( | ||
side_effect=subprocess.CalledProcessError(returncode=1, cmd='ip')) | ||
self.assertEqual( | ||
network.InterfaceStatus(False, None, None), | ||
network.inspect_interface('eth0'), | ||
) | ||
|
||
@mock.patch.object(subprocess, 'check_output') | ||
def test_parses_operstate_down_as_not_connected(self, mock_cmd): | ||
mock_cmd.return_value = """ | ||
[{"operstate":"DOWN"}] | ||
""" | ||
self.assertEqual( | ||
network.InterfaceStatus(False, None, None), | ||
network.inspect_interface('eth0'), | ||
) | ||
|
||
@mock.patch.object(subprocess, 'check_output') | ||
def test_parses_operstate_up_as_connected(self, mock_cmd): | ||
mock_cmd.return_value = """ | ||
[{"operstate":"UP"}] | ||
""" | ||
self.assertEqual( | ||
network.InterfaceStatus(True, None, None), | ||
network.inspect_interface('eth0'), | ||
) | ||
|
||
@mock.patch.object(subprocess, 'check_output') | ||
def test_parses_mac_address(self, mock_cmd): | ||
mock_cmd.return_value = """ | ||
[{"address":"00-b0-d0-63-c2-26"}] | ||
""" | ||
self.assertEqual( | ||
network.InterfaceStatus(False, None, '00-b0-d0-63-c2-26'), | ||
network.inspect_interface('eth0'), | ||
) | ||
|
||
@mock.patch.object(subprocess, 'check_output') | ||
def test_normalizes_mac_address_to_use_dashes(self, mock_cmd): | ||
mock_cmd.return_value = """ | ||
[{"address":"00:b0:d0:63:c2:26"}] | ||
""" | ||
self.assertEqual( | ||
network.InterfaceStatus(False, None, '00-b0-d0-63-c2-26'), | ||
network.inspect_interface('eth0'), | ||
) | ||
|
||
@mock.patch.object(subprocess, 'check_output') | ||
def test_parses_ip_address(self, mock_cmd): | ||
mock_cmd.return_value = """ | ||
[{"addr_info":[{"family":"inet","local":"192.168.2.5"}]}] | ||
""" | ||
self.assertEqual( | ||
network.InterfaceStatus(False, '192.168.2.5', None), | ||
network.inspect_interface('eth0'), | ||
) | ||
|
||
@mock.patch.object(subprocess, 'check_output') | ||
def test_disregards_other_families_such_as_ipv6(self, mock_cmd): | ||
mock_cmd.return_value = """ | ||
[{"addr_info":[{"family":"inet6","local":"::ffff:c0a8:205"}]}] | ||
""" | ||
self.assertEqual( | ||
network.InterfaceStatus(False, None, None), | ||
network.inspect_interface('eth0'), | ||
) | ||
|
||
@mock.patch.object(subprocess, 'check_output') | ||
def test_parses_all_data(self, mock_cmd): | ||
mock_cmd.return_value = """ | ||
[{ | ||
"operstate":"UP", | ||
"address":"00-b0-d0-63-c2-26", | ||
"addr_info":[{"family":"inet","local":"192.168.2.5"}] | ||
}] | ||
""" | ||
self.assertEqual( | ||
network.InterfaceStatus(True, '192.168.2.5', '00-b0-d0-63-c2-26'), | ||
network.inspect_interface('eth0'), | ||
) | ||
|
||
@mock.patch.object(subprocess, 'check_output') | ||
def test_disregards_invalid_json(self, mock_cmd): | ||
mock_cmd.return_value = '[{"address' | ||
self.assertEqual( | ||
network.InterfaceStatus(False, None, None), | ||
network.inspect_interface('eth0'), | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters