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

Netplan status --diff refactoring #444

Merged
merged 7 commits into from
Feb 29, 2024
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
17 changes: 17 additions & 0 deletions doc/netplan-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,23 @@ Currently, **`netplan status`** depends on `systemd-networkd` as a source of dat
`-a`, `--all`
: Show all interface data including inactive.

`--diff`
: Analyze and display differences between the current system configuration and network definitions present in the YAML files.
The configuration analyzed includes IP addresses, routes, MAC addresses, DNS addresses, search domains and missing network interfaces.

The output format is similar to popular diff tools, such `diff` and `git diff`. Configuration present only in the system (and therefore missing in the Netplan YAMLs)
will be displayed with a `+` sign and will be highlighted in green. Configuration present only in Netplan (and therefore missing in the system) will be displayed
with a `-` sign and highlighted in red. The same is applied to network interfaces.

`--diff-only`
: Same as `--diff` but omits all the information that is not a difference.

`--root-dir`
: Read YAML files from this root instead of `/`.

`--verbose`
: Show extra information.

`-f` *`FORMAT`*, `--format` *`FORMAT`*
: Output in machine-readable `json` or `yaml` format.

Expand Down
16 changes: 16 additions & 0 deletions include/netdef.h
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,22 @@ netplan_netdef_get_dhcp4(const NetplanNetDefinition* netdef);
NETPLAN_PUBLIC gboolean
netplan_netdef_get_dhcp6(const NetplanNetDefinition* netdef);

/**
* @brief Query a @ref NetplanNetDefinition for the value of its `link-local` setting for IPv4.
* @param[in] netdef The @ref NetplanNetDefinition to query
* @return Indication if @p netdef is configured to enable the link-local address for IPv4
*/
NETPLAN_PUBLIC gboolean
netplan_netdef_get_link_local_ipv4(const NetplanNetDefinition* netdef);

/**
* @brief Query a @ref NetplanNetDefinition for the value of its `link-local` setting for IPv6.
* @param[in] netdef The @ref NetplanNetDefinition to query
* @return Indication if @p netdef is configured to enable the link-local address for IPv6
*/
NETPLAN_PUBLIC gboolean
netplan_netdef_get_link_local_ipv6(const NetplanNetDefinition* netdef);

