diff options
| -rw-r--r-- | python/vyos/ifconfig/dhcp.py | 266 | ||||
| -rw-r--r-- | python/vyos/ifconfig/interface.py | 272 | 
2 files changed, 270 insertions, 268 deletions
| 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 <maintainers@vyos.io> +# +# 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 <http://www.gnu.org/licenses/>. + +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']]] | 
