i officially hate netctl now i think

This commit is contained in:
brent s. 2019-11-30 01:05:20 -05:00
parent 5e57eb7bc5
commit 3a2eca4b98
13 changed files with 790 additions and 132 deletions

147
aif.xsd
View File

@ -169,11 +169,11 @@
</xs:restriction>
</xs:simpleType>

<xs:simpleType name="t_address_ip4">
<xs:simpleType name="t_addr_ip4">
<!-- This is a REALLY LAZY regex. Matching IPv4 in regex is ugly as heck, so we do that in-code.
This is just a gatekeeper. -->
<xs:restriction base="xs:string">
<!-- This is a REALLY LAZY regex. Matching IPv4 in regex is ugly as heck, so we do that in-code.
This is just a gatekeeper. -->
<xs:pattern value="(dhcp|[0-9.]{7,15}/[0-9]{1,2})"/>
<xs:pattern value="[0-9.]{7,15}/[0-9]{1,2}"/>
<xs:whiteSpace value="collapse"/>
</xs:restriction>
</xs:simpleType>
@ -187,11 +187,11 @@
</xs:restriction>
</xs:simpleType>

<xs:simpleType name="t_address_ip6">
<xs:simpleType name="t_addr_ip6">
<!-- This is a REALLY LAZY regex. Matching IPv6 in regex is ugly as heck, so we do that in-code.
This is just a gatekeeper. -->
<xs:restriction base="xs:string">
<!-- This is a REALLY LAZY regex. Matching IPv6 in regex is ugly as heck, so we do that in-code.
This is just a gatekeeper. -->
<xs:pattern value="(dhcp6|slaac|([A-Za-z0-9:]+)/[0-9]+)"/>
<xs:pattern value="[A-Za-z0-9:]+/[0-9]{1,3}"/>
<xs:whiteSpace value="collapse"/>
</xs:restriction>
</xs:simpleType>
@ -205,12 +205,21 @@
</xs:restriction>
</xs:simpleType>

<xs:simpleType name="t_resolver_addr">
<xs:simpleType name="t_both_addr">
<xs:union memberTypes="aif:t_addr_ip4 aif:t_addr_ip6"/>
</xs:simpleType>

<xs:simpleType name="t_both_gw">
<xs:union memberTypes="aif:t_gw_ip4 aif:t_gw_ip6"/>
</xs:simpleType>

<xs:simpleType name="t_ipv6_auto">
<xs:restriction base="xs:string">
<!-- This is a REALLY LAZY regex. Matching IPv4/IPv6 in regex is ugly as heck, so we do that in-code.
This is just a gatekeeper. -->
<xs:pattern value="([0-9.]{7,15}|[A-Za-z0-9:]+)"/>
<xs:whiteSpace value="collapse"/>
<xs:enumeration value="slaac"/>
<xs:enumeration value="dhcp6"/>
<xs:enumeration value="false"/>
<xs:enumeration value="none"/>
<xs:enumeration value="0"/>
</xs:restriction>
</xs:simpleType>

@ -223,6 +232,14 @@
</xs:restriction>
</xs:simpleType>

<xs:simpleType name="t_dhcp_clients">
<!-- Only valid for netctl. We use the internal daemons for systemd-networkd and NM. -->
<xs:restriction base="xs:string">
<xs:enumeration value="dhcpcd"/>
<xs:enumeration value="dhclient"/>
</xs:restriction>
</xs:simpleType>

