i officially hate netctl now i think
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user