Sunil Mohan Adapa b253166f6d
*: Use action_utils.run instead of subprocess.check_output
- This is to capture stdout and stderr and transmit that from privileged daemon
back to the service to be displayed in HTML.

Tests:

- Unit tests and code checks pass.

- Some of the modified actions work as expected.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Veiko Aasa <veiko17@disroot.org>
2025-09-29 16:58:56 +03:00

193 lines
6.6 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-or-later
"""Configure network manager.
During initial setup, configure networking for all wired and wireless devices
by creating network manager connections.
"""
import collections
import itertools
import logging
import re
import subprocess
from plinth import action_utils
from plinth.actions import privileged
def _sort_interfaces(interfaces: list[str]) -> list[str]:
"""Sort interfaces in a well-defined way: eth0, eth1, eth2, ... eth10."""
def key_func(interface):
parts = re.findall(r'(\D*)(\d*)', interface)
parts = [(string, int(number) if number else number)
for string, number in parts]
return list(itertools.chain(parts))
return sorted(interfaces, key=key_func)
def _get_interfaces() -> dict[str, list[str]]:
"""Return all network interfaces by their type."""
output = action_utils.run(
['nmcli', '--terse', '--fields', 'type,device', 'device'],
check=True).stdout
interfaces = collections.defaultdict(list)
for line in output.decode().splitlines():
type_, _, interface = line.partition(':')
interfaces[type_].append(interface)
for type_ in interfaces:
interfaces[type_] = _sort_interfaces(interfaces[type_])
return interfaces
def _add_connection(connection_name: str, interface: str,
remaining_arguments: list[str]):
"""Add an Ethernet/Wi-Fi connection of type regular or shared."""
output = action_utils.run(
['nmcli', '--terse', '--fields', 'name,device', 'con', 'show'],
check=True).stdout
lines = output.decode().splitlines()
if f'{connection_name}:{interface}' in lines:
logging.info('Connection %s already exists for device %s, not adding.',
connection_name, interface)
else:
action_utils.run([
'nmcli', 'con', 'add', 'con-name', connection_name, 'ifname',
interface
] + remaining_arguments, check=True)
def _activate_connection(connection_name: str):
"""Activate a network connection in a background process."""
subprocess.Popen(['nohup', 'nmcli', 'con', 'up', connection_name],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def _configure_regular_interface(interface: str, zone: str):
"""Create a connection that is not a shared connection."""
connection_name = 'FreedomBox WAN'
properties = {'connection.autoconnect': 'TRUE', 'connection.zone': zone}
# Create n-m connection for a regular interface
_add_connection(connection_name, interface, ['type', 'ethernet'])
_set_connection_properties(connection_name, properties)
_activate_connection(connection_name)
logging.info('Configured interface %s for %s use as %s.', interface, zone,
connection_name)
def _configure_shared_interface(interface: str):
"""Create a shared connection that has traffic forwarding enabled.
Shared connection means:
- Self-assign an address and network
- Start and manage DNS server (dnsmasq)
- Start and manage DHCP server (dnsmasq)
- Register address with mDNS
- Add firewall rules for NATing from this interface
"""
connection_name = f'FreedomBox LAN {interface}'
properties = {
'connection.autoconnect': 'TRUE',
'connection.zone': 'internal',
'ipv4.method': 'shared'
}
# Create n-m connection for eth1
_add_connection(connection_name, interface, ['type', 'ethernet'])
_set_connection_properties(connection_name, properties)
_activate_connection(connection_name)
logging.info('Configured interface %s for shared use as %s.', interface,
connection_name)
def _set_connection_properties(connection_name: str, properties: dict[str,
str]):
"""Configure property key/values on a connection."""
for key, value in properties.items():
action_utils.run(
['nmcli', 'con', 'modify', connection_name, key, value],
check=True)
def _configure_wireless_interface(interface: str):
"""Configure a wireless access point."""
connection_name = f'FreedomBox {interface}'
ssid = f'FreedomBox{interface}'
secret = 'freedombox123'
properties = {
'connection.autoconnect': 'TRUE',
'connection.zone': 'internal',
'ipv4.method': 'shared',
'wifi.mode': 'ap',
'wifi-sec.key-mgmt': 'wpa-psk',
'wifi-sec.psk': secret
}
_add_connection(connection_name, interface, ['type', 'wifi', 'ssid', ssid])
_set_connection_properties(connection_name, properties)
_activate_connection(connection_name)
logging.info('Configured interface %s for shared use as %s', interface,
connection_name)
def _multi_wired_setup(interfaces: list[str]):
"""Configure all Ethernet connections on a system with many of them."""
_configure_regular_interface(interfaces[0], 'external')
for interface in interfaces[1:]:
_configure_shared_interface(interface)
def _one_wired_setup(interface: str, interfaces: dict[str, list[str]]):
"""Configure an Ethernet connection on a system with only one."""
if not len(interfaces['wifi']):
_configure_regular_interface(interface, 'internal')
else:
_configure_regular_interface(interface, 'external')
def _wireless_setup(interfaces: list[str]):
"""Configure all wireless access points."""
for interface in interfaces:
_configure_wireless_interface(interface)
@privileged
def setup():
"""Create network manager connections.
For a user who installed using freedombox-setup Debian package, when
FreedomBox Service (Plinth) is run for the first time, don't run network
setup. This is ensured by checking for the file
/var/lib/freedombox/is-freedombox-disk-image which will not exist.
For a user who installed using FreedomBox disk image, when FreedomBox
Service (Plinth) runs for the first time, setup process executes and
triggers the script due networks module being an essential module.
"""
if not action_utils.is_disk_image():
logging.info(
'Not a FreedomBox disk image. Skipping network configuration.')
return
logging.info('Setting up network configuration...')
interfaces = _get_interfaces()
if len(interfaces['ethernet']) == 0:
logging.info('No wired interfaces detected.')
elif len(interfaces['ethernet']) == 1:
_one_wired_setup(interfaces['ethernet'][0], interfaces)
else:
_multi_wired_setup(interfaces['ethernet'])
_wireless_setup(interfaces['wifi'])
logging.info('Done setting up network configuration.')