Linux lhjmq-records 5.15.0-118-generic #128-Ubuntu SMP Fri Jul 5 09:28:59 UTC 2024 x86_64
Your IP : 3.15.3.17
#!/usr/bin/python3
#
# Copyright (C) 2022 Canonical, Ltd.
# Author: Lukas Märdian <slyon@ubuntu.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
'''netplan status command line'''
import ipaddress
import json
import logging
import re
import socket
import subprocess
import sys
from typing import Union, Dict, List, Type
import dbus
import yaml
import netplan.cli.utils as utils
JSON = Union[Dict[str, 'JSON'], List['JSON'], int, str, float, bool, Type[None]]
MATCH_TAGS = re.compile(r'\[([a-z0-9]+)\].*\[\/\1\]')
RICH_OUTPUT = False
try:
from rich.console import Console
from rich.highlighter import RegexHighlighter
from rich.theme import Theme
class NetplanHighlighter(RegexHighlighter):
base_style = 'netplan.'
highlights = [
r'(^|[\s\/])(?P<int>\d+)([\s:]?\s|$)',
r'(?P<str>(\"|\').+(\"|\'))',
]
RICH_OUTPUT = True
except ImportError: # pragma: nocover (we mock RICH_OUTPUT, ignore the logging)
logging.debug("python3-rich not found, falling back to plain output")
class Interface():
def __extract_mac(self, ip: dict) -> str:
'''
Extract the MAC address if it's set inside the JSON data and seems to
have the correct format. Return 'None' otherwise.
'''
if len(address := ip.get('address', '')) == 17: # 6 byte MAC (+5 colons)
return address.lower()
return None
def __init__(self, ip: dict, nd_data: JSON = [], nm_data: JSON = [],
resolved_data: tuple = (None, None), route_data: tuple = (None, None)):
self.idx: int = ip.get('ifindex', -1)
self.name: str = ip.get('ifname', 'unknown')
self.adminstate: str = 'UP' if 'UP' in ip.get('flags', []) else 'DOWN'
self.operstate: str = ip.get('operstate', 'unknown').upper()
self.macaddress: str = self.__extract_mac(ip)
# Filter networkd/NetworkManager data
nm_data = nm_data or [] # avoid 'None' value on systems without NM
self.nd: JSON = next((x for x in nd_data if x['Index'] == self.idx), None)
self.nm: JSON = next((x for x in nm_data if x['device'] == self.name), None)
# Filter resolved's DNS data
self.dns_addresses: list = None
if resolved_data[0]:
self.dns_addresses = []
for itr in resolved_data[0]:
if int(itr[0]) == int(self.idx):
ipfamily = itr[1]
dns = itr[2]
self.dns_addresses.append(socket.inet_ntop(ipfamily, b''.join([v.to_bytes(1, 'big') for v in dns])))
self.dns_search: list = None
if resolved_data[1]:
self.dns_search = []
for v in resolved_data[1]:
if int(v[0]) == int(self.idx):
self.dns_search.append(str(v[1]))
# Filter route data
_routes: list = []
self.routes: list = None
if route_data[0]:
_routes += route_data[0]
if route_data[1]:
_routes += route_data[1]
if _routes:
self.routes = []
for obj in _routes:
if obj.get('dev') == self.name:
elem = {'to': obj.get('dst')}
val = obj.get('gateway')
if val:
elem['via'] = val
val = obj.get('prefsrc')
if val:
elem['from'] = val
val = obj.get('metric')
if val:
elem['metric'] = val
val = obj.get('type')
if val:
elem['type'] = val
val = obj.get('scope')
if val:
elem['scope'] = val
val = obj.get('protocol')
if val:
elem['protocol'] = val
self.routes.append(elem)
self.addresses: list = None
if addr_info := ip.get('addr_info'):
self.addresses = []
for addr in addr_info:
flags: list = []
if ipaddress.ip_address(addr['local']).is_link_local:
flags.append('link')
if self.routes:
for route in self.routes:
if ('from' in route and
ipaddress.ip_address(route['from']) == ipaddress.ip_address(addr['local'])):
if route['protocol'] == 'dhcp':
flags.append('dhcp')
break
ip_addr = addr['local'].lower()
elem = {ip_addr: {'prefix': addr['prefixlen']}}
if flags:
elem[ip_addr]['flags'] = flags
self.addresses.append(elem)
self.iproute_type: str = None
if info_kind := ip.get('linkinfo', {}).get('info_kind'):
self.iproute_type = info_kind.strip()
# workaround: query some data which is not available via networkctl's JSON output
self._networkctl: str = self.query_networkctl(self.name) or ''
def query_nm_ssid(self, con_name: str) -> str:
ssid: str = None
try:
ssid = utils.nmcli_out(['--get-values', '802-11-wireless.ssid',
'con', 'show', 'id', con_name])
return ssid.strip()
except Exception as e:
logging.warning('Cannot query NetworkManager SSID for {}: {}'.format(
con_name, str(e)))
return ssid
def query_networkctl(self, ifname: str) -> str:
output: str = None
try:
output = subprocess.check_output(['networkctl', 'status', '--', ifname], text=True)
except Exception as e:
logging.warning('Cannot query networkctl for {}: {}'.format(
ifname, str(e)))
return output
def json(self) -> JSON:
json = {
'index': self.idx,
'adminstate': self.adminstate,
'operstate': self.operstate,
}
if self.type:
json['type'] = self.type
if self.ssid:
json['ssid'] = self.ssid
if self.tunnel_mode:
json['tunnel_mode'] = self.tunnel_mode
if self.backend:
json['backend'] = self.backend
if self.netdef_id:
json['id'] = self.netdef_id
if self.macaddress:
json['macaddress'] = self.macaddress
if self.vendor:
json['vendor'] = self.vendor
if self.addresses:
json['addresses'] = self.addresses
if self.dns_addresses:
json['dns_addresses'] = self.dns_addresses
if self.dns_search:
json['dns_search'] = self.dns_search
if self.routes:
json['routes'] = self.routes
if self.activation_mode:
json['activation_mode'] = self.activation_mode
return (self.name, json)
@property
def up(self) -> bool:
return self.adminstate == 'UP' and self.operstate == 'UP'
@property
def down(self) -> bool:
return self.adminstate == 'DOWN' and self.operstate == 'DOWN'
@property
def type(self) -> str:
match = dict({
'bond': 'bond',
'bridge': 'bridge',
'ether': 'ethernet',
'ipgre': 'tunnel',
'ip6gre': 'tunnel',
'loopback': 'ethernet',
'sit': 'tunnel',
'tunnel': 'tunnel',
'tunnel6': 'tunnel',
'wireguard': 'tunnel',
'wlan': 'wifi',
'wwan': 'modem',
'vrf': 'vrf',
'vxlan': 'tunnel',
})
nd_type = self.nd.get('Type') if self.nd else None
if nd_type in match:
return match[nd_type]
logging.warning('Unknown device type: {}'.format(nd_type))
return None
@property
def tunnel_mode(self) -> str:
if self.type == 'tunnel' and self.iproute_type:
return self.iproute_type
return None
@property
def backend(self) -> str:
if (self.nd and
'unmanaged' not in self.nd.get('SetupState', '') and
'run/systemd/network/10-netplan-' in self.nd.get('NetworkFile', '')):
return 'networkd'
elif self.nm and 'run/NetworkManager/system-connections/netplan-' in self.nm.get('filename', ''):
return 'NetworkManager'
return None
@property
def netdef_id(self) -> str:
if self.backend == 'networkd':
return self.nd.get('NetworkFile', '').split(
'run/systemd/network/10-netplan-')[1].split('.network')[0]
elif self.backend == 'NetworkManager':
netdef = self.nm.get('filename', '').split(
'run/NetworkManager/system-connections/netplan-')[1].split('.nmconnection')[0]
if self.nm.get('type', '') == '802-11-wireless':
ssid = self.query_nm_ssid(self.nm.get('name'))
if ssid: # XXX: escaping needed?
netdef = netdef.split('-' + ssid)[0]
return netdef
return None
@property
def vendor(self) -> str:
if self.nd and 'Vendor' in self.nd and self.nd['Vendor']:
return self.nd['Vendor'].strip()
return None
@property
def ssid(self) -> str:
if self.type == 'wifi':
# XXX: available from networkctl's JSON output as of v250:
# https://github.com/systemd/systemd/commit/da7c995
for line in self._networkctl.splitlines():
line = line.strip()
key = 'WiFi access point: '
if line.startswith(key):
ssid = line[len(key):-len(' (xB:SS:ID:xx:xx:xx)')].strip()
return ssid if ssid else None
return None
@property
def activation_mode(self) -> str:
if self.backend == 'networkd':
# XXX: available from networkctl's JSON output as of v250:
# https://github.com/systemd/systemd/commit/3b60ede
for line in self._networkctl.splitlines():
line = line.strip()
key = 'Activation Policy: '
if line.startswith(key):
mode = line[len(key):].strip()
return mode if mode != 'up' else None
# XXX: this is not fully supported on NetworkManager, only 'manual'/'up'
elif self.backend == 'NetworkManager':
return 'manual' if self.nm['autoconnect'] == 'no' else None
return None
class NetplanStatus(utils.NetplanCommand):
def __init__(self):
super().__init__(command_id='status',
description='Query networking state of the running system',
leaf=True)
self.all = False
def run(self):
self.parser.add_argument('ifname', nargs='?', type=str, default=None,
help='Show only this interface')
self.parser.add_argument('-a', '--all', action='store_true',
help='Show all interface data (incl. inactive)')
self.parser.add_argument('-f', '--format', default='tabular',
help='Output in machine readable `json` or `yaml` format')
self.func = self.command
self.parse_args()
self.run_command()
def resolvconf_json(self) -> dict:
res = {
'addresses': [],
'search': [],
'mode': None,
}
try:
with open('/etc/resolv.conf') as f:
# check first line for systemd-resolved stub or compat modes
firstline = f.readline()
if '# This is /run/systemd/resolve/stub-resolv.conf' in firstline:
res['mode'] = 'stub'
elif '# This is /run/systemd/resolve/resolv.conf' in firstline:
res['mode'] = 'compat'
for line in [firstline] + f.readlines():
if line.startswith('nameserver'):
res['addresses'] += line.split()[1:] # append
if line.startswith('search'):
res['search'] = line.split()[1:] # override
except Exception as e:
logging.warning('Cannot parse /etc/resolv.conf: {}'.format(str(e)))
return res
def query_online_state(self, interfaces: list) -> bool:
# TODO: fully implement network-online.target specification (FO020):
# https://discourse.ubuntu.com/t/spec-definition-of-an-online-system/27838
for itf in interfaces:
if itf.up and itf.addresses and itf.routes and itf.dns_addresses:
non_local_ips = []
for addr in itf.addresses:
ip, extra = list(addr.items())[0]
if 'flags' not in extra or 'link' not in extra['flags']:
non_local_ips.append(ip)
default_routes = [x for x in itf.routes if x.get('to', None) == 'default']
if non_local_ips and default_routes and itf.dns_addresses:
return True
return False
def process_generic(self, cmd_output: str) -> JSON:
return json.loads(cmd_output)
def query_iproute2(self) -> JSON:
data: JSON = None
try:
output: str = subprocess.check_output(['ip', '-d', '-j', 'addr'],
text=True)
data = self.process_generic(output)
except Exception as e:
logging.critical('Cannot query iproute2 interface data: {}'.format(str(e)))
return data
def process_networkd(self, cmd_output) -> JSON:
return json.loads(cmd_output)['Interfaces']
def query_networkd(self) -> JSON:
data: JSON = None
try:
output: str = subprocess.check_output(['networkctl', '--json=short'],
text=True)
data = self.process_networkd(output)
except Exception as e:
logging.critical('Cannot query networkd interface data: {}'.format(str(e)))
return data
def process_nm(self, cmd_output) -> JSON:
data: JSON = []
for line in cmd_output.splitlines():
split = line.split(':')
dev = split[0] if split[0] else None
if dev: # ignore inactive connection profiles
data.append({
'device': dev,
'name': split[1],
'uuid': split[2],
'filename': split[3],
'type': split[4],
'autoconnect': split[5],
})
return data
def query_nm(self) -> JSON:
data: JSON = None
try:
output: str = utils.nmcli_out(['-t', '-f',
'DEVICE,NAME,UUID,FILENAME,TYPE,AUTOCONNECT',
'con', 'show'])
data = self.process_nm(output)
except Exception as e:
logging.debug('Cannot query NetworkManager interface data: {}'.format(str(e)))
return data
def query_routes(self) -> tuple:
data4 = None
data6 = None
try:
output4: str = subprocess.check_output(['ip', '-d', '-j', 'route'],
text=True)
data4: JSON = self.process_generic(output4)
output6: str = subprocess.check_output(['ip', '-d', '-j', '-6', 'route'],
text=True)
data6: JSON = self.process_generic(output6)
except Exception as e:
logging.debug('Cannot query iproute2 route data: {}'.format(str(e)))
return (data4, data6)
def query_resolved(self) -> tuple:
addresses = None
search = None
try:
ipc = dbus.SystemBus()
resolve1 = ipc.get_object('org.freedesktop.resolve1', '/org/freedesktop/resolve1')
resolve1_if = dbus.Interface(resolve1, 'org.freedesktop.DBus.Properties')
res = resolve1_if.GetAll('org.freedesktop.resolve1.Manager')
addresses = res['DNS']
search = res['Domains']
except Exception as e:
logging.debug('Cannot query resolved DNS data: {}'.format(str(e)))
return (addresses, search)
def plain_print(self, *args, **kwargs):
if len(args):
lst = list(args)
for tag in MATCH_TAGS.findall(lst[0]):
# remove matching opening and closing tag
lst[0] = lst[0].replace('[{}]'.format(tag), '')\
.replace('[/{}]'.format(tag), '')
return print(*lst, **kwargs)
return print(*args, **kwargs)
def pretty_print(self, data: JSON, total: int, _console_width=None) -> None:
if RICH_OUTPUT:
# TODO: Use a proper (subiquity?) color palette
theme = Theme({
'netplan.int': 'bold cyan',
'netplan.str': 'yellow',
'muted': 'grey62',
'online': 'green bold',
'offline': 'red bold',
'unknown': 'yellow bold',
'highlight': 'bold'
})
console = Console(highlighter=NetplanHighlighter(), theme=theme,
width=_console_width, emoji=False)
pprint = console.print
else:
pprint = self.plain_print
pad = '18'
global_state = data.get('netplan-global-state', {})
interfaces = [(key, data[key]) for key in data if key != 'netplan-global-state']
# Global state
pprint(('{title:>'+pad+'} {value}').format(
title='Online state:',
value='[online]online[/online]' if global_state.get('online', False) else '[offline]offline[/offline]',
))
ns = global_state.get('nameservers', {})
dns_addr: list = ns.get('addresses', [])
dns_mode: str = ns.get('mode')
dns_search: list = ns.get('search', [])
if dns_addr:
for i, val in enumerate(dns_addr):
pprint(('{title:>'+pad+'} {value}[muted]{mode}[/muted]').format(
title='DNS Addresses:' if i == 0 else '',
value=val,
mode=' ({})'.format(dns_mode) if dns_mode else '',
))
if dns_search:
for i, val in enumerate(dns_search):
pprint(('{title:>'+pad+'} {value}').format(
title='DNS Search:' if i == 0 else '',
value=val,
))
pprint()
# Per interface
for (ifname, data) in interfaces:
state = data.get('operstate', 'UNKNOWN') + '/' + data.get('adminstate', 'UNKNOWN')
scolor = 'unknown'
if state == 'UP/UP':
state = 'UP'
scolor = 'online'
elif state == 'DOWN/DOWN':
state = 'DOWN'
scolor = 'offline'
full_type = data.get('type', 'other')
ssid = data.get('ssid')
tunnel_mode = data.get('tunnel_mode')
if full_type == 'wifi' and ssid:
full_type += ('/"' + ssid + '"')
elif full_type == 'tunnel' and tunnel_mode:
full_type += ('/' + tunnel_mode)
pprint('[{col}]●[/{col}] {idx:>2}: {name} {type} [{col}]{state}[/{col}] ({backend}{netdef})'.format(
col=scolor,
idx=data.get('index', '?'),
name=ifname,
type=full_type,
state=state,
backend=data.get('backend', 'unmanaged'),
netdef=': [highlight]{}[/highlight]'.format(data.get('id')) if data.get('id') else ''
))
if data.get('macaddress'):
pprint(('{title:>'+pad+'} {mac}[muted]{vendor}[/muted]').format(
title='MAC Address:',
mac=data.get('macaddress', ''),
vendor=' ({})'.format(data.get('vendor', '')) if data.get('vendor') else '',
))
lst: list = data.get('addresses', [])
if lst:
for i, obj in enumerate(lst):
ip, extra = list(obj.items())[0] # get first (any only) address
flags = []
if extra.get('flags'): # flags
flags = extra.get('flags', [])
highlight_start = ''
highlight_end = ''
if not flags or 'dhcp' in flags:
highlight_start = '[highlight]'
highlight_end = '[/highlight]'
pprint(('{title:>'+pad+'} {start}{ip}/{prefix}{end}[muted]{extra}[/muted]').format(
title='Addresses:' if i == 0 else '',
ip=ip,
prefix=extra.get('prefix', ''),
extra=' ('+', '.join(flags)+')' if flags else '',
start=highlight_start,
end=highlight_end,
))
lst = data.get('dns_addresses', [])
if lst:
for i, val in enumerate(lst):
pprint(('{title:>'+pad+'} {value}').format(
title='DNS Addresses:' if i == 0 else '',
value=val,
))
lst = data.get('dns_search', [])
if lst:
for i, val in enumerate(lst):
pprint(('{title:>'+pad+'} {value}').format(
title='DNS Search:' if i == 0 else '',
value=val,
))
lst = data.get('routes', [])
if lst:
for i, obj in enumerate(lst):
default_start = ''
default_end = ''
if obj['to'] == 'default':
default_start = '[highlight]'
default_end = '[/highlight]'
via = ''
if 'via' in obj:
via = ' via ' + obj['via']
src = ''
if 'from' in obj:
src = ' from ' + obj['from']
metric = ''
if 'metric' in obj:
metric = ' metric ' + str(obj['metric'])
extra = []
if 'protocol' in obj and obj['protocol'] != 'kernel':
proto = obj['protocol']
extra.append(proto)
if 'scope' in obj and obj['scope'] != 'global':
scope = obj['scope']
extra.append(scope)
if 'type' in obj and obj['type'] != 'unicast':
type = obj['type']
extra.append(type)
pprint(('{title:>'+pad+'} {start}{to}{via}{src}{metric}{end}[muted]{extra}[/muted]').format(
title='Routes:' if i == 0 else '',
to=obj['to'],
via=via,
src=src,
metric=metric,
extra=' ('+', '.join(extra)+')' if extra else '',
start=default_start,
end=default_end))
val = data.get('activation_mode')
if val:
pprint(('{title:>'+pad+'} {value}').format(
title='Activation Mode:',
value=val,
))
pprint()
hidden = total - len(interfaces)
if (hidden > 0):
pprint('{} inactive interfaces hidden. Use "--all" to show all.'.format(hidden))
def command(self):
# Make sure sd-networkd is running, as we need the data it provides.
if not utils.systemctl_is_active('systemd-networkd.service'):
if utils.systemctl_is_masked('systemd-networkd.service'):
logging.error('\'netplan status\' depends on networkd, '
'but systemd-networkd.service is masked. '
'Please start it.')
sys.exit(1)
logging.debug('systemd-networkd.service is not active. Starting...')
utils.systemctl('start', ['systemd-networkd.service'], True)
# required data: iproute2 and sd-networkd can be expected to exist,
# due to hard package dependencies
iproute2 = self.query_iproute2()
networkd = self.query_networkd()
if not iproute2 or not networkd:
logging.error('Could not query iproute2 or systemd-networkd')
sys.exit(1)
# optional data
nmcli = self.query_nm()
route4, route6 = self.query_routes()
dns_addresses, dns_search = self.query_resolved()
interfaces = [Interface(itf, networkd, nmcli, (dns_addresses, dns_search), (route4, route6)) for itf in iproute2]
total = len(interfaces)
# show only active interfaces by default
filtered = [itf for itf in interfaces if itf.operstate != 'DOWN']
# down interfaces do not contribute anything to the online state
online_state = self.query_online_state(filtered)
# show only a single interface, if requested
# XXX: bash completion (for interfaces names)
if self.ifname:
filtered = [next((itf for itf in interfaces if itf.name == self.ifname), None)]
filtered = [elem for elem in filtered if elem is not None]
if self.ifname and filtered == []:
logging.error('Could not find interface {}'.format(self.ifname))
sys.exit(1)
# Global state
data = {
'netplan-global-state': {
'online': online_state,
'nameservers': self.resolvconf_json()
}
}
# Per interface
itf_iter = interfaces if self.all else filtered
for itf in itf_iter:
ifname, obj = itf.json()
data[ifname] = obj
# Output data in requested format
output_format = self.format.lower()
if output_format == 'json': # structural JSON output
print(json.dumps(data, indent=None))
elif output_format == 'yaml': # stuctural YAML output
print(yaml.dump(data, default_flow_style=False))
else: # pretty print, human readable output
self.pretty_print(data, total)
|