summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
authorThomas Mangin <thomas.mangin@exa.net.uk>2020-04-10 21:20:02 +0100
committerThomas Mangin <thomas.mangin@exa.net.uk>2020-04-11 11:25:07 +0100
commit7b1a76063b15b238702cc86a71c5f0604c994920 (patch)
treefc1510be76fde5e3b7dbe799897cff3c59172f46 /python
parentcf51589163956bb53c55493c27b177a9d2d5c806 (diff)
downloadvyos-1x-7b1a76063b15b238702cc86a71c5f0604c994920.tar.gz
vyos-1x-7b1a76063b15b238702cc86a71c5f0604c994920.zip
dhcp: T2265: refactor DHCP class
Break the code between v4 and v6, remove need for getter/setter as they are just exposing the underlying dict. Move FixedDict from tunnel code and expose it to other part so it can be used to prevent accidental change to the dhcp option if no default exists already.
Diffstat (limited to 'python')
-rw-r--r--python/vyos/dicts.py50
-rw-r--r--python/vyos/ifconfig/dhcp.py222
-rw-r--r--python/vyos/ifconfig/interface.py22
-rw-r--r--python/vyos/ifconfig_vlan.py22
4 files changed, 166 insertions, 150 deletions
diff --git a/python/vyos/dicts.py b/python/vyos/dicts.py
new file mode 100644
index 000000000..79cab4a08
--- /dev/null
+++ b/python/vyos/dicts.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+class FixedDict(dict):
+ """
+ FixedDict: A dictionnary not allowing new keys to be created after initialisation.
+
+ >>> f = FixedDict(**{'count':1})
+ >>> f['count'] = 2
+ >>> f['king'] = 3
+ File "...", line ..., in __setitem__
+ raise ConfigError(f'Option "{k}" has no defined default')
+ """
+
+ def __init__(self, **options):
+ self._allowed = options.keys()
+ super().__init__(**options)
+
+ def __setitem__(self, k, v):
+ """
+ __setitem__ is a builtin which is called by python when setting dict values:
+ >>> d = dict()
+ >>> d['key'] = 'value'
+ >>> d
+ {'key': 'value'}
+
+ is syntaxic sugar for
+
+ >>> d = dict()
+ >>> d.__setitem__('key','value')
+ >>> d
+ {'key': 'value'}
+ """
+ if k not in self._allowed:
+ raise ConfigError(f'Option "{k}" has no defined default')
+ super().__setitem__(k, v)
diff --git a/python/vyos/ifconfig/dhcp.py b/python/vyos/ifconfig/dhcp.py
index 8ec8263b5..5b0a0bf6a 100644
--- a/python/vyos/ifconfig/dhcp.py
+++ b/python/vyos/ifconfig/dhcp.py
@@ -16,103 +16,55 @@
import os
import jinja2
+from vyos.dicts import FixedDict
from vyos.ifconfig.control import Control
-template_v4 = """
-# generated by ifconfig.py
-option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;
-timeout 60;
-retry 300;
-
-interface "{{ intf }}" {
- send host-name "{{ hostname }}";
- {% if client_id -%}
- send dhcp-client-identifier "{{ client_id }}";
- {% endif -%}
- {% if vendor_class_id -%}
- send vendor-class-identifier "{{ vendor_class_id }}";
- {% endif -%}
- request subnet-mask, broadcast-address, routers, domain-name-servers,
- rfc3442-classless-static-routes, domain-name, interface-mtu;
- require subnet-mask;
-}
-
-"""
-
-template_v6 = """
-# generated by ifconfig.py
-interface "{{ intf }}" {
- request routers, domain-name-servers, domain-name;
-}
-
-"""
-
-class DHCP (Control):
+
+class _DHCP (Control):
client_base = r'/var/lib/dhcp/dhclient_'
- def __init__ (self, ifname, **kargs):
+ def __init__(self, ifname, version, **kargs):
super().__init__(**kargs)
-
- # per interface DHCP config files
- self._dhcp = {
- 4: {
- 'ifname': ifname,
- 'conf': self.client_base + ifname + '.conf',
- 'pid': self.client_base + ifname + '.pid',
- 'lease': self.client_base + ifname + '.leases',
- 'options': {
- 'intf': ifname,
- 'hostname': '',
- 'client_id': '',
- 'vendor_class_id': ''
- },
- },
- 6: {
- 'ifname': ifname,
- 'conf': self.client_base + ifname + '.v6conf',
- 'pid': self.client_base + ifname + '.v6pid',
- 'lease': self.client_base + ifname + '.v6leases',
- 'accept_ra': f'/proc/sys/net/ipv6/conf/{ifname}/accept_ra',
- 'options': {
- 'intf': ifname,
- 'dhcpv6_prm_only': False,
- 'dhcpv6_temporary': False
- },
- },
+ self.version = version
+ self.file = {
+ 'ifname': ifname,
+ 'conf': self.client_base + ifname + '.' + version + 'conf',
+ 'pid': self.client_base + ifname + '.' + version + 'pid',
+ 'lease': self.client_base + ifname + '.' + version + 'leases',
}
- def get_dhcp_options(self):
- """
- Return dictionary with supported DHCP options.
-
- Dictionary should be altered and send back via set_dhcp_options()
- so those options are applied when DHCP is run.
- """
- return self._dhcp[4]['options']
-
- def set_dhcp_options(self, options):
- """
- Store new DHCP options used by next run of DHCP client.
- """
- self._dhcp[4]['options'] = options
-
- def get_dhcpv6_options(self):
- """
- Return dictionary with supported DHCPv6 options.
-
- Dictionary should be altered and send back via set_dhcp_options()
- so those options are applied when DHCP is run.
- """
- return self._dhcp[6]['options']
-
- def set_dhcpv6_options(self, options):
- """
- Store new DHCP options used by next run of DHCP client.
- """
- self._dhcp[6]['options'] = options
+class _DHCPv4 (_DHCP):
+ template = """\
+ # generated by ifconfig.py
+ option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;
+ timeout 60;
+ retry 300;
+
+ interface "{{ ifname }}" {
+ send host-name "{{ hostname }}";
+ {% if client_id -%}
+ send dhcp-client-identifier "{{ client_id }}";
+ {% endif -%}
+ {% if vendor_class_id -%}
+ send vendor-class-identifier "{{ vendor_class_id }}";
+ {% endif -%}
+ request subnet-mask, broadcast-address, routers, domain-name-servers,
+ rfc3442-classless-static-routes, domain-name, interface-mtu;
+ require subnet-mask;
+ }
+ """.replace(' ', '')
+
+ def __init__(self, ifname):
+ super().__init__(ifname, '')
+ self.options = FixedDict(**{
+ 'ifname': ifname,
+ 'hostname': '',
+ 'client_id': '',
+ 'vendor_class_id': ''
+ })
# replace dhcpv4/v6 with systemd.networkd?
- def _set_dhcp(self):
+ def set(self):
"""
Configure interface as DHCP client. The dhclient binary is automatically
started in background!
@@ -121,21 +73,19 @@ class DHCP (Control):
>>> from vyos.ifconfig import Interface
>>> j = Interface('eth0')
- >>> j.set_dhcp()
+ >>> j.dhcp.v4.set()
"""
- dhcp = self.get_dhcp_options()
- if not dhcp['hostname']:
+ if not self.options['hostname']:
# read configured system hostname.
# maybe change to vyos hostd client ???
with open('/etc/hostname', 'r') as f:
- dhcp['hostname'] = f.read().rstrip('\n')
+ self.options['hostname'] = f.read().rstrip('\n')
# render DHCP configuration
- tmpl = jinja2.Template(template_v4)
- dhcp_text = tmpl.render(dhcp)
- with open(self._dhcp[4]['conf'], 'w') as f:
- f.write(dhcp_text)
+ tmpl = jinja2.Template(self.template)
+ with open(self.file['conf'], 'w') as f:
+ f.write(tmpl.render(self.options))
cmd = 'start-stop-daemon'
cmd += ' --start'
@@ -146,9 +96,9 @@ class DHCP (Control):
cmd += ' --'
# now pass arguments to dhclient binary
cmd += ' -4 -nw -cf {conf} -pf {pid} -lf {lease} {ifname}'
- return self._cmd(cmd.format(**self._dhcp[4]))
+ return self._cmd(cmd.format(**self.file))
- def _del_dhcp(self):
+ def delete(self):
"""
De-configure interface as DHCP clinet. All auto generated files like
pid, config and lease will be removed.
@@ -157,14 +107,14 @@ class DHCP (Control):
>>> from vyos.ifconfig import Interface
>>> j = Interface('eth0')
- >>> j.del_dhcp()
+ >>> j.dhcp.v4.delete()
"""
- if not os.path.isfile(self._dhcp[4]['pid']):
+ if not os.path.isfile(self.file['pid']):
self._debug_msg('No DHCP client PID found')
return None
- # with open(self._dhcp[4]['pid'], 'r') as f:
- # pid = int(f.read())
+ # with open(self.file['pid'], 'r') as f:
+ # pid = int(f.read())
# stop dhclient, we need to call dhclient and tell it should release the
# aquired IP address. tcpdump tells me:
@@ -178,14 +128,35 @@ class DHCP (Control):
# Hostname Option 12, length 10: "vyos"
#
cmd = '/sbin/dhclient -cf {conf} -pf {pid} -lf {lease} -r {ifname}'
- self._cmd(cmd.format(**self._dhcp[4]))
+ self._cmd(cmd.format(**self.file))
# cleanup old config files
for name in ('conf', 'pid', 'lease'):
- if os.path.isfile(self._dhcp[4][name]):
- os.remove(self._dhcp[4][name])
+ if os.path.isfile(self.file[name]):
+ os.remove(self.file[name])
- def _set_dhcpv6(self):
+
+class _DHCPv6 (_DHCP):
+ template = """\
+ # generated by ifconfig.py
+ interface "{{ ifname }}" {
+ request routers, domain-name-servers, domain-name;
+ }
+
+ """.replace(' ', '')
+
+ def __init__(self, ifname):
+ super().__init__(ifname, 'v6')
+ self.options = FixedDict(**{
+ 'ifname': ifname,
+ 'dhcpv6_prm_only': False,
+ 'dhcpv6_temporary': False,
+ })
+ self.file.update({
+ 'accept_ra': f'/proc/sys/net/ipv6/conf/{ifname}/accept_ra',
+ })
+
+ def set(self):
"""
Configure interface as DHCPv6 client. The dhclient binary is automatically
started in background!
@@ -196,22 +167,20 @@ class DHCP (Control):
>>> j = Interface('eth0')
>>> j.set_dhcpv6()
"""
- dhcpv6 = self.get_dhcpv6_options()
# better save then sorry .. should be checked in interface script
# but if you missed it we are safe!
- if dhcpv6['dhcpv6_prm_only'] and dhcpv6['dhcpv6_temporary']:
+ if self.options['dhcpv6_prm_only'] and self.options['dhcpv6_temporary']:
raise Exception(
'DHCPv6 temporary and parameters-only options are mutually exclusive!')
# render DHCP configuration
- tmpl = jinja2.Template(template_v6)
- dhcpv6_text = tmpl.render(dhcpv6)
- with open(self._dhcp[6]['conf'], 'w') as f:
- f.write(dhcpv6_text)
+ tmpl = jinja2.Template(self.template)
+ with open(self.file['conf'], 'w') as f:
+ f.write(tmpl.render(self.options))
# no longer accept router announcements on this interface
- self._write_sysfs(self._dhcp[6]['accept_ra'], 0)
+ self._write_sysfs(self.file['accept_ra'], 0)
# assemble command-line to start DHCPv6 client (dhclient)
cmd = 'start-stop-daemon'
@@ -224,15 +193,15 @@ class DHCP (Control):
# now pass arguments to dhclient binary
cmd += ' -6 -nw -cf {conf} -pf {pid} -lf {lease}'
# add optional arguments
- if dhcpv6['dhcpv6_prm_only']:
+ if self.options['dhcpv6_prm_only']:
cmd += ' -S'
- if dhcpv6['dhcpv6_temporary']:
+ if self.options['dhcpv6_temporary']:
cmd += ' -T'
cmd += ' {ifname}'
- return self._cmd(cmd.format(**self._dhcp[6]))
+ return self._cmd(cmd.format(**self.file))
- def _del_dhcpv6(self):
+ def delete(self):
"""
De-configure interface as DHCPv6 clinet. All auto generated files like
pid, config and lease will be removed.
@@ -243,12 +212,12 @@ class DHCP (Control):
>>> j = Interface('eth0')
>>> j.del_dhcpv6()
"""
- if not os.path.isfile(self._dhcp[6]['pid']):
+ if not os.path.isfile(self.file['pid']):
self._debug_msg('No DHCPv6 client PID found')
return None
- # with open(self._dhcp[6]['pid'], 'r') as f:
- # pid = int(f.read())
+ # with open(self.file['pid'], 'r') as f:
+ # pid = int(f.read())
# stop dhclient
cmd = 'start-stop-daemon'
@@ -256,13 +225,18 @@ class DHCP (Control):
cmd += ' --oknodo'
cmd += ' --quiet'
cmd += ' --pidfile {pid}'
- self._cmd(cmd.format(**self._dhcp[6]))
+ self._cmd(cmd.format(**self.file))
# accept router announcements on this interface
- self._write_sysfs(self._dhcp[6]['accept_ra'], 1)
+ self._write_sysfs(self.options['accept_ra'], 1)
# cleanup old config files
for name in ('conf', 'pid', 'lease'):
- if os.path.isfile(self._dhcp[6][name]):
- os.remove(self._dhcp[6][name])
+ if os.path.isfile(self.file[name]):
+ os.remove(self.file[name])
+
+class DHCP (object):
+ def __init__(self, ifname):
+ self.v4 = _DHCPv4(ifname)
+ self.v6 = _DHCPv6(ifname)
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index 22c71a464..43f823eca 100644
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -41,8 +41,10 @@ from vyos.validate import assert_mtu
from vyos.validate import assert_positive
from vyos.validate import assert_range
+from vyos.ifconfig.control import Control
-class Interface(DHCP):
+
+class Interface(Control):
options = []
required = []
default = {
@@ -181,7 +183,8 @@ class Interface(DHCP):
self.config['ifname'] = ifname
# we must have updated config before initialising the Interface
- super().__init__(ifname, **kargs)
+ super().__init__(**kargs)
+ self.dhcp = DHCP(ifname)
if not os.path.exists('/sys/class/net/{}'.format(self.config['ifname'])):
# Any instance of Interface, such as Interface('eth0')
@@ -224,8 +227,8 @@ class Interface(DHCP):
>>> i.remove()
"""
# stop DHCP(v6) if running
- self._del_dhcp()
- self._del_dhcpv6()
+ self.dhcp.v4.delete()
+ self.dhcp.v6.delete()
# remove all assigned IP addresses from interface - this is a bit redundant
# as the kernel will remove all addresses on interface deletion, but we
@@ -668,12 +671,13 @@ class Interface(DHCP):
# do not change below 'if' ordering esle you will get an exception as:
# ValueError: 'dhcp' does not appear to be an IPv4 or IPv6 address
if addr != 'dhcp' and is_ipv4(addr):
- raise ConfigError("Can't configure both static IPv4 and DHCP address on the same interface")
+ raise ConfigError(
+ "Can't configure both static IPv4 and DHCP address on the same interface")
if addr == 'dhcp':
- self._set_dhcp()
+ self.dhcp.v4.set()
elif addr == 'dhcpv6':
- self._set_dhcpv6()
+ self.dhcp.v6.set()
else:
if not is_intf_addr_assigned(self.config['ifname'], addr):
cmd = 'ip addr add "{}" dev "{}"'.format(addr, self.config['ifname'])
@@ -702,9 +706,9 @@ class Interface(DHCP):
['2001:db8::ffff/64']
"""
if addr == 'dhcp':
- self._del_dhcp()
+ self.dhcp.v4.delete()
elif addr == 'dhcpv6':
- self._del_dhcpv6()
+ self.dhcp.v6.delete()
else:
if is_intf_addr_assigned(self.config['ifname'], addr):
cmd = 'ip addr del "{}" dev "{}"'.format(addr, self.config['ifname'])
diff --git a/python/vyos/ifconfig_vlan.py b/python/vyos/ifconfig_vlan.py
index ed22646c1..899fd17da 100644
--- a/python/vyos/ifconfig_vlan.py
+++ b/python/vyos/ifconfig_vlan.py
@@ -25,32 +25,20 @@ def apply_vlan_config(vlan, config):
if not vlan.definition['vlan']:
raise TypeError()
- # get DHCP config dictionary and update values
- opt = vlan.get_dhcp_options()
-
if config['dhcp_client_id']:
- opt['client_id'] = config['dhcp_client_id']
+ vlan.dhcp.v4.options['client_id'] = config['dhcp_client_id']
if config['dhcp_hostname']:
- opt['hostname'] = config['dhcp_hostname']
+ vlan.dhcp.v4.options['hostname'] = config['dhcp_hostname']
if config['dhcp_vendor_class_id']:
- opt['vendor_class_id'] = config['dhcp_vendor_class_id']
-
- # store DHCP config dictionary - used later on when addresses are aquired
- vlan.set_dhcp_options(opt)
-
- # get DHCPv6 config dictionary and update values
- opt = vlan.get_dhcpv6_options()
+ vlan.dhcp.v4.options['vendor_class_id'] = config['dhcp_vendor_class_id']
if config['dhcpv6_prm_only']:
- opt['dhcpv6_prm_only'] = True
+ vlan.dhcp.v6.options['dhcpv6_prm_only'] = True
if config['dhcpv6_temporary']:
- opt['dhcpv6_temporary'] = True
-
- # store DHCPv6 config dictionary - used later on when addresses are aquired
- vlan.set_dhcpv6_options(opt)
+ vlan.dhcp.v6.options['dhcpv6_temporary'] = True
# update interface description used e.g. within SNMP
vlan.set_alias(config['description'])