Skip to content

Commit

Permalink
feat(api): Add /networking/status endpoint to get all interface info (#…
Browse files Browse the repository at this point in the history
…2471)

Closes #2445
  • Loading branch information
mcous authored Oct 12, 2018
1 parent 241a8a3 commit 7555e26
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -276,32 +276,65 @@ async def configure(request: web.Request) -> web.Response:

async def status(request: web.Request) -> web.Response:
"""
Get request will return the status of the wifi connection from the
RaspberryPi to the internet.
Get request will return the status of the machine's connection to the
internet as well as the status of its network interfaces.
The body of the response is a json dict containing
'status': connectivity status, where the options are:
'status': internet connectivity status, where the options are:
"none" - no connection to router or network
"portal" - device behind a captive portal and cannot reach full internet
"limited" - connection to router but not internet
"full" - connection to router and internet
"unknown" - an exception occured while trying to determine status
'ipAddress': the ip address, if it exists (null otherwise); this also
contains the subnet mask in CIDR notation, e.g. 10.2.12.120/16
'macAddress': the mac address
'gatewayAddress': the address of the current gateway, if it exists (null
otherwise)
'interfaces': JSON object of networking interfaces, keyed by device name,
where the value of each entry is another object with the keys:
- 'type': "ethernet" or "wifi"
- 'state': state string, e.g. "disconnected", "connecting", "connected"
- 'ipAddress': the ip address, if it exists (null otherwise); this also
contains the subnet mask in CIDR notation, e.g. 10.2.12.120/16
- 'macAddress': the MAC address of the interface device
- 'gatewayAddress': the address of the current gateway, if it exists
(null otherwise)
Example request:
```
GET /networking/status
```
Example response:
```
200 OK
{
"status": "full",
"interfaces": {
"wlan0": {
"ipAddress": "192.168.43.97/24",
"macAddress": "B8:27:EB:6C:95:CF",
"gatewayAddress": "192.168.43.161",
"state": "connected",
"type": "wifi"
},
"eth0": {
"ipAddress": "169.254.229.173/16",
"macAddress": "B8:27:EB:39:C0:9A",
"gatewayAddress": null,
"state": "connected",
"type": "ethernet"
}
}
}
```
"""
connectivity = {'status': 'none',
'ipAddress': None,
'macAddress': 'unknown',
'gatewayAddress': None}
connectivity = {'status': 'none', 'interfaces': {}}
try:
connectivity['status'] = await nmcli.is_connected()
net_info = await nmcli.iface_info(nmcli.NETWORK_IFACES.WIFI)
connectivity.update(net_info)
connectivity['interfaces'] = {
i.value: await nmcli.iface_info(i) for i in nmcli.NETWORK_IFACES
}
log.debug("Connectivity: {}".format(connectivity['status']))
log.debug("Interfaces: {}".format(connectivity['interfaces']))
status = 200
except subprocess.CalledProcessError as e:
log.error("CalledProcessError: {}".format(e.stdout))
Expand Down
20 changes: 11 additions & 9 deletions api/src/opentrons/server/http.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging
from . import endpoints as endp
from .endpoints import (wifi, control, settings, update)
from .endpoints import (networking, control, settings, update)
from opentrons.deck_calibration import endpoints as dc_endp