<xs:simpleType name="t_netprov">
<xs:restriction base="xs:token">
<xs:enumeration value="netctl"/>
@ -245,7 +262,7 @@
<xs:element name="address" minOccurs="0" maxOccurs="unbounded">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="aif:t_address_ip4">
<xs:extension base="aif:t_addr_ip4">
<xs:attribute name="gateway"
type="aif:t_gw_ip4"
use="optional"/>
@ -254,6 +271,7 @@
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="auto" type="xs:boolean" use="optional" default="true"/>
</xs:complexType>
<xs:unique name="uniq_ipv4_addr">
<xs:selector xpath="aif:address"/>
@ -266,7 +284,7 @@
<xs:element name="address" minOccurs="0" maxOccurs="unbounded">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="aif:t_address_ip6">
<xs:extension base="aif:t_addr_ip6">
<xs:attribute name="gateway"
type="aif:t_gw_ip6"
use="optional"/>
@ -275,6 +293,8 @@
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="auto" type="aif:t_ipv6_auto"
use="optional" default="slaac"/>
</xs:complexType>
<xs:unique name="uniq_ipv6_addr">
<xs:selector xpath="aif:address"/>
@ -288,10 +308,85 @@
<xs:element name="resolvers" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:sequence minOccurs="1" maxOccurs="unbounded">
<xs:element name="resolver" minOccurs="1" maxOccurs="unbounded"
type="aif:t_resolver_addr"/>
<xs:choice minOccurs="1" maxOccurs="unbounded">
<xs:element name="resolver" minOccurs="1" maxOccurs="unbounded"
type="aif:t_both_gw"/>
<xs:element name="ipv4" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:attribute name="auto" type="xs:boolean"
use="required"/>
</xs:complexType>
</xs:element>
<xs:element name="ipv6" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:attribute name="auto" type="xs:boolean"
use="required"/>
</xs:complexType>
</xs:element>
</xs:choice>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="routes" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:sequence minOccurs="1" maxOccurs="unbounded">
<xs:choice minOccurs="1" maxOccurs="unbounded">
<xs:element name="ipv4">
<xs:complexType>
<xs:sequence>
<xs:element name="route" minOccurs="0" maxOccurs="unbounded">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="aif:t_addr_ip4">
<xs:attribute name="gateway"
type="aif:t_gw_ip4"
use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="auto" type="xs:boolean" use="optional" default="true"/>
<xs:attribute name="defaultGateway" type="xs:boolean"
use="optional" default="false"/>
</xs:complexType>
<xs:unique name="uniq_ipv4_route">
<xs:selector xpath="aif:route"/>
<xs:field xpath="."/>
</xs:unique>
</xs:element>
<xs:element name="ipv6">
<xs:complexType>
<xs:sequence>
<xs:element name="address" minOccurs="0" maxOccurs="unbounded">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="aif:t_addr_ip6">
<xs:attribute name="gateway"
type="aif:t_gw_ip6"
use="required"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:sequence>
<!-- DHCPv6 does not have an option to send routes,
so you need to use RAs. -->
<!-- See https://www.isc.org/blogs/routing-configuration-over-dhcpv6-2/
for more information. -->
<!-- https://datatracker.ietf.org/doc/draft-ietf-mif-dhcpv6-route-option/
expired. Shame, that. -->
<xs:attribute name="auto" type="xs:boolean" use="optional" default="true"/>
<xs:attribute name="defaultGateway" type="xs:boolean"
use="optional" default="false"/>
</xs:complexType>
<xs:unique name="uniq_ipv6_route">
<xs:selector xpath="aif:route"/>
<xs:field xpath="."/>
</xs:unique>
</xs:element>
</xs:choice>
</xs:sequence>
<xs:attribute name="noAuto" type="xs:boolean" use="optional" default="false"/>
</xs:complexType>
</xs:element>
</xs:choice>
@ -323,7 +418,7 @@
<xs:element name="type" minOccurs="1" maxOccurs="1" default="wpa2">
<xs:simpleType>
<xs:restriction base="xs:token">
<xs:enumeration value="wep"/>
<!-- <xs:enumeration value="wep"/> -->
<xs:enumeration value="wpa"/>
<xs:enumeration value="wpa2"/>
<!-- <xs:enumeration value="wpa3"/> -->
@ -331,14 +426,14 @@
</xs:restriction>
</xs:simpleType>
</xs:element>
<!-- "mode" only valid for WPA2 -->
<!-- "mode" only valid for WPA/WPA2 (and maybe WPA3 once supported?) -->
<xs:element name="mode" minOccurs="0" maxOccurs="1" default="personal">
<xs:simpleType>
<xs:restriction base="xs:token">
<!-- PSK -->
<xs:enumeration value="personal"/>
<!-- RADIUS -->
<xs:enumeration value="enterprise"/>
<!-- RADIUS, etc. -->
<!-- <xs:enumeration value="enterprise"/> -->
<xs:whiteSpace value="collapse"/>
</xs:restriction>
</xs:simpleType>
@ -351,7 +446,7 @@
<xs:simpleType>
<xs:restriction base="xs:token">
<xs:enumeration value="psk"/>
<xs:enumeration value="radius"/>
<!-- <xs:enumeration value="radius"/> -->
</xs:restriction>
</xs:simpleType>
</xs:attribute>
@ -369,7 +464,7 @@
<xs:element name="encryption" type="aif:t_wifi_crypto" minOccurs="0" maxOccurs="1"/>
</xs:sequence>
<xs:attribute name="essid" type="xs:string" use="required"/>
<xs:attribute name="bssid" type="xs:string" use="optional"/>
<xs:attribute name="bssid" type="aif:t_mac_addr" use="optional"/>
</xs:extension>
</xs:complexContent>
</xs:complexType>
@ -783,7 +878,6 @@
</xs:element>
<!-- END STORAGE -->
<!-- BEGIN NETWORK -->
<!-- TODO: make network optional? -->
<xs:element name="network" minOccurs="1" maxOccurs="1">
<xs:complexType>
<xs:choice minOccurs="1" maxOccurs="unbounded">
@ -793,7 +887,8 @@
</xs:choice>
<!-- It's nearly impossible to validate FQDNs/hostnames in XSD, so we do it in-code. -->
<xs:attribute name="hostname" type="aif:t_nonempty" use="required"/>
<xs:attribute name="provider" type="aif:t_netprov" use="optional" default="netctl"/>
<xs:attribute name="provider" type="aif:t_netprov" use="optional" default="systemd"/>
<xs:attribute name="dhcpClient" type="aif:t_dhcp_clients" use="optional" default="dhcpcd"/>
</xs:complexType>
<xs:unique name="uniq_iface_eth">
<xs:selector xpath="aif:ethernet|aif:wireless"/>

View File

@ -15,4 +15,18 @@ from . import pacman

class AIF(object):
def __init__(self):
# Process:
# 0.) get config (already initialized at this point)
# 1.) run pre scripts*
# 2.) initialize all objects' classes
# 3.) disk ops = partition, mount*
# 3.) b.) "pivot" logging here. create <chroot>/root/aif/ and copy log to <chroot>/root/aif/aif.log, use that
# as new log file. copy over scripts.
# 4.) install base system*
# 4.) b.) other system.* tasks. locale(s), etc.*
# 5.) run pkg scripts*
# 6.) install kernel(?), pkg items*
# 6.) b.) remember to install the .packages items for each object
# 7.) write out confs and other object application methods*
# * = log but don't do anything for dryrun
pass

View File

