networks: Use privileged decorator for actions

Tests:

- Initial setup of during first setup works
  - When there are no wired network interfaces
  - When there is 1 wired network interface
    - When there is one wifi interface. wired network is setup as 'external'
      WAN. (simulated with edit of _get_interfaces())
    - When there are no wifi interfaces. wired network is setup as 'internal'
      WAN.
  - When there are multiple wired network interfaces
    - First one is setup as WAN rest as shared
  - When there is one wifi interface, interface is setup as shared.
  - When there are no wifi interfaces

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Sunil Mohan Adapa 2022-09-03 12:23:09 -07:00 committed by James Valleroy
parent f7ce8a7c66
commit 4dda9ad6b9
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
5 changed files with 222 additions and 167 deletions

View File

@ -16,7 +16,7 @@ code-quality:
stage: test
needs: []
script:
- python3 -m flake8 --exclude actions/networks container plinth actions/*
- python3 -m flake8 container plinth actions/*
unit-tests:
stage: test

View File

@ -1,161 +0,0 @@
#!/bin/bash
set -e
# Configure networking for all wired and wireless devices.
#
# Creates network-manager connections.
function get-interfaces {
WIRED_IFACES=$(nmcli --terse --fields type,device device | grep "^ethernet:" | cut -d: -f2 | sort -V)
NO_OF_WIRED_IFACES=$(echo $WIRED_IFACES | wc -w)
WIRELESS_IFACES=$(nmcli --terse --fields type,device device | grep "^wifi:" | cut -d: -f2 | sort -V)
NO_OF_WIRELESS_IFACES=$(echo $WIRELESS_IFACES | wc -w)
}
function add-connection {
local connection_name="$1"
shift
local interface="$1"
shift
local remaining_arguments="$@"
already_exists=$(nmcli --terse --fields name,device con show | grep "$connection_name:$interface" || true)
if [ -n "$already_exists" ]; then
echo "Connection '$connection_name' already exists for device '$interface', not adding."
else
nmcli con add con-name "$connection_name" ifname "$interface" $remaining_arguments
fi
}
function activate-connection {
connection_name="$1"
nohup nmcli con up "$connection_name" &>/dev/null &
}
function configure-regular-interface {
local interface="$1"
local zone="$2"
local connection_name="FreedomBox WAN"
# Create n-m connection for a regular interface
add-connection "$connection_name" "$interface" type ethernet
nmcli con modify "$connection_name" connection.autoconnect TRUE
nmcli con modify "$connection_name" connection.zone "$zone"
activate-connection "$connection_name"
echo "Configured interface '$interface' for '$zone' use as '$connection_name'."
}
function configure-shared-interface {
local interface="$1"
local connection_name="FreedomBox LAN $interface"
# Create n-m connection for eth1
add-connection "$connection_name" "$interface" type ethernet
nmcli con modify "$connection_name" connection.autoconnect TRUE
nmcli con modify "$connection_name" connection.zone internal
# Configure this interface to be shared with other computers.
# - 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
nmcli con modify "$connection_name" ipv4.method shared
activate-connection "$connection_name"
echo "Configured interface '$interface' for shared use as '$connection_name'."
}
function configure-wireless-interface {
local interface="$1"
local connection_name="FreedomBox $interface"
local ssid="FreedomBox$interface"
local secret="freedombox123"
add-connection "$connection_name" "$interface" type wifi ssid "$ssid"
nmcli con modify "$connection_name" connection.autoconnect TRUE
nmcli con modify "$connection_name" connection.zone internal
nmcli con modify "$connection_name" ipv4.method shared
nmcli con modify "$connection_name" wifi.mode ap
nmcli con modify "$connection_name" wifi-sec.key-mgmt wpa-psk
nmcli con modify "$connection_name" wifi-sec.psk "$secret"
activate-connection "$connection_name"
echo "Configured interface '$interface' for shared use as '$connection_name'."
}
function multi-wired-setup {
local first_interface="$1"
shift
local remaining_interfaces="$@"
configure-regular-interface "$first_interface" external
for interface in $remaining_interfaces
do
configure-shared-interface "$interface"
done
}
function one-wired-setup {
local interface="$1"
case $NO_OF_WIRELESS_IFACES in
"0")
configure-regular-interface "$interface" internal
;;
*)
configure-regular-interface "$interface" external
;;
esac
}
function wireless-setup {
local interfaces="$@"
for interface in $interfaces
do
configure-wireless-interface "$interface"
done
}
function setup {
echo "Setting up network configuration..."
get-interfaces
case $NO_OF_WIRED_IFACES in
"0")
echo "No wired interfaces detected."
;;
"1")
one-wired-setup $WIRED_IFACES
;;
*)
multi-wired-setup $WIRED_IFACES
esac
wireless-setup $WIRELESS_IFACES
echo "Done setting up network configuration."
}
#
# 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 [ -f "/var/lib/freedombox/is-freedombox-disk-image" ]
then
setup
else
echo "Not a FreedomBox disk image. Skipping network configuration."
fi

View File

@ -1,18 +1,17 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
FreedomBox app to interface with network-manager.
"""
"""FreedomBox app to interface with network-manager."""
import subprocess
from logging import Logger
from django.utils.translation import gettext_lazy as _
from plinth import actions
from plinth import app as app_module
from plinth import daemon, kvstore, menu, network
from plinth.package import Packages
from . import privileged
first_boot_steps = [
{
'id': 'network_topology_wizard',
@ -83,7 +82,7 @@ class NetworksApp(app_module.App):
def setup(self, old_version):
"""Install and configure the app."""
super().setup(old_version)
actions.superuser_run('networks')
privileged.setup()
self.enable()

View File

@ -0,0 +1,189 @@
# 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 = subprocess.check_output(
['nmcli', '--terse', '--fields', 'type,device', 'device'])
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 = subprocess.check_output(
['nmcli', '--terse', '--fields', 'name,device', 'con', 'show'])
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:
subprocess.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():
subprocess.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.')

View File

@ -0,0 +1,28 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Test privileged method for networks app."""
from unittest.mock import patch
from .. import privileged
@patch('subprocess.check_output')
def test_get_interfaces(check_output):
"""Test returning list of network interfaces in sorted order."""
check_output.return_value = '\n'.join([
'ethernet:ve-fbx-testing',
'ethernet:enp39s0',
'ethernet:enp32s1',
'ethernet:enp4s1',
'bridge:virbr0',
'wifi:wlp41s0',
'loopback:lo',
]).encode()
interfaces = privileged._get_interfaces()
assert interfaces == {
'ethernet': ['enp4s1', 'enp32s1', 'enp39s0', 've-fbx-testing'],
'bridge': ['virbr0'],
'wifi': ['wlp41s0'],
'loopback': ['lo']
}