Expand All @@ -16,16 +16,18 @@ def __init__(self, app, log_file_path):
self.app.router.add_get(
'/health', endp.health)
self.app.router.add_get(
'/wifi/list', wifi.list_networks)
self.app.router.add_post(
'/wifi/configure', wifi.configure)
'/networking/status', networking.status)
# TODO(mc, 2018-10-12): s/wifi/networking
self.app.router.add_get(
'/wifi/status', wifi.status)
self.app.router.add_post('/wifi/keys', wifi.add_key)
self.app.router.add_get('/wifi/keys', wifi.list_keys)
self.app.router.add_delete('/wifi/keys/{key_uuid}', wifi.remove_key)
'/wifi/list', networking.list_networks)
self.app.router.add_post(
'/wifi/configure', networking.configure)
self.app.router.add_post('/wifi/keys', networking.add_key)
self.app.router.add_get('/wifi/keys', networking.list_keys)
self.app.router.add_delete(
'/wifi/keys/{key_uuid}', networking.remove_key)
self.app.router.add_get(
'/wifi/eap-options', wifi.eap_options)
'/wifi/eap-options', networking.eap_options)
self.app.router.add_post(
'/identify', control.identify)
self.app.router.add_get(
Expand Down
59 changes: 44 additions & 15 deletions api/src/opentrons/system/nmcli.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,8 +457,8 @@ async def configure(ssid: str,
# This unfortunately doesn’t respect the --terse flag, so we need to
# regex out the name or the uuid to use later in connection up; the
# uuid is slightly more regular, so that’s what we use.
uuid_matches = re.search( # noqa
"Connection '(.*)'[\s]+\(([\w\d-]+)\) successfully", res) # noqa
uuid_matches = re.search( # noqa
"Connection '(.*)'[\s]+\(([\w\d-]+)\) successfully", res) # noqa
if not uuid_matches:
return False, err.split('\r')[-1]
name = uuid_matches.group(1)
Expand Down Expand Up @@ -507,29 +507,58 @@ async def iface_info(which_iface: NETWORK_IFACES) -> Dict[str, Optional[str]]:
which_iface should be a string in IFACE_NAMES.
"""
default_res: Dict[str, Optional[str]] = {'ipAddress': None,
'macAddress': None,
'gatewayAddress': None}
fields = ['GENERAL.HWADDR', 'IP4.ADDRESS', 'IP4.GATEWAY', 'GENERAL.STATE']
# example device info lines
# GENERAL.HWADDR:B8:27:EB:24:D1:D0
# IP4.ADDRESS[1]:10.10.2.221/22
# capture the field name (without the number in brackets) and the value
# using regex instead of split because there may be ":" in the value
_DEV_INFO_LINE_RE = re.compile(r'([\w.]+)(?:\[\d+])?:(.*)')
# example device info: 30 (disconnected)
# capture the string without the number
_IFACE_STATE_RE = re.compile(r'\d+ \((.+)\)')

info: Dict[str, Optional[str]] = {'ipAddress': None,
'macAddress': None,
'gatewayAddress': None,
'state': None,
'type': None}
fields = ['GENERAL.HWADDR', 'IP4.ADDRESS',
'IP4.GATEWAY', 'GENERAL.TYPE', 'GENERAL.STATE']
# Note on this specific command: Most nmcli commands default to a tabular
# output mode, where if there are multiple things to pull a couple specific
# fields from it you’ll get a table where rows are, say, connections, and
# columns are field name. However, specifically ‘con show <con-name>‘ and
# ‘dev show <dev-name>’ default to a multiline representation, and even if
# explicitly ask for it to be tabular, it’s not quite the same as the other
# commands. So we have to special-case the parsing.
res, err = await _call(['--mode', 'tabular',
res, err = await _call(['--mode', 'multiline',
'--escape', 'no',
'--terse', '--fields', ','.join(fields),
'dev', 'show', which_iface.value])
values = res.split('\n')
if len(fields) != len(values):
# We failed
raise ValueError("Bad result from nmcli: {}".format(err))
default_res['macAddress'] = values[0]
default_res['ipAddress'] = values[1]
default_res['gatewayAddress'] = values[2]
return default_res

field_map = {}
for line in res.split('\n'):
# pull the key (without brackets) and the value out of the line
match = _DEV_INFO_LINE_RE.fullmatch(line)
if match is None:
raise ValueError(
"Bad nmcli result; out: {}; err: {}".format(res, err))
key, val = match.groups()
# nmcli can put "--" instead of "" for None
field_map[key] = None if val == '--' else val

info['macAddress'] = field_map.get('GENERAL.HWADDR')
info['ipAddress'] = field_map.get('IP4.ADDRESS')
info['gatewayAddress'] = field_map.get('IP4.GATEWAY')
info['type'] = field_map.get('GENERAL.TYPE')
state_val = field_map.get('GENERAL.STATE')

if state_val:
state_match = _IFACE_STATE_RE.fullmatch(state_val)
if state_match:
info['state'] = state_match.group(1)

return info


async def _call(cmd: List[str]) -> Tuple[str, str]:
Expand Down
Loading

0 comments on commit 7555e26

Please sign in to comment.