@ -10,6 +10,7 @@ VERSION = '0.2.0'
# blkinfo, mdstat, and pyparted are only needed for the non-gi fallbacks.
EXTERNAL_DEPS = ['blkinfo',
'gpg',
'jinja2',
'lxml',
'mdstat',
'parse',

View File

@ -1,19 +1,20 @@
from . import _common
from . import netctl
from . import networkd
from . import networkmanager
from . import net

# No longer necessary:
# try:
# from . import _common
# except ImportError:
# pass # GI isn't supported, so we don't even use a fallback.

from . import netctl

# TODO: use DBus interface for systemd but fallback to subprocess?
# http://0pointer.net/blog/the-new-sd-bus-api-of-systemd.html
# https://www.youtube.com/watch?v=ZUX9Fx8Rwzg
# https://www.youtube.com/watch?v=lBQgMGPxqNo
# https://github.com/facebookincubator/pystemd has some unit/service examples
try:
from . import networkd
except ImportError:
from . import networkd_fallback as networkd

from . import networkmanager
from . import net
# try:
# from . import networkd
# except ImportError:
# from . import networkd_fallback as networkd

View File

@ -1,3 +1,213 @@
import ipaddress
import os
import pathlib
import re
##
from pyroute2 import IPDB
##
import aif.utils

# Not needed
# import gi
# gi.require_version('NM', '1.0')
# from gi.repository import GObject, NM, GLib


def canonizeEUI(phyaddr):
# The easy transformations first.
phyaddr = re.sub(r'[.:-]', '', phyaddr.upper().strip())
eui = ':'.join(['{0}'.format(phyaddr[i:i+2]) for i in range(0, 12, 2)])
return(eui)


def convertIpTuples(addr_xmlobj):
# These tuples follow either:
# ('dhcp'/'dhcp6'/'slaac', None, None) for auto configuration
# (ipaddress.IPv4/6Address(IP), CIDR, ipaddress.IPv4/6Address(GW)) for static configuration
if addr_xmlobj.text in ('dhcp', 'dhcp6', 'slaac'):
addr = addr_xmlobj.text.strip()
net = None
gw = None
else:
components = addr_xmlobj.text.strip().split('/')
if len(components) > 2:
raise ValueError('Invalid IP/CIDR format: {0}'.format(addr_xmlobj.text))
if len(components) == 1:
addr = ipaddress.ip_address(components[0])
if addr.version == 4:
components.append('24')
elif addr.version == 6:
components.append('64')
addr = ipaddress.ip_address(components[0])
net = ipaddress.ip_network('/'.join(components), strict = False)
try:
gw = ipaddress.ip_address(addr_xmlobj.attrib.get('gateway').strip())
except (ValueError, AttributeError):
gw = next(net.hosts())
return((addr, net, gw))


def convertWifiCrypto(crypto_xmlobj):
crypto = {'type': crypto_xmlobj.find('type').text.strip()}
# if crypto['type'] in ('wpa', 'wpa2', 'wpa3'):
if crypto['type'] in ('wpa', 'wpa2'):
crypto['mode'] = crypto_xmlobj.find('mode')
if not crypto['mode']:
crypto['mode'] = 'personal'
else:
crypto['mode'] = crypto['mode'].text.strip()
else:
crypto['mode'] = None
creds = crypto_xmlobj.find('creds')
crypto['auth'] = {'type': creds.attrib.get('type', 'psk').strip()}
if crypto['auth']['type'] == 'psk':
crypto['auth']['psk'] = creds.text
# TODO: enterprise support
return(crypto)


def getDefIface(ifacetype):
if ifacetype == 'ethernet':
if isNotPersistent():
prefix = 'eth'
else:
prefix = 'en'
elif ifacetype == 'wireless':
prefix = 'wl'
else:
raise ValueError('ifacetype must be one of "ethernet" or "wireless"')
ifname = None
with IPDB() as ipdb:
for iface in ipdb.interfaces.keys():
if iface.startswith(prefix):
ifname = iface
break
if not ifname:
return(None)
return(ifname)


def isNotPersistent(chroot_base = '/'):
chroot_base = pathlib.Path(chroot_base)
systemd_override = chroot_base.joinpath('etc',
'systemd',
'network',
'99-default.link')
kernel_cmdline = chroot_base.joinpath('proc', 'cmdline')
devnull = chroot_base.joinpath('dev', 'null')
rootdevnull = pathlib.PosixPath('/dev/null')
if os.path.islink(systemd_override) and pathlib.Path(systemd_override).resolve() in (devnull, rootdevnull):
return(True)
cmds = aif.utils.kernelCmdline(chroot_base)
if 'net.ifnames' in cmds.keys() and cmds['net.ifnames'] == '0':
return(True)
return(False)


class BaseConnection(object):
def __init__(self, iface_xml):
self.xml = iface_xml
self.id = self.xml.attrib['id'].strip()
self.device = self.xml.attrib['device'].strip()
self.is_defroute = aif.utils.xmlBool(self.xml.attrib.get('defroute', 'false').strip())
try:
self.domain = self.xml.attrib.get('searchDomain').strip()
except AttributeError:
self.domain = None
self.dhcp_client = self.xml.attrib.get('dhcpClient', 'dhcpcd').strip()
self._cfg = None
self.connection_type = None
self.provider_type = None
self.packages = []
self.services = {}
self.resolvers = []
self.addrs = {'ipv4': [],
'ipv6': []}
self.routes = {'ipv4': [],
'ipv6': []}
self.auto = {}
for x in ('resolvers', 'routes', 'addresses'):
self.auto[x] = {}
x_xml = self.xml.find(x)
for t in ('ipv4', 'ipv6'):
if t == 'ipv6' and x == 'addresses':
self.auto[x][t] = 'slaac'
else:
self.auto[x][t] = True
if x_xml:
t_xml = x_xml.find(t)
if t_xml:
if t == 'ipv6' and x == 'addresses':
a = t_xml.attrib.get('auto', 'slaac').strip()
if a.lower() in ('false', '0', 'none'):
self.auto[x][t] = False
else:
self.auto[x][t] = a
else:
self.auto[x][t] = aif.utils.xmlBool(t_xml.attrib.get('auto', 'true').strip())
# These defaults are from the man page. However, we might want to add:
# domain-search, netbios-scope, interface-mtu, rfc3442-classless-static-routes, ntp-servers,
# dhcp6.fqdn, dhcp6.sntp-servers
# under requests and for requires, maybe:
# routers, domain-name-servers, domain-name, domain-search, host-name
self.dhcp_defaults = {
'dhclient': {'requests': {'ipv4': ('subnet-mask', 'broadcast-address', 'time-offset', 'routers',
'domain-name', 'domain-name-servers', 'host-name'),
'ipv6': ('dhcp6.name-servers',
'dhcp6.domain-search')},
'requires': {'ipv4': tuple(),
'ipv6': tuple()}},
'dhcpcd': {'default_opts': ('hostname', 'duid', 'persistent', 'slaac private', 'noipv4ll'),
# dhcpcd -V to display variables.
# "option <foo>", prepend "dhcp6_" for ipv6. if no ipv6 opts present, same are mapped to ipv6.
# But we explicitly add them for munging downstream.
'requests': {'ipv4': ('rapid_commit', 'domain_name_servers', 'domain_name', 'domain_search',
'host_name', 'classless_static_routes', 'interface_mtu'),
'ipv6': ('dhcp6_rapid_commit', 'dhcp6_domain_name_servers', 'dhcp6_domain_name',
'dhcp6_domain_search', 'dhcp6_host_name', 'dhcp6_classless_static_routes',
'dhcp6_interface_mtu')},
# "require <foo>"
'requires': {'ipv4': ('dhcp_server_identifier', ),
'ipv6': tuple()}}}
self._initAddrs()
self._initResolvers()
self._initRoutes()

def _initAddrs(self):
for addrtype in ('ipv4', 'ipv6'):
for a in self.xml.findall('addresses/{0}/address'.format(addrtype)):
addrset = convertIpTuples(a)
if addrset not in self.addrs[addrtype]:
self.addrs[addrtype].append(addrset)
return()

def _initCfg(self):
# A dummy method; this is overridden by the subclasses.
# It's honestly here to make my IDE stop complaining. :)
pass
return()

def _initConnCfg(self):
# A dummy method; this is overridden by the subclasses.
# It's honestly here to make my IDE stop complaining. :)
pass
return()

def _initResolvers(self):
resolvers_xml = self.xml.find('resolvers')
if resolvers_xml:
for r in resolvers_xml.findall('resolver'):
resolver = ipaddress.ip_address(r.text.strip())
if resolver not in self.resolvers:
self.resolvers.append(resolver)
return()

def _initRoutes(self):
routes_xml = self.xml.find('routes')
if routes_xml:
for addrtype in ('ipv4', 'ipv6'):
for a in self.xml.findall('routes/{0}/route'.format(addrtype)):
addrset = convertIpTuples(a)
if addrset not in self.routes[addrtype]:
self.routes[addrtype].append(addrset)
return()

View File

@ -1,8 +1,11 @@
import os


class Network(object):
def __init__(self, network_xml):
self.xml = network_xml
self.hostname = self.xml.attrib['hostname']
self.provider = self.xml.attrib.get('provider', 'netctl')
self.hostname = self.xml.attrib['hostname'].strip()
self.provider = self.xml.attrib.get('provider', 'systemd').strip()
handler = None
if self.provider == 'netctl':
import aif.network.netctl as handler
@ -24,3 +27,14 @@ class Network(object):
elif e.tag == 'wireless':
conn = self.provider.Wireless(e)
self.connections.append(conn)

def apply(self, chroot_base):
cfg = os.path.join(chroot_base, 'etc', 'hostname')
with open(cfg, 'w') as fh:
fh.write('{0}\n'.format(self.hostname))
os.chown(cfg, 0, 0)
os.chmod(cfg, 0o0644)
# TODO: symlinks for systemd for provider
# TODO: writeConf for provider

return()

View File

@ -0,0 +1,277 @@
import configparser
import io
import os
##
import aif.utils
import aif.network._common


class Connection(aif.network._common.BaseConnection):
def __init__(self, iface_xml):
super().__init__(iface_xml)
# TODO: disabling default route is not supported in-band.
# https://bugs.archlinux.org/task/64651
# TODO: disabling autoroutes is not supported in-band.
# https://bugs.archlinux.org/task/64651
# TODO: netctl profiles only support a single gateway.
# is there a way to manually add alternative gateways?
if not self.dhcp_client:
self.dhcp_client = 'dhcpcd'
self.provider_type = 'netctl'
self.packages = {'netctl', 'openresolv'}
self.services = {('/usr/lib/systemd/system/netctl@.service'): ('etc/systemd/system'
'/multi-user.target.wants'
'/netctl@{0}.service').format(self.id)}
# Only used if we need to override default dhcp/dhcp6 behaviour. I don't *think* we can customize SLAAC?
self.chroot_dir = os.path.join('etc', 'netctl', 'custom', self.dhcp_client)
self.chroot_cfg = os.path.join(self.chroot_dir, self.id)
self.desc = None

def _initCfg(self):
if self.device == 'auto':
self.device = aif.network._common.getDefIface(self.connection_type)
self.desc = ('A {0} profile for {1} (generated by AIF-NG)').format(self.connection_type,
self.device)
self._cfg = configparser.ConfigParser()
self._cfg.optionxform = str
# configparser *requires* sections. netctl doesn't use them. We strip it when we write.
self._cfg['BASE'] = {'Description': self.desc,
'Interface': self.device,
'Connection': self.connection_type}
# Addresses
if self.auto['addresses']['ipv4']:
self.packages.add(self.dhcp_client)
self._cfg['BASE']['IP'] = 'dhcp'
self._cfg['BASE']['DHCPClient'] = self.dhcp_client
else:
if self.addrs['ipv4']:
self._cfg['BASE']['IP'] = 'static'
else:
self._cfg['BASE']['IP'] = 'no'
if self.domain:
self._cfg['BASE']['DNSSearch'] = self.domain
if self.auto['addresses']['ipv6']:
if self.auto['addresses']['ipv6'] == 'slaac':
self._cfg['BASE']['IP6'] = 'stateless'
elif self.auto['addresses']['ipv6'] == 'dhcp6':
self._cfg['BASE']['IP6'] = 'dhcp'
self._cfg['BASE']['DHCP6Client'] = self.dhcp_client
self.packages.add(self.dhcp_client)
else:
if not self.addrs['ipv6']:
self._cfg['BASE']['IP6'] = 'no'
else:
self._cfg['BASE']['IP6'] = 'static'
for addrtype in ('ipv4', 'ipv6'):
keysuffix = ('6' if addrtype == 'ipv6' else '')
addrkey = 'Address{0}'.format(keysuffix)
gwkey = 'Gateway{0}'.format(keysuffix)
str_addrs = []
if self.addrs[addrtype] and not self.auto['addresses'][addrtype]:
for ip, cidr, gw in self.addrs[addrtype]:
if not self.is_defroute:
self._cfg['BASE'][gwkey] = str(gw)
str_addrs.append("'{0}/{1}'".format(str(ip), str(cidr.prefixlen)))
self._cfg['BASE'][addrkey] = '({0})'.format(' '.join(str_addrs))
elif self.addrs[addrtype]:
if 'IPCustom' not in self._cfg['BASE']:
# TODO: do this more cleanly somehow? Might conflict with other changes earlier/later.
# Weird hack because netctl doesn't natively support assigning add'l addrs to
# a dhcp/dhcp6/slaac iface.
self._cfg['BASE']['IPCustom'] = []
for ip, cidr, gw in self.addrs[addrtype]:
self._cfg['BASE']['IPCustom'].append("'ip address add {0}/{1} dev {2}'".format(str(ip),
str(cidr.prefixlen),
self.device))
# Resolvers may also require a change to /etc/resolvconf.conf?
for addrtype in ('ipv4', 'ipv6'):
if self.resolvers:
resolverkey = 'DNS'
str_resolvers = []
for r in self.resolvers:
str_resolvers.append("'{0}'".format(str(r)))
self._cfg['BASE'][resolverkey] = '({0})'.format(' '.join(str_resolvers))
# Routes
for addrtype in ('ipv4', 'ipv6'):
if self.routes[addrtype]:
keysuffix = ('6' if addrtype == 'ipv6' else '')
routekey = 'Routes{0}'.format(keysuffix)
str_routes = []
for dest, net, gw in self.routes[addrtype]:
str_routes.append("'{0}/{1} via {2}'".format(str(dest),
str(net.prefixlen),
str(gw)))
self._cfg['BASE'][routekey] = '({0})'.format(' '.join(str_routes))
# Weird hack because netctl doesn't natively support assigning add'l addrs to a dhcp/dhcp6/slaac iface.
if 'IPCustom' in self._cfg['BASE'].keys() and isinstance(self._cfg['BASE']['IPCustom'], list):
self._cfg['BASE']['IPCustom'] = '({0})'.format(' '.join(self._cfg['BASE']['IPCustom']))
return()

def writeConf(self, chroot_base):
systemd_base = os.path.join(chroot_base, 'etc', 'systemd', 'system')
systemd_file = os.path.join(systemd_base, 'netctl@{0}.service.d'.format(self.id), 'profile.conf')
netctl_file = os.path.join(chroot_base, 'etc', 'netctl', self.id)
for f in (systemd_file, netctl_file):
dpath = os.path.dirname(f)
os.makedirs(dpath, exist_ok = True)
os.chmod(dpath, 0o0755)
os.chown(dpath, 0, 0)
for root, dirs, files in os.walk(dpath):
for d in dirs:
fulld = os.path.join(root, d)
os.chmod(fulld, 0o0755)
os.chown(fulld, 0, 0)
systemd_cfg = configparser.ConfigParser()
systemd_cfg.optionxform = str
systemd_cfg['Unit'] = {'Description': self.desc,
'BindsTo': 'sys-subsystem-net-devices-{0}.device'.format(self.device),
'After': 'sys-subsystem-net-devices-{0}.device'.format(self.device)}
with open(systemd_file, 'w') as fh:
systemd_cfg.write(fh, space_around_delimiters = False)
# This is where it gets... weird.
# Gross hacky workarounds because netctl, while great for simple setups, sucks for complex/advanced ones.
no_auto = not all((self.auto['resolvers']['ipv4'],
self.auto['resolvers']['ipv6'],
self.auto['routes']['ipv4'],
self.auto['routes']['ipv6']))
no_dhcp = not any((self.auto['addresses']['ipv4'],
self.auto['addresses']['ipv6']))
if (no_auto and not no_dhcp) or (not self.is_defroute and not no_dhcp):
if self.dhcp_client == 'dhcpcd':
if not all((self.auto['resolvers']['ipv4'],
self.auto['routes']['ipv4'],
self.auto['addresses']['ipv4'])):
self._cfg['BASE']['DhcpcdOptions'] = "'--config {0}'".format(os.path.join('/', self.chroot_cfg))
if not all((self.auto['resolvers']['ipv6'],
self.auto['routes']['ipv6'],
self.auto['addresses']['ipv6'])):
self._cfg['BASE']['DhcpcdOptions6'] = "'--config {0}'".format(os.path.join('/', self.chroot_cfg))
elif self.dhcp_client == 'dhclient':
if not all((self.auto['resolvers']['ipv4'],
self.auto['routes']['ipv4'],
self.auto['addresses']['ipv4'])):
self._cfg['BASE']['DhcpcdOptions'] = "'-cf {0}'".format(os.path.join('/', self.chroot_cfg))
if not all((self.auto['resolvers']['ipv6'],
self.auto['routes']['ipv6'],
self.auto['addresses']['ipv6'])):
self._cfg['BASE']['DhcpcdOptions6'] = "'-cf {0}'".format(os.path.join('/', self.chroot_cfg))
custom_dir = os.path.join(chroot_base, self.chroot_dir)
custom_cfg = os.path.join(chroot_base, self.chroot_cfg)
os.makedirs(custom_dir, exist_ok = True)
for root, dirs, files in os.walk(custom_dir):
os.chown(root, 0, 0)
os.chmod(root, 0o0755)
for d in dirs:
dpath = os.path.join(root, d)
os.chown(dpath, 0, 0)
os.chmod(dpath, 0o0755)
for f in files:
fpath = os.path.join(root, f)
os.chown(fpath, 0, 0)
os.chmod(fpath, 0o0644)
# Modify DHCP options. WHAT a mess.
# The default requires are VERY sparse, and fine to remain unmangled for what we do.
opts = {}
for x in ('requests', 'requires'):
opts[x] = {}
for t in ('ipv4', 'ipv6'):
opts[x][t] = list(self.dhcp_defaults[self.dhcp_client][x][t])
opt_map = {
'dhclient': {
'resolvers': {
'ipv4': ('domain-name-servers', ),
'ipv6': ('dhcp6.domain-name-servers', )},
'routes': {
'ipv4': ('rfc3442-classless-static-routes', 'static-routes'),
'ipv6': tuple()}, # ???
# There is no way, as far as I can tell, to tell dhclient to NOT request an address.
'addresses': {
'ipv4': tuple(),
'ipv6': tuple()}},
'dhcpcd': {
'resolvers': {
'ipv4': ('domain_name_servers', ),
'ipv6': ('dhcp6_domain_name_servers', )},
'routes': {
'ipv4': ('classless_static_routes', 'static_routes'),
'ipv6': tuple()}, # ???
# I don't think dhcpcd lets us refuse an address.
'addresses': {
'ipv4': tuple(),
'ipv6': tuple()}}}
# This ONLY works for DHCPv6 on the IPv6 side. Not SLAAC. Netctl doesn't use a dhcp client for
# SLAAC.
# x = routers, addresses, resolvers
# t = ipv4/ipv6 dicts
# i = ipv4/ipv6 key
# v = boolean of auto
# o = each option for given auto type and IP type
for x, t in self.auto.items():
for i, v in t.items():
if not v:
for o in opt_map[self.dhcp_client][x][i]:
for n in ('requests', 'requires'):
if o in opts[n][i]:
opts[n][i].remove(o)
# We don't want the default route if we're not the default route iface.
if not self.is_defroute:
# IPv6 uses RA for the default route... We'll probably need to do that via an ExecUpPost?
# TODO.
for i in ('requests', 'requires'):
if 'routers' in opts[i]['ipv4']:
opts[i]['ipv4'].remove('routers')
if self.dhcp_client == 'dhclient':
conf = ['lease {',
' interface "{0}";'.format(self.device),
'}']
for i in ('request', 'require'):
k = '{0}s'.format(i)
optlist = []
for t in ('ipv4', 'ipv6'):
optlist.extend(opts[k][t])
if optlist:
conf.insert(-1, ' {0} {1};'.format(k, ', '.join(optlist)))
elif self.dhcp_client == 'dhcpcd':
conf = []
conf.extend(list(self.dhcp_defaults['dhcpcd']['default_opts']))
for i in ('requests', 'requires'):
if i == 'requests':
k = 'option'
else:
k = 'require'
optlist = []
optlist.extend(opts[i]['ipv4'])
optlist.extend(opts[i]['ipv6'])
# TODO: does require support comma-separated list like option does?
conf.append('{0} {1};'.format(k, ','.join(optlist)))
with open(custom_cfg, 'w') as fh:
fh.write('\n'.join(conf))
fh.write('\n')
os.chmod(custom_cfg, 0o0644)
os.chown(custom_cfg, 0, 0)
# And we have to strip out the section from the ini.
cfgbuf = io.StringIO()
self._cfg.write(cfgbuf, space_around_delimiters = False)
cfgbuf.seek(0, 0)
with open(netctl_file, 'w') as fh:
for line in cfgbuf.readlines():
if line.startswith('[BASE]') or line.strip() == '':
continue
fh.write(line)
os.chmod(netctl_file, 0o0600)
os.chown(netctl_file, 0, 0)
return()


class Ethernet(Connection):
def __init__(self, iface_xml):
super().__init__(iface_xml)
self.connection_type = 'ethernet'
self._initCfg()


class Wireless(Connection):
def __init__(self, iface_xml):
super().__init__(iface_xml)
self.connection_type = 'wireless'
self._initCfg()

View File

@ -1,4 +1,7 @@
import ipaddress
import socket


##
# We have to use Jinja2 because while there are ways to *parse* an INI with duplicate keys
# (https://stackoverflow.com/a/38286559/733214), there's no way to *write* an INI with them using configparser.
# So we use Jinja2 logic.
import jinja2

View File

@ -1,59 +1,33 @@
import configparser
import datetime
import ipaddress
import os
import uuid
##
import aif.utils

# TODO: auto dev assignment
import aif.network._common


class Connection(object):
class Connection(aif.network._common.BaseConnection):
def __init__(self, iface_xml):
self.xml = iface_xml
self.id = self.xml.attrib['id']
self.device = self.xml.attrib['device']
self.is_defroute = aif.utils.xmlBool(self.xml.attrib.get('defroute', 'false'))
self.domain = self.xml.attrib.get('searchDomain', None)
self._cfg = None
self.connection_type = None
super().__init__(iface_xml)
self.provider_type = 'NetworkManager'
self.addrs = {'ipv4': set(),
'ipv6': set()}
self.resolvers = []
self.packages = set('networkmanager')
self.services = {
('/usr/lib/systemd/system/NetworkManager.service'): ('etc/systemd/system/'
'multi-user.target.wants/'
'NetworkManager.service'),
('/usr/lib/systemd/system/NetworkManager-dispatcher.service'): ('etc/systemd/system/'
'dbus-org.freedesktop.'
'nm-dispatcher.service'),
('/usr/lib/systemd/system/NetworkManager-wait-online.service'): ('etc/systemd/'
'system/'
'network-online.target.wants/'
'NetworkManager-wait-online.service')}
self.uuid = uuid.uuid4()
self._initAddrs()
self._initResolvers()

def _initAddrs(self):
# These tuples follow either:
# ('dhcp'/'dhcp6'/'slaac', None, None) for auto configuration
# (ipaddress.IPv4/6Address(IP), CIDR, ipaddress.IPv4/6Address(GW)) for static configuration
for addrtype in ('ipv4', 'ipv6'):
for a in self.xml.findall('addresses/{0}/address'.format(addrtype)):
if a.text in ('dhcp', 'dhcp6', 'slaac'):
addr = a.text
net = None
gw = None
else:
components = a.text.split('/')
if len(components) > 2:
raise ValueError('Invalid IP/CIDR format: {0}'.format(a.text))
if len(components) == 1:
addr = components[0]
if addrtype == 'ipv4':
components.append('24')
elif addrtype == 'ipv6':
components.append('64')
addr = ipaddress.ip_address(components[0])
net = ipaddress.ip_network('/'.join(components), strict = False)
gw = ipaddress.ip_address(a.attrib.get('gateway'))
self.addrs[addrtype].add((addr, net, gw))
self.addrs[addrtype] = list(self.addrs[addrtype])
return()

def _initCfg(self):
if self.device == 'auto':
self.device = aif.network._common.getDefIface(self.connection_type)
self._cfg = configparser.ConfigParser()
self._cfg.optionxform = str
self._cfg['connection'] = {'id': self.id,
@ -63,61 +37,60 @@ class Connection(object):
'permissions': '',
'timestamp': datetime.datetime.utcnow().timestamp()}
# We *theoretically* could do this in _initAddrs() but we do it separately so we can trim out duplicates.
# TODO: rework this? we technically don't need to split in ipv4/ipv6 since ipaddress does that for us.
for addrtype, addrs in self.addrs.items():
self._cfg[addrtype] = {}
cidr_gws = {}
# Routing
if not self.is_defroute:
self._cfg[addrtype]['never-default'] = 'true'
if not self.auto['routes'][addrtype]:
self._cfg[addrtype]['ignore-auto-routes'] = 'true'
# DNS
self._cfg[addrtype]['dns-search'] = (self.domain if self.domain else '')
if not self.auto['resolvers'][addrtype]:
self._cfg[addrtype]['ignore-auto-dns'] = 'true'
# Address handling
if addrtype == 'ipv6':
self._cfg[addrtype]['addr-gen-mode'] = 'stable-privacy'
if not addrs:
if not addrs and not self.auto['addresses'][addrtype]:
self._cfg[addrtype]['method'] = 'ignore'
elif self.auto['addresses'][addrtype]:
if addrtype == 'ipv4':
self._cfg[addrtype]['method'] = 'auto'
else:
self._cfg[addrtype]['method'] = ('auto' if self.auto['addresses'][addrtype] == 'slaac'
else 'dhcp6')
else:
self._cfg[addrtype]['method'] = 'manual'
for idx, (ip, cidr, gw) in enumerate(addrs):
if cidr not in cidr_gws.keys():
cidr_gws[cidr] = gw
new_cidr = True
else:
new_cidr = False
if addrtype == 'ipv4':
if ip == 'dhcp':
self._cfg[addrtype]['method'] = 'auto'
continue
elif addrtype == 'ipv6':
if ip == 'dhcp6':
self._cfg[addrtype]['method'] = 'dhcp'
continue
elif ip == 'slaac':
self._cfg[addrtype]['method'] = 'auto'
continue
addrnum = idx + 1
addr_str = '{0}/{1}'.format(str(ip), str(cidr.prefixlen))
if new_cidr:
addr_str = '{0},{1}'.format(addr_str, str(gw))
self._cfg[addrtype]['address{0}'.format(addrnum)] = addr_str
for r in self.resolvers:
if addrtype == 'ipv{0}'.format(r.version):
for idx, (ip, cidr, gw) in enumerate(addrs):
if cidr not in cidr_gws.keys():
cidr_gws[cidr] = gw
new_cidr = True
else:
new_cidr = False
addrnum = idx + 1
addr_str = '{0}/{1}'.format(str(ip), str(cidr.prefixlen))
if new_cidr:
addr_str = '{0},{1}'.format(addr_str, str(gw))
self._cfg[addrtype]['address{0}'.format(addrnum)] = addr_str
# Resolvers
for resolver in self.resolvers:
if addrtype == 'ipv{0}'.format(resolver.version):
if 'dns' not in self._cfg[addrtype]:
self._cfg[addrtype]['dns'] = []
self._cfg[addrtype]['dns'].append(str(r))
self._cfg[addrtype]['dns'].append(str(resolver))
if 'dns' in self._cfg[addrtype].keys():
self._cfg[addrtype]['dns'] = '{0};'.format(';'.join(self._cfg[addrtype]['dns']))
# Routes
for idx, (dest, net, gw) in self.routes[addrtype]:
routenum = idx + 1
self._cfg[addrtype]['route{0}'.format(routenum)] = '{0}/{1},{2}'.format(str(dest),
str(net.prefixlen),
str(gw))
self._initConnCfg()
return()

def _initConnCfg(self):
# A dummy method; this is overridden by the subclasses.
# It's honestly here to make my IDE stop complaining. :)
pass
return()

def _initResolvers(self):
for r in self.xml.findall('resolvers/resolver'):
resolver = ipaddress.ip_address(r.text)
if resolver not in self.resolvers:
self.resolvers.append(resolver)
return()

def writeConf(self, chroot_base):
cfgroot = os.path.join(chroot_base, 'etc', 'NetworkManager')
cfgdir = os.path.join(cfgroot, 'system-connections')
@ -146,7 +119,8 @@ class Ethernet(Connection):
self._initCfg()

def _initConnCfg(self):
pass
self._cfg[self.connection_type] = {'mac-address-blacklist': ''}
return()


class Wireless(Connection):
@ -156,4 +130,27 @@ class Wireless(Connection):
self._initCfg()

def _initConnCfg(self):
pass
self._cfg['wifi'] = {'mac-address-blacklist': '',
'mode': 'infrastructure',
'ssid': self.xml.attrib['essid']}
try:
bssid = self.xml.attrib.get('bssid').strip()
except AttributeError:
bssid = None
if bssid:
bssid = aif.network._common.canonizeEUI(bssid)
self._cfg['wifi']['bssid'] = bssid
self._cfg['wifi']['seen-bssids'] = '{0};'.format(bssid)
crypto = self.xml.find('encryption')
if crypto:
self.packages.add('wpa_supplicant')
self._cfg['wifi-security'] = {}
crypto = aif.network._common.convertWifiCrypto(crypto)
# if crypto['type'] in ('wpa', 'wpa2', 'wpa3'):
if crypto['type'] in ('wpa', 'wpa2'):
# TODO: WPA2 enterprise
self._cfg['wifi-security']['key-mgmt'] = 'wpa-psk'
# if crypto['type'] in ('wep', 'wpa', 'wpa2', 'wpa3'):
if crypto['type'] in ('wpa', 'wpa2'):
self._cfg['wifi-security']['psk'] = crypto['auth']['psk']
return()

View File

@ -1,6 +1,8 @@
import math
import os
import pathlib
import re
import shlex
import subprocess
##
import psutil
@ -55,6 +57,22 @@ def isPowerofTwo(n):
return(isPowerOf2)


def kernelCmdline(chroot_base = '/'):
cmds = {}
chroot_base = pathlib.PosixPath(chroot_base)
cmdline = chroot_base.joinpath('proc', 'cmdline')
if not os.path.isfile(cmdline):
return(cmds)
with open(cmdline, 'r') as fh:
raw_cmds = fh.read().strip()
for c in shlex.split(raw_cmds):
l = c.split('=', 1)
if len(l) < 2:
l.append(None)
cmds[l[0]] = l[1]
return(cmds)


def kernelFilesystems():
# I wish there was a better way of doing this.
# https://unix.stackexchange.com/a/98680

View File

@ -571,6 +571,9 @@ Using `start`/`stop` attributes makes sense for disk partitions because they ope

LVM (LVs, in particular), however, aren't consecutive. There *is* no concept of a "start" and "stop" for an LV; LVM uses chunks called "(physical) extents" rather than sectors, and VGs don't have geometry since they're essentially a pool of blocks. This is also why the modifiers like `-` and `+` aren't allowed for LV sizes - they're position-based.

=== "How do I specify packages from the AUR?"
You'd have to https://wiki.archlinux.org/index.php/Makepkg[build the package(s)^], https://wiki.archlinux.org/index.php/Pacman/Tips_and_tricks#Custom_local_repository[set up a repository^], serve it via e.g. https://www.nginx.com/[nginx^], and add it as a repo (`/aif/pacman/repos/repo`) first. Then you can specify the package as normal as a `/aif/pacman/software/package` item.

=== "Why aren't the network settings in <network> being applied during install?"
Simply put, a logical race condition. In order for probably 90+% of AIF-NG deploys to bootstrap, they fetch their XML configuration via a network URI (rather than a file URI). This means it needs a network connection that pre-exists in the *install environment* (LiveCD, LiveUSB, PXE/iPXE, etc.) before it even knows what network configuration you want the *persistent environment* to have.

@ -580,6 +583,23 @@ If you desire the configuration to be applied *during* the install, you can do i

If you wish to SSH into the install environment to check the status/progress of the install, it is recommended that you set up a static lease (if using DHCP) or use SLAAC (if using IPv6) beforehand and configure your install environment beforehand. Remember, AIF-NG only *installs* Arch Linux; it tries very hard to *not* interact with the install environment.

=== "Why isn't enabling/disabling automatic DNS resolvers working?"
This is going to be highly unpredictable based on the networking provider you choose. This is a limitation of underlying network provider intercompatibility, resolver libraries, and technology architecture. This may be changed in the future, but because of how DNS servers are handled via DHCP/RDNSS and glibc (and the fact that IPv4 resolver addresses can serve IPv6 -- e.g. AAAA -- records and vice versa) and inherent limitations in some network providers like netctl, I wouldn't hold your breath.

=== "I'm using netctl as my network provider, and-"
I'ma let you finish, but netctl is a *really* simple network provider. I mean REALLY simple. As such, a lot of things (like mixing auto DNS and non-auto addressing) don't work at all feasibly, and probably might not ever. It's great for simple and flat configurations (i.e. all static everything, all automatic everything, etc.) and I even use it on my own machines where I can, but it just simply doesn't make allowances for more complex setups. (This is why init scripts were replaced by systemd for init, remember? Script-and-shell-based utilities, such as netctl -- seriously, the entire thing's written in Bash -- just can't handle more complex jobs reliably.)

If you need more advanced functionality but don't want a lot of cruft or bloat, I recommend `systemd` as your network provider. It requires no extra packages (other than wpa_supplicant, if you're using wireless) because it's part of the systemd package (which is part of the most basic install of Arch) and handles more advanced configurations a lot more reliably.

