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

New 'netplan status' CLI #290

Merged
merged 24 commits into from
Jan 5, 2023
Merged

New 'netplan status' CLI #290

merged 24 commits into from
Jan 5, 2023

Conversation

slyon
Copy link
Collaborator

@slyon slyon commented Aug 31, 2022

Description

Implement new, initial netplan status command, according to spec FO049, which will show a system's current networking state, querying data from multiple backends (e.g. sd-networkd, NetworkManager, iproute2, DBus/sd-resolved, ...) and connecting the interface data with its corresponding Netplan/Netdef ID.

A new depdendency python3-rich is used for pretty printing the output to a terminal, in addition to structural JSON or YAML output. Usage:

usage: netplan status [-h] [--debug] [-a] [-f FORMAT] [ifname]

Query networking state of the running system

positional arguments:
  ifname                Show only this interface

options:
  -h, --help            show this help message and exit
  --debug               Enable debug messages
  -a, --all             Show all interface data (incl. inactive)
  -f FORMAT, --format FORMAT
                        Output in machine readable `json` or `yaml` format

Example output (non-pretty, as we're missing colors/bold text in this GitHub PR view):

     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)
                   192.168.178.1 from 192.168.178.62 metric 100 (dhcp, 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
                   default via fe80::cece:1eff:fe3d:c737 metric 100 (ra)
  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.

The code in this branch can be executed like this:

sudo apt build-dep netplan.io
sudo apt install python3-rich
meson setup build --prefix=/usr
meson compile -C build
LD_LIBRARY_PATH=build/src/ PYTHONPATH=. src/netplan.script status

Checklist

  • Runs make check successfully.
  • Retains 100% code coverage (make check-coverage).
  • New/changed keys in YAML format are documented.
  • (Optional) Adds example YAML for new feature.
  • (Optional) Closes an open bug in Launchpad.

@slyon slyon force-pushed the slyon/status branch 3 times, most recently from 917a3f1 to a44c6b6 Compare October 26, 2022 15:00
@slyon slyon marked this pull request as ready for review October 26, 2022 15:24
Copy link
Collaborator

@daniloegea daniloegea left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the most critical issue I've found is in the Interface class' constructor. It crashes in systems where network-manager is not installed, like LXD images (at least kinetic).

import subprocess
import sys
import yaml
import netplan.cli.utils as utils
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: We probably should follow PEP8 and group imports in this order:

  • standard library import
  • third party imports
  • application imports

self.adminstate: str = 'UP' if 'UP' in ip['flags'] else 'DOWN'
self.operstate: str = ip['operstate'].upper()
self.macaddress: str = None
if 'address' in ip and len(ip['address']) == 17: # 6 byte MAC
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Magic numbers look better as 'constants' with meaningful names, or we could even have a little private method returning the MAC address or None.

netplan/cli/commands/status.py Show resolved Hide resolved
@property
def netdef_id(self) -> str:
if self.backend == 'networkd':
return self.nd.get('NetworkFile', '').split(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: If the goal here is to get the interface name, wouldn't it be better to just use the Name attribute?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. This is not about getting the interface name, but rather the Netplan netdef_id (which COULD be equal to the interface name, but can also be different). E.g. on my system I have this, where "usbC" is the NetdefID inside my netplan YAML config, while "lan0" is the interface name.

		{
			"Index" : 14,
			"Name" : "lan0",
			"AlternativeNames" : [],
			"Type" : "ether",
			"Driver" : "ax88179_178a",
			"SetupState" : "configured",
			"OperationalState" : "routable",
			"CarrierState" : "carrier",
			"AddressState" : "routable",
			"IPv4AddressState" : "routable",
			"IPv6AddressState" : "routable",
			"OnlineState" : "online",
			"NetworkFile" : "/run/systemd/network/10-netplan-usbC.network",
			"LinkFile" : "/run/systemd/network/10-netplan-usbC.link",
			"Path" : "pci-0000:00:0d.0-usb-0:3.2.1:1.0",
			"Vendor" : "ASIX Electronics Corp.",
			"Model" : "AX88179 Gigabit Ethernet"
		}

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 len(non_local_ips) > 0 and len(default_routes) > 0 and len(itf.dns_addresses) > 0:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: empty lists will evaluate to False so this could be simplified to if list1 and list2 and list3.

dns_addr = ns.get('addresses', [])
dns_mode = ns.get('mode')
dns_search = ns.get('search', [])
if len(dns_addr) > 0:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: empty lists evaluate to False, could be simplified to if dns_addr:. There are few more instances like this across the code.

# due to hard package dependencies
iproute2 = self.query_iproute2()
networkd = self.query_networkd()
if not iproute2 or not networkd:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Is it expected to fail on systems that don't use networkd? It's stopping here on my system (standard kinetic upgraded from jammy).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

systemd[-networkd] should be installed on any relevant system, as it is a dependency of netplan.io. IIRC I designed this new CLI in a way that it would assume networkd to be available... But maybe I should double-check if networkd is actually up and running on a system which does not necessarily depend on it (like a standard Ubuntu Desktop system).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dang. I can confirm that systemd-networkd is installed but not running on a default Ubuntu Desktop installation. Let's see what I can do about that (maybe starting the service before trying to query it).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking the activity status of systemd-networkd.service and starting it if needed solves this issue. We assume systemd-networkd to be installed by default (it's part of the systemd package in Ubuntu).

One drawback: netplan status would be able to run without sudo privileges, if not for this reason of starting a system service... But maybe that's something to solve at a later time, as Netplan is a system/admin tool and requires root privileges for most of it's actions anyways.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. So, assuming sd-networkd is a hard dependency for netplan status, and I don't have a strong opinion on that, we should check first if we can start it. If it's masked for example netplan status will crash.

It would be nice to show a nice message like "netplan status depends on networkd" if we can't start it. A good thing is that once it's running one can call netplan status as a non-privileged user.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe we could just tell the user that networkd is not running and ask them to start it instead of trying to do that inside netplan...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to have it working "out of the box" on a default Ubuntu Desktop installation, that's why I'd like to automatically have it started if needed.

But checking if systemd-networkd.service is masked is a fair point. We'll now be bailing out if networkd is masked with an error message notifying the user to start networkd. If it's just disabled OTOH we'll try to start it automatically.

Netplan's 'dbus' tests module was in conflict with the system's global
'dbus' module (i.e. D-Bus bindings), leading to failing tests. We rename
the tests to 'netplan_dbus' to avoid this.

Cleanup pytest-3 deprecation warnings while on it
We make use of the previously assembled structural JSON data, to make sure we
always present the same data in JSON/YAML and human readable format and don't
miss anything in the structual output.
@slyon
Copy link
Collaborator Author

slyon commented Jan 4, 2023

Thank you very much for the review @daniloegea I think I addressed all of your remarks.

PTAL.

@slyon slyon requested review from daniloegea and removed request for schopin-pro January 4, 2023 14:14
Copy link
Collaborator

@daniloegea daniloegea left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The output looks really nice. Now it's working on the scenarios I've tested before.

Found only 2 things we probably should address: we should at least check if networkd is not masked before trying to start it to avoid a crash and disable the conversion of sequences like :ab: to emojis.

def command(self):
networkd: str = subprocess.check_output(['networkctl', '--json=short'],
universal_newlines=True)
networkd_data = yaml.safe_load(networkd)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: hmm using the yaml module to load a JSON string, not a problem just counterintuitive. Anyway, the result of json.loads(networkd) is exactly the same.

logging.debug('Cannot query resolved DNS data: {}'.format(str(e)))
return (addresses, search)

def pretty_print(self, data: JSON, total: int, _console_width=None) -> None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've found by accident that sequences like :ab: and :1234: are being converted to emojis:

● 13: br123 bridge DOWN/UP (NetworkManager: br123)
      MAC Address: ee:21:ad:24:f9:71
        Addresses: a🆎b🆎a🔢a:abcd/128
           Routes: a🆎b🆎a🔢a:abcd metric 426

self.routes.append(elem)

self.addresses: list = None
if 'addr_info' in ip and ip['addr_info']:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (no need to change): since python 3.8 this pattern can be simplified a bit with the := operator

if addr_info := ip.get('addr_info'):
  for addr in addr_info:

# due to hard package dependencies
iproute2 = self.query_iproute2()
networkd = self.query_networkd()
if not iproute2 or not networkd:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe we could just tell the user that networkd is not running and ask them to start it instead of trying to do that inside netplan...

@slyon
Copy link
Collaborator Author

slyon commented Jan 5, 2023

Thanks for another sanity check and spotting those issues!

@slyon slyon requested a review from daniloegea January 5, 2023 11:25
Copy link
Collaborator

@daniloegea daniloegea left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@slyon slyon merged commit bb86353 into main Jan 5, 2023
@slyon slyon deleted the slyon/status branch January 5, 2023 15:47
kraj pushed a commit to YoeDistro/meta-openembedded that referenced this pull request Mar 8, 2023
Add python3-dbus and python3-rich[1] to RDEPENDS.

[1] canonical/netplan#290

Signed-off-by: Yi Zhao <[email protected]>
Signed-off-by: Khem Raj <[email protected]>
halstead pushed a commit to openembedded/meta-openembedded that referenced this pull request Mar 9, 2023
Add python3-dbus and python3-rich[1] to RDEPENDS.

[1] canonical/netplan#290

Signed-off-by: Yi Zhao <[email protected]>
Signed-off-by: Khem Raj <[email protected]>
daregit pushed a commit to daregit/yocto-combined that referenced this pull request May 22, 2024
Add python3-dbus and python3-rich[1] to RDEPENDS.

[1] canonical/netplan#290

Signed-off-by: Yi Zhao <[email protected]>
Signed-off-by: Khem Raj <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants