From dbaaa56c53ce9af73601b54351d97ebce21a9df7 Mon Sep 17 00:00:00 2001 From: Thomas Mangin Date: Tue, 24 Mar 2020 17:48:47 +0000 Subject: ifconfig: T2057: break down DHCP --- python/vyos/ifconfig/dhcp.py | 266 +++++++++++++++++++++++++++++++++++++ python/vyos/ifconfig/interface.py | 272 +------------------------------------- 2 files changed, 270 insertions(+), 268 deletions(-) create mode 100644 python/vyos/ifconfig/dhcp.py diff --git a/python/vyos/ifconfig/dhcp.py b/python/vyos/ifconfig/dhcp.py new file mode 100644 index 000000000..8d3653433 --- /dev/null +++ b/python/vyos/ifconfig/dhcp.py @@ -0,0 +1,266 @@ +# Copyright 2020 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see . + +import os +import jinja2 + +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): + client_base = r'/var/lib/dhcp/dhclient_' + + def __init__ (self, ifname): + # 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 + }, + }, + } + + 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 + + # replace dhcpv4/v6 with systemd.networkd? + def _set_dhcp(self): + """ + Configure interface as DHCP client. The dhclient binary is automatically + started in background! + + Example: + + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.set_dhcp() + """ + + dhcp = self.get_dhcp_options() + if not dhcp['hostname']: + # read configured system hostname. + # maybe change to vyos hostd client ??? + with open('/etc/hostname', 'r') as f: + dhcp['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) + + cmd = 'start-stop-daemon' + cmd += ' --start' + cmd += ' --oknodo' + cmd += ' --quiet' + cmd += ' --pidfile {pid}' + cmd += ' --exec /sbin/dhclient' + 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])) + + def _del_dhcp(self): + """ + De-configure interface as DHCP clinet. All auto generated files like + pid, config and lease will be removed. + + Example: + + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.del_dhcp() + """ + if not os.path.isfile(self._dhcp[4]['pid']): + self._debug_msg('No DHCP client PID found') + return None + + # with open(self._dhcp[4]['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: + # 172.16.35.103.68 > 172.16.35.254.67: [bad udp cksum 0xa0cb -> 0xb943!] BOOTP/DHCP, Request from 00:50:56:9d:11:df, length 300, xid 0x620e6946, Flags [none] (0x0000) + # Client-IP 172.16.35.103 + # Client-Ethernet-Address 00:50:56:9d:11:df + # Vendor-rfc1048 Extensions + # Magic Cookie 0x63825363 + # DHCP-Message Option 53, length 1: Release + # Server-ID Option 54, length 4: 172.16.35.254 + # Hostname Option 12, length 10: "vyos" + # + cmd = '/sbin/dhclient -cf {conf} -pf {pid} -lf {lease} -r {ifname}' + self._cmd(cmd.format(**self._dhcp[4])) + + # cleanup old config files + for name in ('conf', 'pid', 'lease'): + if os.path.isfile(self._dhcp[4][name]): + os.remove(self._dhcp[4][name]) + + def _set_dhcpv6(self): + """ + Configure interface as DHCPv6 client. The dhclient binary is automatically + started in background! + + Example: + + >>> from vyos.ifconfig import Interface + >>> 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']: + 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) + + # no longer accept router announcements on this interface + self._write_sysfs(self._dhcp[6]['accept_ra'], 0) + + # assemble command-line to start DHCPv6 client (dhclient) + cmd = 'start-stop-daemon' + cmd += ' --start' + cmd += ' --oknodo' + cmd += ' --quiet' + cmd += ' --pidfile {pid}' + cmd += ' --exec /sbin/dhclient' + cmd += ' --' + # now pass arguments to dhclient binary + cmd += ' -6 -nw -cf {conf} -pf {pid} -lf {lease}' + # add optional arguments + if dhcpv6['dhcpv6_prm_only']: + cmd += ' -S' + if dhcpv6['dhcpv6_temporary']: + cmd += ' -T' + cmd += ' {ifname}' + + return self._cmd(cmd.format(**self._dhcp[6])) + + def _del_dhcpv6(self): + """ + De-configure interface as DHCPv6 clinet. All auto generated files like + pid, config and lease will be removed. + + Example: + + >>> from vyos.ifconfig import Interface + >>> j = Interface('eth0') + >>> j.del_dhcpv6() + """ + if not os.path.isfile(self._dhcp[6]['pid']): + self._debug_msg('No DHCPv6 client PID found') + return None + + # with open(self._dhcp[6]['pid'], 'r') as f: + # pid = int(f.read()) + + # stop dhclient + cmd = 'start-stop-daemon' + cmd += ' --start' + cmd += ' --oknodo' + cmd += ' --quiet' + cmd += ' --pidfile {pid}' + self._cmd(cmd.format(**self._dhcp[6])) + + # accept router announcements on this interface + self._write_sysfs(self._dhcp[6]['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]) + diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 4f72271c9..a1f8198c7 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -15,7 +15,6 @@ import os import re -import jinja2 import json import glob import time @@ -35,41 +34,9 @@ from tabulate import tabulate from hurry.filesize import size,alternative from datetime import timedelta -from vyos.ifconfig.control import Control +from vyos.ifconfig.dhcp import DHCP -dhclient_base = r'/var/lib/dhcp/dhclient_' -dhcp_cfg = """ -# 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; -} - -""" - -dhcpv6_cfg = """ -# generated by ifconfig.py -interface "{{ intf }}" { - request routers, domain-name-servers, domain-name; -} - -""" - - - -class Interface(Control): +class Interface(DHCP): options = [] required = [] default = { @@ -165,6 +132,8 @@ class Interface(Control): >>> i = Interface('eth0') """ + DHCP.__init__(self, ifname) + self.config = deepcopy(self.default) self.config['ifname'] = ifname @@ -183,31 +152,6 @@ class Interface(Control): self._create() - # per interface DHCP config files - self._dhcp_cfg_file = dhclient_base + self.config['ifname'] + '.conf' - self._dhcp_pid_file = dhclient_base + self.config['ifname'] + '.pid' - self._dhcp_lease_file = dhclient_base + self.config['ifname'] + '.leases' - - # per interface DHCPv6 config files - self._dhcpv6_cfg_file = dhclient_base + self.config['ifname'] + '.v6conf' - self._dhcpv6_pid_file = dhclient_base + self.config['ifname'] + '.v6pid' - self._dhcpv6_lease_file = dhclient_base + self.config['ifname'] + '.v6leases' - - # DHCP options - self._dhcp_options = { - 'intf' : self.config['ifname'], - 'hostname' : '', - 'client_id' : '', - 'vendor_class_id' : '' - } - - # DHCPv6 options - self._dhcpv6_options = { - 'intf' : self.config['ifname'], - 'dhcpv6_prm_only' : False, - 'dhcpv6_temporary' : False - } - # list of assigned IP addresses self._addr = [] @@ -623,214 +567,6 @@ class Interface(Control): cmd = 'ip addr del "{}" dev "{}"'.format(addr, self.config['ifname']) return self._cmd(cmd) - - 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_options - - def set_dhcp_options(self, options): - """ - Store new DHCP options used by next run of DHCP client. - """ - self._dhcp_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._dhcpv6_options - - def set_dhcpv6_options(self, options): - """ - Store new DHCP options used by next run of DHCP client. - """ - self._dhcpv6_options = options - - # replace dhcpv4/v6 with systemd.networkd? - def _set_dhcp(self): - """ - Configure interface as DHCP client. The dhclient binary is automatically - started in background! - - Example: - - >>> from vyos.ifconfig import Interface - >>> j = Interface('eth0') - >>> j.set_dhcp() - """ - - dhcp = self.get_dhcp_options() - if not dhcp['hostname']: - # read configured system hostname. - # maybe change to vyos hostd client ??? - with open('/etc/hostname', 'r') as f: - dhcp['hostname'] = f.read().rstrip('\n') - - # render DHCP configuration - tmpl = jinja2.Template(dhcp_cfg) - dhcp_text = tmpl.render(dhcp) - with open(self._dhcp_cfg_file, 'w') as f: - f.write(dhcp_text) - - cmd = 'start-stop-daemon' - cmd += ' --start ' - cmd += ' --quiet' - cmd += ' --oknodo' - cmd += ' --pidfile ' + self._dhcp_pid_file - cmd += ' --exec /sbin/dhclient --' - # now pass arguments to dhclient binary - cmd += ' -4 -nw -cf {} -pf {} -lf {} {}'.format( - self._dhcp_cfg_file, self._dhcp_pid_file, self._dhcp_lease_file, self.config['ifname']) - return self._cmd(cmd) - - - def _del_dhcp(self): - """ - De-configure interface as DHCP clinet. All auto generated files like - pid, config and lease will be removed. - - Example: - - >>> from vyos.ifconfig import Interface - >>> j = Interface('eth0') - >>> j.del_dhcp() - """ - pid = 0 - if os.path.isfile(self._dhcp_pid_file): - with open(self._dhcp_pid_file, 'r') as f: - pid = int(f.read()) - else: - self._debug_msg('No DHCP client PID found') - return None - - # stop dhclient, we need to call dhclient and tell it should release the - # aquired IP address. tcpdump tells me: - # 172.16.35.103.68 > 172.16.35.254.67: [bad udp cksum 0xa0cb -> 0xb943!] BOOTP/DHCP, Request from 00:50:56:9d:11:df, length 300, xid 0x620e6946, Flags [none] (0x0000) - # Client-IP 172.16.35.103 - # Client-Ethernet-Address 00:50:56:9d:11:df - # Vendor-rfc1048 Extensions - # Magic Cookie 0x63825363 - # DHCP-Message Option 53, length 1: Release - # Server-ID Option 54, length 4: 172.16.35.254 - # Hostname Option 12, length 10: "vyos" - # - cmd = '/sbin/dhclient -cf {} -pf {} -lf {} -r {}'.format( - self._dhcp_cfg_file, self._dhcp_pid_file, self._dhcp_lease_file, self.config['ifname']) - self._cmd(cmd) - - # cleanup old config file - if os.path.isfile(self._dhcp_cfg_file): - os.remove(self._dhcp_cfg_file) - - # cleanup old pid file - if os.path.isfile(self._dhcp_pid_file): - os.remove(self._dhcp_pid_file) - - # cleanup old lease file - if os.path.isfile(self._dhcp_lease_file): - os.remove(self._dhcp_lease_file) - - - def _set_dhcpv6(self): - """ - Configure interface as DHCPv6 client. The dhclient binary is automatically - started in background! - - Example: - - >>> from vyos.ifconfig import Interface - >>> 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']: - raise Exception('DHCPv6 temporary and parameters-only options are mutually exclusive!') - - # render DHCP configuration - tmpl = jinja2.Template(dhcpv6_cfg) - dhcpv6_text = tmpl.render(dhcpv6) - with open(self._dhcpv6_cfg_file, 'w') as f: - f.write(dhcpv6_text) - - # no longer accept router announcements on this interface - self._write_sysfs('/proc/sys/net/ipv6/conf/{}/accept_ra' - .format(self.config['ifname']), 0) - - # assemble command-line to start DHCPv6 client (dhclient) - cmd = 'start-stop-daemon' - cmd += ' --start ' - cmd += ' --quiet' - cmd += ' --oknodo' - cmd += ' --pidfile ' + self._dhcpv6_pid_file - cmd += ' --exec /sbin/dhclient --' - # now pass arguments to dhclient binary - cmd += ' -6 -nw -cf {} -pf {} -lf {}'.format( - self._dhcpv6_cfg_file, self._dhcpv6_pid_file, self._dhcpv6_lease_file) - - # add optional arguments - if dhcpv6['dhcpv6_prm_only']: - cmd += ' -S' - if dhcpv6['dhcpv6_temporary']: - cmd += ' -T' - - cmd += ' {}'.format(self.config['ifname']) - return self._cmd(cmd) - - - def _del_dhcpv6(self): - """ - De-configure interface as DHCPv6 clinet. All auto generated files like - pid, config and lease will be removed. - - Example: - - >>> from vyos.ifconfig import Interface - >>> j = Interface('eth0') - >>> j.del_dhcpv6() - """ - pid = 0 - if os.path.isfile(self._dhcpv6_pid_file): - with open(self._dhcpv6_pid_file, 'r') as f: - pid = int(f.read()) - else: - self._debug_msg('No DHCPv6 client PID found') - return None - - # stop dhclient - cmd = 'start-stop-daemon' - cmd += ' --stop' - cmd += ' --oknodo' - cmd += ' --quiet' - cmd += ' --pidfile ' + self._dhcpv6_pid_file - self._cmd(cmd) - - # accept router announcements on this interface - self._write_sysfs('/proc/sys/net/ipv6/conf/{}/accept_ra' - .format(self.config['ifname']), 1) - - # cleanup old config file - if os.path.isfile(self._dhcpv6_cfg_file): - os.remove(self._dhcpv6_cfg_file) - - # cleanup old pid file - if os.path.isfile(self._dhcpv6_pid_file): - os.remove(self._dhcpv6_pid_file) - - # cleanup old lease file - if os.path.isfile(self._dhcpv6_lease_file): - os.remove(self._dhcpv6_lease_file) - def op_show_interface_stats(self): stats = self.get_interface_stats() rx = [['bytes','packets','errors','dropped','overrun','mcast'],[stats['rx_bytes'],stats['rx_packets'],stats['rx_errors'],stats['rx_dropped'],stats['rx_over_errors'],stats['multicast']]] -- cgit v1.2.3