=== "How do I specify WEP for a wireless network?"
You can't. WEP's pretty broken. I understand some legacy networks may still use it, but I'm incredibly uncomfortable supporting it.

If absolutely necessary, you can manually configure it yourself via a `/aif/scripts/post/script` script.

=== "How do I connect to a WPA2 Enterprise network?"
You can't, currently; support is only stubbed out for now. If absolutely necessary, you can manually configure it yourself via a `/aif/scripts/post/script` script.

This hopefully will be changed in the future, however, as I'm interested in adding support. For now, WPA/WPA2 PSK only are considered supported.

== Bug Reports/Feature Requests
NOTE: It is possible to submit a bug or feature request without registering in my bugtracker. One of my pet peeves is needing to create an account/register on a bugtracker simply to report a bug! The following links only require an email address to file a bug (which is necessary in case I need any further clarification from you or to keep you updated on the status of the bug/feature request -- so please be sure to use a valid email address).

View File

@ -122,16 +122,23 @@
<network hostname="aiftest.square-r00t.net" provider="netctl">
<ethernet id="lan" device="auto" defroute="true" searchDomain="domain.tld">
<addresses>
<ipv4>
<address>dhcp</address>
<ipv4 auto="true">
<address gateway="192.168.1.1">192.168.1.5/24</address>
</ipv4>
<ipv6>
<address>slaac</address>
<ipv6 auto="slaac">
<address>fde4:16b9:654b:bbfa::15/64</address>
</ipv6>
</addresses>
<resolvers noAuto="true">
<routes>
<ipv4 defaultGateway="true" auto="true">
<route gateway="192.168.1.1">10.1.1.0/24</route>
<route gateway="10.1.1.4">172.16.1.20/32</route>
</ipv4>
<ipv6 defaultGateway="false" auto="true"/>
</routes>
<resolvers>
<ipv4 auto="false"/>
<ipv6 auto="false"/>
<resolver>64.6.64.6</resolver>
<resolver>4.2.2.1</resolver>
<resolver>8.8.8.8</resolver>
@ -140,10 +147,11 @@
<wireless id="wlan" device="wlp2s0" essid="MyWirelessLan"
bssid="00-00-5E-00-53-00" defroute="false" searchDomain="wifi.lan">
<addresses>
<ipv4>
<address>dhcp</address>
</ipv4>
<ipv4 auto="true"/>
</addresses>
<routes>
<ipv6 defaultGateway="true" auto="true"/>
</routes>
<encryption>
<type>wpa2</type>
<mode>personal</mode>