Commit a14ebf3f authored by Nigel Kukard's avatar Nigel Kukard
Browse files

Added network configuration support

parent 1d053a84
......@@ -15,6 +15,7 @@
"""Guts of the IDMS Linux Installer."""
import ipaddress
import json
import locale
import re
......@@ -105,6 +106,15 @@ class Ili:
if not self.state.networknaming_strategy:
self._get_networknaming_strategy()
# Process network interface mapping before we get network config so the devices have the right names
self._plugins.process_network_interface_mapping(self.state)
# If we don't have any attributes defined somewhere, we need to get them from the user
if not self.state.network_interface_attributes:
# Loop with all devices
for device_name in self.state.network_interface_mappings:
self._get_network_config(device_name)
# Set hostname
if not self.state.hostname:
self._get_hostname()
......@@ -181,6 +191,9 @@ class Ili:
# Read config as string
self._preseed.read_string(config_data)
# Grab the preseed sections
sections = self._preseed.sections()
# Process storage section
if self._preseed.has_section('storage'):
# Process install disks
......@@ -267,6 +280,16 @@ class Ili:
self._status_callback('preseed network -> interface_mapping')
self.state.network_interface_mappings = self._preseed_read_json('network', 'interface_mapping')
# Loop with network device settings
network_interface_sections = [x for x in sections if x.startswith('network ')]
for section in network_interface_sections:
# Grab device name
device_name = section.replace('network ', '')
# Loop with section items
for item, value in self._preseed.items(section):
self.state.add_network_interface_attribute(device_name, item, value)
# pylama:select=C901,R0912,R0915
def _get_install_disks(self):
"""Get the user to choose the disks to install on."""
......@@ -299,7 +322,7 @@ class Ili:
# Get available diskusage strategies
self._plugins.get_diskusage_strategies(self.state)
chosen_strategy = ""
chosen_strategy = ''
# Loop while no disk usage strategy was selected
while not chosen_strategy:
......@@ -330,7 +353,7 @@ class Ili:
# Get available disklayout strategies
self._plugins.get_disklayout_strategies(self.state)
chosen_strategy = ""
chosen_strategy = ''
# Loop while no disk layout strategy was selected
while not chosen_strategy:
......@@ -361,7 +384,7 @@ class Ili:
# Get available networknaming strategies
self._plugins.get_networknaming_strategies(self.state)
chosen_strategy = ""
chosen_strategy = ''
# Loop while no network naming strategy was selected
while not chosen_strategy:
......@@ -386,16 +409,204 @@ class Ili:
# Set the networknaming strategy
self.state.networknaming_strategy = chosen_strategy
def _get_network_config(self, interface: str):
"""Get network device config from user."""
chosen_addressing = ''
# Get user to choose the network addressing type
while not chosen_addressing:
# Display a radiolist with the network addressing types
code, chosen_addressing = self._dialog.radiolist(
f'Select addressing type for {interface}',
height=20, width=60, list_height=10,
title='Network Addressing Type',
choices=[
('NONE', 'No configuration', False),
('STATIC', 'Static addressing', False),
('DHCP', 'Automatic DHCP', False),
],
)
# If the user selected cancel, we need to abort
if code == self._dialog.DIALOG_CANCEL:
exit(0)
# If no addressing type was selected, we need to display an error, and do it again
if not chosen_addressing:
self._error('ERROR', 'No network addressing type was selected!')
# Return now if we have no addressing type
if chosen_addressing == 'NONE':
return
# If we were set to DHCP, add the attribute and return
if chosen_addressing == 'DHCP':
self.state.add_network_interface_attribute(interface, 'dhcp', 'yes')
return
# Get user to choose IPv4 address
while 1:
# Display an inputbox for the IPv4 address
code, ipv4address_raw = self._dialog.inputbox(
f'Enter IPv4 address for {interface} (in CIDR format)...',
height=8, width=60,
title='IPv4 Address',
)
# If the user selected cancel, we need to abort
if code == self._dialog.DIALOG_CANCEL:
exit(0)
# If no IPv4 address was selected, complain
if not ipv4address_raw:
self._error('ERROR', 'No IPv4 address was selected!')
continue
try:
ipaddress.IPv4Network(ipv4address_raw, strict=False)
self.state.add_network_interface_attribute(interface, 'ipv4address', ipv4address_raw)
break
except ipaddress.AddressValueError:
self._error('ERROR', f'The IPv4 address "{ipv4address_raw}" is not valid!')
# Get user to choose IPv4 gateway
while 1:
# Display an inputbox for the IPv4 gateway
code, ipv4gateway_raw = self._dialog.inputbox(
f'Enter IPv4 gateway for {interface} (blank for none)...',
height=8, width=60,
title='IPv4 Gateway',
)
# If the user selected cancel, we need to abort
if code == self._dialog.DIALOG_CANCEL:
exit(0)
# If no IPv4 gateway was entered break, else process it
if not ipv4gateway_raw:
break
else:
try:
ipaddress.IPv4Address(ipv4gateway_raw)
self.state.add_network_interface_attribute(interface, 'ipv4gateway', ipv4gateway_raw)
break
except ipaddress.AddressValueError:
self._error('ERROR', f'The IPv4 gateway "{ipv4gateway_raw}" is not valid!')
# Get user to choose IPv6 address
has_ipv6 = False
while 1:
# Display an inputbox for the IPv6 address
code, ipv6address_raw = self._dialog.inputbox(
f'Enter IPv6 address for {interface} (blank for none)...',
height=8, width=60,
title='IPv6 Address',
)
# If the user selected cancel, we need to abort
if code == self._dialog.DIALOG_CANCEL:
exit(0)
# If no IPv6 address was entered break, else process it
if not ipv6address_raw:
break
else:
try:
ipaddress.IPv6Network(ipv6address_raw, strict=False)
self.state.add_network_interface_attribute(interface, 'ipv6address', ipv6address_raw)
has_ipv6 = True
break
except ipaddress.AddressValueError:
self._error('ERROR', f'The IPv6 address "{ipv6address_raw}" is not valid!')
# If we have IPv6
if has_ipv6:
# Get user to choose IPv6 gateway
while 1:
# Display an inputbox for the IPv6 gateway
code, ipv6gateway_raw = self._dialog.inputbox(
f'Enter IPv6 gateway for {interface} (blank for none)...',
height=8, width=60,
title='IPv6 Gateway',
)
# If the user selected cancel, we need to abort
if code == self._dialog.DIALOG_CANCEL:
exit(0)
# If no IPv6 gateway was entered break, else process it
if not ipv6gateway_raw:
break
else:
try:
ipaddress.IPv6Address(ipv6gateway_raw)
self.state.add_network_interface_attribute(interface, 'ipv6gateway', ipv6gateway_raw)
break
except ipaddress.AddressValueError:
self._error('ERROR', f'The IPv6 gateway "{ipv6gateway_raw}" is not valid!')
# Get user to choose DNS1 address
has_dns1 = False
while 1:
# Display an inputbox for the DNS1 address
code, dns1address_raw = self._dialog.inputbox(
f'Enter DNS1 address for {interface} (blank for none)...',
height=8, width=60,
title='DNS1 Address',
)
# If the user selected cancel, we need to abort
if code == self._dialog.DIALOG_CANCEL:
exit(0)
# If no DNS1 address was entered break, else process it
if not dns1address_raw:
break
else:
try:
ipaddress.ip_address(dns1address_raw)
self.state.add_network_interface_attribute(interface, 'dns1address', dns1address_raw)
has_dns1 = True
break
except ValueError:
self._error('ERROR', f'The DNS1 address "{dns1address_raw}" is not valid!')
# Get user to choose DNS2 address
if has_dns1:
while 1:
# Display an inputbox for the DNS2 address
code, dns2address_raw = self._dialog.inputbox(
f'Enter DNS2 address for {interface} (blank for none)...',
height=8, width=60,
title='DNS2 Address',
)
# If the user selected cancel, we need to abort
if code == self._dialog.DIALOG_CANCEL:
exit(0)
# If no DNS2 address was entered break, else process it
if not dns2address_raw:
break
else:
try:
ipaddress.ip_address(dns2address_raw)
self.state.add_network_interface_attribute(interface, 'dns2address', dns2address_raw)
break
except ValueError:
self._error('ERROR', f'The DNS2 address "{dns2address_raw}" is not valid!')
def _get_hostname(self):
"""Get hostname from user."""
hostname = ""
hostname = ''
# Loop while no disk layout strategy was selected
while not hostname:
# Display an inputbox for the hostname
code, hostname = self._dialog.inputbox(
'Enter system hostname',
'Enter system hostname...',
height=8, width=40,
title='Hostname',
)
......@@ -410,11 +621,11 @@ class Ili:
def _get_root_password(self):
"""Get root password from user."""
root_password = ""
root_password = ''
# Display an inputbox for the root_password
code, root_password = self._dialog.passwordbox(
'Enter root password (optional)',
'Enter root password (optional)...',
height=8, width=40, insecure=True,
title='Root Password',
)
......@@ -430,12 +641,12 @@ class Ili:
def _get_user_username(self):
"""Get user username from user."""
user_username = ""
user_username = ''
# Display an inputbox for the user_username
code, user_username = self._dialog.inputbox(
'Enter extra user username (optional)',
height=8, width=40,
'Enter extra user username (optional)...',
height=8, width=50,
title='Extra User Username',
)
......@@ -450,12 +661,12 @@ class Ili:
def _get_user_password(self):
"""Get user password from user."""
user_password = ""
user_password = ''
# Display an inputbox for the user_password
code, user_password = self._dialog.passwordbox(
'Enter user password (optional)',
height=8, width=40, insecure=True,
'Enter user password (optional)...',
height=8, width=50, insecure=True,
title='User Password',
)
......
......@@ -62,6 +62,7 @@ class IliState:
_enable_services: List[str]
# Network
_network_interface_mappings: Dict[str, str]
_network_interface_attributes: Dict[str, Dict[str, str]]
#
# INTERNALS
......@@ -128,6 +129,7 @@ class IliState:
self._user_sshkeys = []
self._enable_services = []
self._network_interface_mappings = {}
self._network_interface_attributes = {}
# These properties are set during the install process
self._supported_diskusage_strategies = []
......@@ -386,6 +388,15 @@ class IliState:
self._network_interface_mappings[interface] = mac
def add_network_interface_attribute(self, interface: str, item: str, value: str):
"""Add a network interface attribute."""
# If this interface doesn't yet exist, add it
if interface not in self._network_interface_attributes:
self._network_interface_attributes[interface] = {}
self._network_interface_attributes[interface][item] = value
@property
def network_interface_mappings(self):
"""Return network_interface_mappings."""
......@@ -396,6 +407,11 @@ class IliState:
"""Set the network_interface_mappings."""
self._network_interface_mappings = value
@property
def network_interface_attributes(self):
"""Return network_interface_attributes."""
return self._network_interface_attributes
# Block devices
def add_blockdevice(self, usage: str, blockdevice: str):
"""Add a block device that we will be creating a filesystems on."""
......
......@@ -52,6 +52,10 @@ class Plugin:
"""Commit the disk layout strategy."""
raise NotImplementedError
def process_network_interface_mapping(self, ili_state: IliState):
"""Process network interface mapping."""
raise NotImplementedError
def create_filesystems(self, ili_state: IliState):
"""Create filesystems."""
raise NotImplementedError
......@@ -113,6 +117,11 @@ class PluginCollection:
self._call_plugins(ili_state, 'get_networknaming_strategies')
def process_network_interface_mapping(self, ili_state: IliState):
"""Process network interface mapping."""
self._call_plugins(ili_state, 'process_network_interface_mapping')
def commit_diskusage_strategy(self, ili_state: IliState):
"""Call plugins to commit the disk usage strategy."""
......
......@@ -41,49 +41,89 @@ class ConfigSystemdNetworkd(Plugin):
ili_state.add_networknaming_strategy('NONE', 'Do not rename interfaces')
ili_state.add_networknaming_strategy('AUTO-SEQ', 'Rename en* devices as e0pN')
def config_system(self, ili_state: IliState):
"""Configure system systemd networkd."""
def process_network_interface_mapping(self, ili_state: IliState):
"""Commit the network naming to state."""
# Grab system network devices object
networkdevices = NetworkDevices()
# Grab the ethernet devices subset
ethernet_devices = networkdevices.ethernet
# Just return now if the strategy is NONE
if ili_state.networknaming_strategy == 'NONE':
return
ili_state.output_callback('Configuring systemd-networkd')
# This denotes if we're going to write out link config
write_link_config = False
# Load the network devices as they are
for device_name in sorted(ethernet_devices):
ili_state.add_network_interface_mapping(device_name, ethernet_devices[device_name]['mac'])
# Check if we have the automatic strategy
if ili_state.networknaming_strategy == 'AUTO-SEQ':
# Grab system network devices object
networkdevices = NetworkDevices()
# Grab the ethernet devices subset
ethernet_devices = networkdevices.ethernet
elif ili_state.networknaming_strategy == 'AUTO-SEQ':
# Loop with each device and name sequentially
seq = 0
for device_name in sorted(ethernet_devices):
ili_state.add_network_interface_mapping(f'e0p{seq}', ethernet_devices[device_name]['mac'])
seq += 1
# Write out link config below
write_link_config = True
# If this is custom naming, we don't need to do anything apart from write out the link config below
if ili_state.networknaming_strategy == 'CUSTOM':
write_link_config = True
# If we're going to write configs out, do it now
if write_link_config:
# Loop with interfaces name mapping and output config files
for device_name, device_mac in ili_state.network_interface_mappings.items():
# Open the hardware address file
with open(f'{ili_state.target_root}/etc/systemd/network/10-{device_name}.link', 'w') as link_file:
link_file.write(f'[Match]\n')
link_file.write(f'Path=pci-*\n')
link_file.write(f'MACAddress={device_mac}\n')
link_file.write('\n')
link_file.write(f'[Link]\n')
link_file.write(f'Name={device_name}\n')
link_file.close()
def config_system(self, ili_state: IliState):
"""Configure system systemd networkd."""
ili_state.output_callback('Configuring systemd-networkd')
# If the strategy is not NONE, then we need to name the interfaces
if ili_state.networknaming_strategy != 'NONE':
self._write_link_files(ili_state)
# Loop with network configuration and output config files
for device_name, config in ili_state.network_interface_attributes.items():
self._write_network_file(ili_state, device_name, config)
def _write_link_files(self, ili_state: IliState):
"""Write out link files."""
# Loop with interfaces name mapping and output config files
for device_name, device_mac in ili_state.network_interface_mappings.items():
# Open the hardware address file
with open(f'{ili_state.target_root}/etc/systemd/network/10-{device_name}.link', 'w') as link_file:
link_file.write(f'# Created during install\n')
link_file.write(f'[Match]\n')
link_file.write(f'Path=pci-*\n')
link_file.write(f'MACAddress={device_mac}\n')
link_file.write('\n')
link_file.write(f'[Link]\n')
link_file.write(f'Name={device_name}\n')
link_file.close()
def _write_network_file(self, ili_state: IliState, device_name: str, config: Dict[str, str]):
# Open the hardware address file
with open(f'{ili_state.target_root}/etc/systemd/network/50-{device_name}.network', 'w') as network_file:
network_file.write(f'# Created during install\n')
network_file.write(f'[Match]\n')
network_file.write(f'Name={device_name}\n')
network_file.write('\n')
network_file.write('[Network]\n')
# Check if this interface uses DHCP
if 'dhcp' in config:
network_file.write(f'DHCP=yes\n')
# Check if we have IP addresses
if 'ipv4address' in config:
network_file.write(f'address=%s\n' % config['ipv4address'])
if 'ipv6address' in config:
network_file.write(f'address=%s\n' % config['ipv6address'])
# Check if we have gateways
if 'ipv4gateway' in config:
network_file.write(f'gateway=%s\n' % config['ipv4gateway'])
if 'ipv6gateway' in config:
network_file.write(f'gateway=%s\n' % config['ipv6gateway'])
# Check if we have DNS servers
if 'dns1address' in config:
network_file.write(f'DNS=%s\n' % config['dns1address'])
if 'dns2address' in config:
network_file.write(f'DNS=%s\n' % config['dns2address'])
network_file.write(f'\n')
# Finally close the file
network_file.close()
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment