Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cli:status: Make rich pretty printing optional #388

Merged
merged 2 commits into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 44 additions & 22 deletions netplan_cli/cli/commands/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,31 @@
'''netplan status command line'''

import json
import logging
import re

import yaml
from rich.console import Console
from rich.highlighter import RegexHighlighter
from rich.theme import Theme

from .. import utils
from ..state import SystemConfigState, JSON


class NetplanHighlighter(RegexHighlighter):
base_style = 'netplan.'
highlights = [
r'(^|[\s\/])(?P<int>\d+)([\s:]?\s|$)',
r'(?P<str>(\"|\').+(\"|\'))',
]
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 NetplanStatus(utils.NetplanCommand):
Expand All @@ -57,20 +66,33 @@ def run(self):
self.parse_args()
self.run_command()

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:
# 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
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', {})
Expand Down
141 changes: 92 additions & 49 deletions tests/cli/test_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,57 @@
DNS_ADDRESSES = [(5, 2, DNS_IP4), (5, 10, DNS_IP6), (2, 2, DNS_IP4), (2, 10, DNS_IP6)] # (IFidx, IPfamily, IPbytes)
DNS_SEARCH = [(5, 'search.domain', False), (2, 'search.domain', False)]
FAKE_DEV = {'ifindex': 42, 'ifname': 'fakedev0', 'flags': [], 'operstate': 'DOWN'}
STATUS_OUTPUT = '''\
Online state: online
DNS Addresses: 127.0.0.53 (stub)
DNS Search: search.domain

● 2: enp0s31f6 ethernet UP (networkd: enp0s31f6)
MAC Address: 54:e1:ad:5f:24:b4 (Intel Corporation)
Addresses: 192.168.178.62/24 (dhcp)
2001:9e8:a19f:1c00:56e1:adff:fe5f:24b4/64
fe80::56e1:adff:fe5f:24b4/64 (link)
DNS Addresses: 192.168.178.1
fd00::cece:1eff:fe3d:c737
DNS Search: search.domain
Routes: default via 192.168.178.1 from 192.168.178.62 metric 100 (dhcp)
192.168.178.0/24 from 192.168.178.62 metric 100 (link)
2001:9e8:a19f:1c00::/64 metric 100 (ra)
2001:9e8:a19f:1c00::/56 via fe80::cece:1eff:fe3d:c737 metric 100 (ra)
fe80::/64 metric 256
Activation Mode: manual

● 5: wlan0 wifi/"MYCON" UP (NetworkManager: NM-b6b7a21d-186e-45e1-b3a6-636da1735563)
MAC Address: 1c:4d:70:e4:e4:0e (Intel Corporation)
Addresses: 192.168.178.142/24
2001:9e8:a19f:1c00:7011:2d1:951:ad03/64
2001:9e8:a19f:1c00:f24f:f724:5dd1:d0ad/64
fe80::fec1:6ced:5268:b46c/64 (link)
DNS Addresses: 192.168.178.1
fd00::cece:1eff:fe3d:c737
DNS Search: search.domain
Routes: default via 192.168.178.1 metric 600 (dhcp)
192.168.178.0/24 from 192.168.178.142 metric 600 (link)
2001:9e8:a19f:1c00::/64 metric 600 (ra)
2001:9e8:a19f:1c00::/56 via fe80::cece:1eff:fe3d:c737 metric 600 (ra)
fe80::/64 metric 1024
default via fe80::cece:1eff:fe3d:c737 metric 20600 (ra)

● 41: wg0 tunnel/wireguard UNKNOWN/UP (networkd: wg0)
Addresses: 10.10.0.2/24
Routes: 10.10.0.0/24 from 10.10.0.2 (link)
Activation Mode: manual

● 48: tun0 tunnel/sit UNKNOWN/UP (networkd: tun0)
Addresses: 2001:dead:beef::2/64
Routes: 2001:dead:beef::/64 metric 256
Activation Mode: manual

● 42: fakedev0 other DOWN (unmanaged)
Routes: 10.0.0.0/16 via 10.0.0.1 (local)

1 inactive interfaces hidden. Use "--all" to show all.
'''


class TestStatus(unittest.TestCase):
Expand All @@ -53,9 +104,10 @@ def _call(self, args):
def _get_itf(self, ifname):
return next((itf for itf in yaml.safe_load(IPROUTE2) if itf['ifname'] == ifname), None)

