diff --git a/aif.xsd b/aif.xsd
index ede6290..54dd294 100644
--- a/aif.xsd
+++ b/aif.xsd
@@ -169,11 +169,11 @@
-
+
+
-
-
+
@@ -187,11 +187,11 @@
-
+
+
-
-
+
@@ -205,12 +205,21 @@
-
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
@@ -223,6 +232,14 @@
+
+
+
+
+
+
+
+
@@ -245,7 +262,7 @@
-
+
@@ -254,6 +271,7 @@
+
@@ -266,7 +284,7 @@
-
+
@@ -275,6 +293,8 @@
+
@@ -288,10 +308,85 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
@@ -323,7 +418,7 @@
-
+
@@ -331,14 +426,14 @@
-
+
-
-
+
+
@@ -351,7 +446,7 @@
-
+
@@ -369,7 +464,7 @@
-
+
@@ -783,7 +878,6 @@
-
@@ -793,7 +887,8 @@
-
+
+
diff --git a/aif/__init__.py b/aif/__init__.py
index 5e8d022..bb5745b 100644
--- a/aif/__init__.py
+++ b/aif/__init__.py
@@ -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 /root/aif/ and copy log to /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
diff --git a/aif/constants_fallback.py b/aif/constants_fallback.py
index 2217f03..f9672b4 100644
--- a/aif/constants_fallback.py
+++ b/aif/constants_fallback.py
@@ -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',
diff --git a/aif/network/__init__.py b/aif/network/__init__.py
index bad57e3..e419203 100644
--- a/aif/network/__init__.py
+++ b/aif/network/__init__.py
@@ -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
diff --git a/aif/network/_common.py b/aif/network/_common.py
index 8d87a29..9d7312c 100644
--- a/aif/network/_common.py
+++ b/aif/network/_common.py
@@ -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 ", 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 "
+ '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()
diff --git a/aif/network/net.py b/aif/network/net.py
index 11938df..622162b 100644
--- a/aif/network/net.py
+++ b/aif/network/net.py
@@ -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()
diff --git a/aif/network/netctl.py b/aif/network/netctl.py
index e69de29..11cd01a 100644
--- a/aif/network/netctl.py
+++ b/aif/network/netctl.py
@@ -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()
diff --git a/aif/network/networkd.py b/aif/network/networkd.py
index dca0967..0c1bb62 100644
--- a/aif/network/networkd.py
+++ b/aif/network/networkd.py
@@ -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
diff --git a/aif/network/networkd_fallback.py b/aif/network/networkd_fallback.py
deleted file mode 100644
index e69de29..0000000
diff --git a/aif/network/networkmanager.py b/aif/network/networkmanager.py
index a477d32..e1facd4 100644
--- a/aif/network/networkmanager.py
+++ b/aif/network/networkmanager.py
@@ -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()
diff --git a/aif/utils.py b/aif/utils.py
index 48ef2da..9aa042d 100644
--- a/aif/utils.py
+++ b/aif/utils.py
@@ -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
diff --git a/docs/MANUAL.adoc b/docs/MANUAL.adoc
index 66b82ea..32f1e31 100644
--- a/docs/MANUAL.adoc
+++ b/docs/MANUAL.adoc
@@ -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 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).
diff --git a/examples/aif.xml b/examples/aif.xml
index 5306aa0..8bb9b47 100644
--- a/examples/aif.xml
+++ b/examples/aif.xml
@@ -122,16 +122,23 @@
-
- dhcp
+
192.168.1.5/24
-
- slaac
+
fde4:16b9:654b:bbfa::15/64
-
+
+
+ 10.1.1.0/24
+ 172.16.1.20/32
+
+
+
+
+
+
64.6.64.6
4.2.2.1
8.8.8.8
@@ -140,10 +147,11 @@
-
- dhcp
-
+
+
+
+
wpa2
personal