/**
* @brief Get the `macaddress` setting of a given @ref NetplanNetDefinition.
* @details Copies a `NUL`-terminated string into a sized @p out_buffer. If the
Expand Down
2 changes: 1 addition & 1 deletion netplan.completions
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ _netplan_completions() {
;;

'status'*)
while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "$(_netplan_completions_filter "-h --help --debug -a --all -f --format $(ls /sys/class/net 2> /dev/null)")" -- "$cur" )
while read -r; do COMPREPLY+=( "$REPLY" ); done < <( compgen -W "$(_netplan_completions_filter "-h --help --debug -a --all --diff --diff-only --root-dir --verbose -f --format $(ls /sys/class/net 2> /dev/null)")" -- "$cur" )
;;

'apply'*)
Expand Down
68 changes: 47 additions & 21 deletions netplan_cli/cli/state_diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from netplan.netdef import NetplanRoute
from netplan_cli.cli.state import SystemConfigState, NetplanConfigState, DEVICE_TYPES
from netplan_cli.cli.utils import is_valid_macaddress, route_table_lookup


class DiffJSONEncoder(json.JSONEncoder):
Expand All @@ -43,6 +44,8 @@ def __init__(self, system_state: SystemConfigState, netplan_state: NetplanConfig
self.system_state = system_state
self.netplan_state = netplan_state

self.route_lookup_table_names = {}

def get_full_state(self) -> dict:
'''
Return the states of both the system and Netplan in a common representation
Expand Down Expand Up @@ -135,6 +138,7 @@ def _analyze_ip_addresses(self, config: dict, iface: dict) -> None:

missing_dhcp4_address = config.get('netplan_state', {}).get('dhcp4', False)
missing_dhcp6_address = config.get('netplan_state', {}).get('dhcp6', False)
link_local = config.get('netplan_state', {}).get('link_local', [])
system_ips = set()
for addr, addr_data in config.get('system_state', {}).get('addresses', {}).items():
ip = ipaddress.ip_interface(addr)
Expand All @@ -144,6 +148,15 @@ def _analyze_ip_addresses(self, config: dict, iface: dict) -> None:
if 'dhcp' not in flags and 'link' not in flags:
system_ips.add(addr)

# Handle the link local address
# If it's present but the respective setting is not enabled in the netdef
# it's considered a difference.
if 'link' in flags and ip.is_link_local:
if isinstance(ip.ip, ipaddress.IPv4Address) and 'ipv4' not in link_local:
system_ips.add(addr)
if isinstance(ip.ip, ipaddress.IPv6Address) and 'ipv6' not in link_local:
system_ips.add(addr)

# TODO: improve the detection of addresses assigned dynamically
# in the class Interface.
if 'dhcp' in flags:
Expand Down Expand Up @@ -291,6 +304,11 @@ def _analyze_mac_addresses(self, config: dict, iface: dict) -> None:
system_macaddress = config.get('system_state', {}).get('macaddress')
netplan_macaddress = config.get('netplan_state', {}).get('macaddress')

# if the macaddress in netplan is an special option (such as 'random')
# don't try to diff it against the system MAC address
if netplan_macaddress and not is_valid_macaddress(netplan_macaddress):
return

if system_macaddress and netplan_macaddress:
if system_macaddress != netplan_macaddress:
iface[name]['system_state'].update({
Expand All @@ -308,7 +326,7 @@ def _analyze_routes(self, config: dict, iface: dict) -> None:

# Filter out some routes that are expected to be added automatically
system_addresses = [ip for ip in config.get('system_state', {}).get('addresses', {})]
system_routes = self._filter_system_routes(system_routes, system_addresses)
system_routes = self._filter_system_routes(system_routes, system_addresses, config)

present_only_in_netplan = netplan_routes.difference(system_routes)
present_only_in_system = system_routes.difference(netplan_routes)
Expand Down Expand Up @@ -424,7 +442,7 @@ def _normalize_routes(self, routes: set) -> set:

return new_routes_set

def _filter_system_routes(self, system_routes: AbstractSet[NetplanRoute], system_addresses: list[str]) -> set:
def _filter_system_routes(self, system_routes: AbstractSet[NetplanRoute], system_addresses: list[str], config: dict) -> set:
'''
Some routes found in the system are installed automatically/dynamically without
being configured in Netplan.
Expand All @@ -434,25 +452,39 @@ def _filter_system_routes(self, system_routes: AbstractSet[NetplanRoute], system
'''

local_networks = [str(ipaddress.ip_interface(ip).network) for ip in system_addresses]
# filter out the local link network as we give special treatment to it
local_networks = list(filter(lambda n: n != 'fe80::/64', local_networks))
addresses = [str(ipaddress.ip_interface(ip).ip) for ip in system_addresses]
link_local = config.get('netplan_state', {}).get('link_local', [])
routes = set()
for route in system_routes:
# Filter out link routes
if route.scope == 'link':
# Filter out link routes (but not link local as we handle them differently)
if route.scope == 'link' and route.to != 'default' and not ipaddress.ip_interface(route.to).is_link_local:
continue

# Filter out routes installed by DHCP or RA
if route.protocol == 'dhcp' or route.protocol == 'ra':
continue

# Filter out Link Local routes
if route.to != 'default' and ipaddress.ip_interface(route.to).is_link_local:
continue
# We only filter them out if the respective 'link-local' setting is present in the netdef
if route.to != 'default':
route_to = ipaddress.ip_interface(route.to)
if route_to.is_link_local:
if route.family == 10 and 'ipv6' in link_local:
continue
if route.family == 2 and 'ipv4' in link_local:
continue

# Filter out host scoped routes
if (route.scope == 'host' and route.type == 'local' and
(route.to in addresses or ipaddress.ip_interface(route.to).is_loopback)):
continue

# Filter out the default IPv6 multicast route
if route.family == 10 and route.type == 'multicast' and route.to == 'ff00::/8':
continue

# Filter IPv6 local routes
if route.family == 10 and (route.to in local_networks or route.to in addresses):
continue
Expand All @@ -474,6 +506,8 @@ def _get_netplan_interfaces(self) -> dict:
iface_ref['dhcp4'] = config.dhcp4
iface_ref['dhcp6'] = config.dhcp6

iface_ref['link_local'] = config.link_local

addresses = [addr for addr in config.addresses]
if addresses:
iface_ref['addresses'] = {}
Expand Down Expand Up @@ -624,18 +658,10 @@ def _system_route_to_netplan(self, system_route: dict) -> NetplanRoute:
return NetplanRoute(**route)

def _default_route_tables_name_to_number(self, name: str) -> int:
value = 0
# Mapped in /etc/iproute2/rt_tables
if name == 'default':
value = 253
elif name == 'main':
value = 254
elif name == 'local':
value = 255
else:
try:
value = int(name)
except ValueError:
value = 0

return value
if name.isdigit():
return int(name)

if not self.route_lookup_table_names:
self.route_lookup_table_names = route_table_lookup()

return self.route_lookup_table_names.get(name, 0)
23 changes: 20 additions & 3 deletions netplan_cli/cli/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
NM_SERVICE_NAME = 'NetworkManager.service'
NM_SNAP_SERVICE_NAME = 'snap.network-manager.networkmanager.service'

OLD_RT_TABLES_PATH = '/etc/iproute2/rt_tables'
NEW_RT_TABLES_PATH = '/usr/share/iproute2/rt_tables'
RT_TABLES_DEFAULT = {0: 'unspec', 253: 'default', 254: 'main', 255: 'local',
'unspec': 0, 'default': 253, 'main': 254, 'local': 255}

config_errors = (ConfigurationError, NetplanException, RuntimeError)


Expand Down Expand Up @@ -213,17 +218,29 @@ def find_matching_iface(interfaces: list, netdef):
return matches[0]


def is_valid_macaddress(macaddress: str) -> bool:
MAC_PATTERN = '^[a-fA-F0-9][a-fA-F0-9](:[a-fA-F0-9][a-fA-F0-9]){5}((:[a-fA-F0-9][a-fA-F0-9]){14})?$'
return re.match(MAC_PATTERN, macaddress) is not None


def route_table_lookup() -> dict:
lookup_table = {}
path = NEW_RT_TABLES_PATH

if not os.path.exists(path):
path = OLD_RT_TABLES_PATH

try:
with open('/etc/iproute2/rt_tables', 'r') as rt_tables:
with open(path, 'r') as rt_tables:
for line in rt_tables:
split_line = line.split()
if len(split_line) == 2 and split_line[0].isnumeric():
lookup_table[int(split_line[0])] = split_line[1]
lookup_table[split_line[1]] = int(split_line[0])
except Exception:
logging.debug('Cannot open \'/etc/iproute2/rt_tables\' for reading')
return {}
logging.debug(f'Cannot open \'{path}\' for reading')
# defaults to the standard content found in the file
return RT_TABLES_DEFAULT

return lookup_table

Expand Down
2 changes: 2 additions & 0 deletions python-cffi/netplan/_build_cffi.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@
const NetplanNetDefinition* netdef, const char* name, const char* mac, const char* driver_name);
gboolean netplan_netdef_get_dhcp4(const NetplanNetDefinition* netdef);
gboolean netplan_netdef_get_dhcp6(const NetplanNetDefinition* netdef);
gboolean netplan_netdef_get_link_local_ipv4(const NetplanNetDefinition* netdef);
gboolean netplan_netdef_get_link_local_ipv6(const NetplanNetDefinition* netdef);
ssize_t netplan_netdef_get_macaddress(const NetplanNetDefinition* netdef, char* out_buffer, size_t out_buffer_size);

// NetDefinition (internal)
Expand Down
9 changes: 9 additions & 0 deletions python-cffi/netplan/netdef.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ def dhcp4(self) -> bool:
def dhcp6(self) -> bool:
return bool(lib.netplan_netdef_get_dhcp6(self._ptr))

@property
def link_local(self) -> list:
linklocal = []
if bool(lib.netplan_netdef_get_link_local_ipv4(self._ptr)):
linklocal.append('ipv4')
if bool(lib.netplan_netdef_get_link_local_ipv6(self._ptr)):
linklocal.append('ipv6')
return linklocal
slyon marked this conversation as resolved.
Show resolved Hide resolved

@property
def nameserver_addresses(self) -> '_NetdefNameserverIterator':
return _NetdefNameserverIterator(self._ptr)
Expand Down
12 changes: 12 additions & 0 deletions src/util.c
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,18 @@ netplan_netdef_get_dhcp6(const NetplanNetDefinition* netdef)
return netdef->dhcp6;
}

gboolean
netplan_netdef_get_link_local_ipv4(const NetplanNetDefinition* netdef)
{
return netdef->linklocal.ipv4;
}

gboolean
netplan_netdef_get_link_local_ipv6(const NetplanNetDefinition* netdef)
{
return netdef->linklocal.ipv6;
}

gboolean
is_multicast_address(const char* address)
{
Expand Down
Loading
Loading