@patch('netplan_cli.cli.commands.status.RICH_OUTPUT', False)
@patch('netplan_cli.cli.state.Interface.query_nm_ssid')
@patch('netplan_cli.cli.state.Interface.query_networkctl')
def test_pretty_print(self, networkctl_mock, nm_ssid_mock):
def test_plain_print(self, networkctl_mock, nm_ssid_mock):
SSID = 'MYCON'
nm_ssid_mock.return_value = SSID
# networkctl mock output reduced to relevant lines
Expand Down Expand Up @@ -92,57 +144,48 @@ def test_pretty_print(self, networkctl_mock, nm_ssid_mock):
status.verbose = False
status.pretty_print(data, len(interfaces)+1, _console_width=130)
out = f.getvalue()
self.assertEqual(out, '''\
Online state: online
DNS Addresses: 127.0.0.53 (stub)
DNS Search: search.domain

● 2: enp0s31f6 ethernet UP (networkd: enp0s31f6)
MAC Address: 54:e1:ad:5f:24:b4 (Intel Corporation)
Addresses: 192.168.178.62/24 (dhcp)
2001:9e8:a19f:1c00:56e1:adff:fe5f:24b4/64
fe80::56e1:adff:fe5f:24b4/64 (link)
DNS Addresses: 192.168.178.1
fd00::cece:1eff:fe3d:c737
DNS Search: search.domain
Routes: default via 192.168.178.1 from 192.168.178.62 metric 100 (dhcp)
192.168.178.0/24 from 192.168.178.62 metric 100 (link)
2001:9e8:a19f:1c00::/64 metric 100 (ra)
2001:9e8:a19f:1c00::/56 via fe80::cece:1eff:fe3d:c737 metric 100 (ra)
fe80::/64 metric 256
Activation Mode: manual

● 5: wlan0 wifi/"MYCON" UP (NetworkManager: NM-b6b7a21d-186e-45e1-b3a6-636da1735563)
MAC Address: 1c:4d:70:e4:e4:0e (Intel Corporation)
Addresses: 192.168.178.142/24
2001:9e8:a19f:1c00:7011:2d1:951:ad03/64
2001:9e8:a19f:1c00:f24f:f724:5dd1:d0ad/64
fe80::fec1:6ced:5268:b46c/64 (link)
DNS Addresses: 192.168.178.1
fd00::cece:1eff:fe3d:c737
DNS Search: search.domain
Routes: default via 192.168.178.1 metric 600 (dhcp)
192.168.178.0/24 from 192.168.178.142 metric 600 (link)
2001:9e8:a19f:1c00::/64 metric 600 (ra)
2001:9e8:a19f:1c00::/56 via fe80::cece:1eff:fe3d:c737 metric 600 (ra)
fe80::/64 metric 1024
default via fe80::cece:1eff:fe3d:c737 metric 20600 (ra)
self.assertEqual(out, STATUS_OUTPUT)

● 41: wg0 tunnel/wireguard UNKNOWN/UP (networkd: wg0)
Addresses: 10.10.0.2/24
Routes: 10.10.0.0/24 from 10.10.0.2 (link)
Activation Mode: manual

● 48: tun0 tunnel/sit UNKNOWN/UP (networkd: tun0)
Addresses: 2001:dead:beef::2/64
Routes: 2001:dead:beef::/64 metric 256
Activation Mode: manual
@patch('netplan_cli.cli.state.Interface.query_nm_ssid')
@patch('netplan_cli.cli.state.Interface.query_networkctl')
def test_pretty_print(self, networkctl_mock, nm_ssid_mock):
SSID = 'MYCON'
nm_ssid_mock.return_value = SSID
# networkctl mock output reduced to relevant lines
networkctl_mock.return_value = \
'''Activation Policy: manual
WiFi access point: {} (b4:fb:e4:75:c6:21)'''.format(SSID)

● 42: fakedev0 other DOWN (unmanaged)
Routes: 10.0.0.0/16 via 10.0.0.1 (local)
nd = SystemConfigState.process_networkd(NETWORKD)
nm = SystemConfigState.process_nm(NMCLI)
dns = (DNS_ADDRESSES, DNS_SEARCH)
routes = (SystemConfigState.process_generic(ROUTE4), SystemConfigState.process_generic(ROUTE6))
fakeroute = {'type': 'local', 'dst': '10.0.0.0/16', 'gateway': '10.0.0.1', 'dev': FAKE_DEV['ifname'], 'table': 'main'}

1 inactive interfaces hidden. Use "--all" to show all.
''')
interfaces = [
Interface(self._get_itf('enp0s31f6'), nd, nm, dns, routes),
Interface(self._get_itf('wlan0'), nd, nm, dns, routes),
Interface(self._get_itf('wg0'), nd, nm, dns, routes),
Interface(self._get_itf('tun0'), nd, nm, dns, routes),
Interface(FAKE_DEV, [], None, (None, None), ([fakeroute], None)),
]
data = {'netplan-global-state': {
'online': True,
'nameservers': {
'addresses': ['127.0.0.53'],
'search': ['search.domain'],
'mode': 'stub',
}}}
for itf in interfaces:
ifname, obj = itf.json()
data[ifname] = obj
f = io.StringIO()
with redirect_stdout(f):
status = NetplanStatus()
status.verbose = False
status.pretty_print(data, len(interfaces)+1, _console_width=130)
out = f.getvalue()
self.assertEqual(out, STATUS_OUTPUT)

@patch('netplan_cli.cli.state.Interface.query_nm_ssid')
@patch('netplan_cli.cli.state.Interface.query_networkctl')
Expand Down
Loading